、http基礎篇
簡介
http(超文本傳輸文本協議), 用于web應用傳輸數據的協議, 只能由客戶端發起, 由服務端響應。 具有無狀態等特點。
結構
http協議的傳輸單位是http報文(請求報文、響應報文)。 報文的結構可分為:請求/響應行、 首部字段、實體部分。
get請求報文
GET /index.html HTTP/1.1 //請求行
Host: test.com //首部字段
1
2
3
get響應報文
HTTP/1.1 200 OK //響應行
Date: Tue, 10 Jul 2012 06;50:15 GMT //首部字段
Content-Length: 362 //首部字段
Content-Type: text/html //首部字段
<html> //實體
...
1
2
3
4
5
6
7
請求行用于說明請求方法 , 請求地址, http版本號
響應行用于說明服務器http版本號, 響應狀態碼, 狀態碼的原因短句
首部字段分為: 通用首部字段、 請求首部字段、 響應首部字段、 實體首部字段
對于實體內的內容, 可以用實體首部字段加以說明。 最常使用的是content-type: xxxx, 說明實體內容的類型。
二、javaScript操作http
瀏覽器中, http請求可以由瀏覽器中的如下內容發送:
1. 瀏覽器中的url地址欄
2. 頁面有src屬性的標簽(img、script、 link等)
3. 帶有action屬性的form表單
4. XMLHttpRequest對象
1. XMLHttpRequest的基本用法
在這些方法中, XMLHttpRequest對象提供了接口讓我們操作http.基本用法如下:
var xhr=new XMLHttpRequest();//此時readyState屬性值為0
xhr.open('post', 'http://www.test.com', false)//此時readyState屬性值為1
xhr.send("name=yang&psd=123")//readyState屬性值為2
xhr.onreadyStatechange=function(){
if(xhr.readState===4 && xhr.status===200 ){
console.log(xhr.responseText)
}else{
console.log('Request was unsuccessfull:' + xhr.status)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
以上是XMLHttpRequest的基本使用方法。
1). 發送數據, 使用send方法
這里的發送數據指的是post方法發送數據
xhr.send("name=yang&psd=123")//post方法發送了一個form表單數據
1
如果是get方法則數據拼接到url后面(使用encodeURIComponent()將名和值進行編碼之后), send方法參數必須是null
xhr.open('get', 'http://www.test.com?name='yang'&psd=123, false)//將name和value進行encodeURIComponent編碼, (同cookie的value一樣), 其中open方法最后一個參數代表是否異步
xhr.send(null)//不能不寫
1
2
2). 使用readyState可以查看當前xhr對象的狀態, 狀態有:
0– 沒調用open方法
1– 沒調用send方法
2– 調用send方法, 未接受到響應
3– 正在接受響應, 未接受完成
4– 響應全部接受
3). 獲得響應的狀態, 使用status屬性, 當屬性的值為200表示請求成功
var httpStatus=xhr.status
if(httpStatus===200){
//請求成功,可以做接下來的事情了
}
1
2
3
4
4). 獲得響應的數據,使用responseText屬性
var result=xhr.responseText
1
5). 添加首部字段, 使用setRequestHeader方法
xhr.setRequestHeader('myHeader', 'myValue')//這里必須放在open方法, 和send方法中間, 否則不能成功添加首部字段
1
6). 獲得首部字段, 使用getResponseHeader或getAllResponseHeaders方法
var header=xhr.getResponseHeader('myHeader')//傳入首部字段名
var headers=xhr.getAllResponseHeader()//獲得全部的首部字段,返回多行文本內容
//這是headers的結果
Date: Sun, 14 Nov 2004 18:04:03 GMT
Server: Apache/1.3.29(Unix)
Vary: Accept
X-Powered-By: PHP/4.3.8
Connection: close
Content-Type: text/html;charset=ios-8859-1
1
2
3
4
5
6
7
8
9
10
2. XMLHttpRequest跨域用法
使用XHR對象通信,有一個限制就是跨域安全策略。 默認情況下, XHR對下只能訪問包含它的頁面位于同一個域中的資源。 但是有時我們開發不能不進行跨域請求。
1). CORS跨域源資源共享
基本思想: 使用自定義的首部字段讓給瀏覽器與服務器溝通, 從而決定請求或響應是否應該成功。
整個CORS通信過程,都是瀏覽器自動完成,不需要用戶參與。對于開發者來說,CORS通信與同源的AJAX通信沒有差別,代碼完全一樣。瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭信息(Origin首部字段),有時還會多出一次附加的請求,但用戶不會有感覺。
2). 原理
客戶端
瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭信息(Origin首部字段),有時還會多出一次附加的請求(分簡單請求),但用戶不會有感覺。
服務端
服務器讀取Origin首部字段的值, 判斷是否應該成功, 如果成功返回的響應報文中首部字段包含Access-control-allow-Origin:xxxxxx。 如果xxxxx為*或與自己發送的Origin的值相同, 瀏覽器就會判斷請求成功。
3). CORS的簡單請求與非簡單請求
局限
CORS跨域請求, 存在以下限制, 例如:
求方法為post/get/head,
首部字段只設置Content-Type
不能訪問響應頭部
cookie不隨請求發送
簡單情求
請求方法為post/get/head, 首部字段只設置content-type(只限于三個值application/x-www-form-urlencoded、multipart/form-data、text/plain等
), 這樣的請求為簡單請求。 這是瀏覽器將會在請求報文中添加Origin的首部字段,完成情趣。
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
1
2
3
4
5
6
非簡單請求
如果不是簡單請求, 瀏覽器將不會想處理簡單請求一樣處理, 例如我們希望添加其他的首部字段。 這瀏覽器將會發送一個預檢請求(Preflighted Requests)
Preflighted Requests,如下
OPTIONS /cors HTTP/1.1 //請求的方法, 地址, http版本
Origin: http://api.bob.com // 客戶端的域名
Access-Control-Request-Method: PUT //即將發起非簡單請求的方法, 用于服務器判斷是否支持該方法
Access-Control-Request-Headers: X-Custom-Header //即將發起非簡單請求攜帶的首部字段, 用于服務器判斷是否支持該字段
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
1
2
3
4
5
6
7
8
這種請求的方法是options方法, 用于服務器詢問。 如果服務都滿足, 將會如下
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com //允許跨域的域
Access-Control-Allow-Methods: GET, POST, PUT //支持的請求方法
Access-Control-Allow-Headers: X-Custom-Header //支持的頭部
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
1
2
3
4
5
6
7
8
9
10
11
12
瀏覽器將會用響應報文的首部字段中以Access-control開頭的字段與即將發送的請求比對, 如果服務將會如同簡單請求一樣發送請求。 故,非簡單請求會有一個預檢請求。
同時, 瀏覽器會將響應按照這個時間:(Access-Control-Max-Age: 1728000)保存, 在該時間未過期期間, 就不必發送預檢請求, 而直接發起請求。
攜帶cookie
默認情況下, 跨域請求不會攜帶cookie。 需要我們設置一個屬性值–withCredentials
xhr.withCredentials=true
1
當然跨域攜帶cookie也需要服務器支持才行, 如果服務愿意接受攜帶cookie的跨域信息, 就會在預檢請求響應頭部添加如下首部字段:
Access-Control-Allow-Credentials: true
1
3. 跨瀏覽器的CORS
function createCORSRequest(method, url){
var xhr=new XMLHttpRequest()
if("withCredentials" in xhr){
xhr.open(method, url, true);
}else if (typeof XDomainRequest() !='undefined') {
xhr=new XDomainRequest()
xhr.open(method, url)
}else{
xhr=null
}
return xhr
}
var request=createCORSRequest('get', 'http://test.com')
if(request){
request.onload=function(){//XMLHttpRequest 2級增加的事件
//對request.responseText進行處理
}
request.send(null)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
總結
詳細了解http呢是有必要的, 對于我們理解很多東西都有非常大的好處。 比如這篇文章, 關于操作http部分, 其重點就是添加實體, 添加首部字段的操作。 而關于添加首部字段呢, 就有必要明白各個首部字段的意義了。
面試過程總會被問到“HTTP協議如何工作?“,”一次完整的http請求是經歷什么過程“...... 確實此題能衡量程序員的功底,如果你回答非常完整,說明你對網絡請求過程是非常了解的,對大流量和大并發場景你就很清楚如何進行優化,本篇文章從輸入URL到瀏覽器顯示頁面發生了什么這視角大體了解一下,當你在瀏覽器地址欄輸入網址后瀏覽器是怎么把最終的頁面呈現出來的呢?這個過程從程序員理解的角度可以分為以下幾個步驟:
我先給大家看看整體的請求過程,為能更好地讓讀者明白,作者會分期完整介紹以下過程。
請求整體過程
域名解析 -> 發起TCP的3次握手 -> 建立TCP連接后發起http請求 -> 服務器響應http請求->瀏覽器得到html代碼 -> 瀏覽器解析html代碼同時請求html代碼中的資源(如js、css、圖片等) -> 瀏覽器對頁面進行渲染呈現給用戶。
獲取內容請求
以上過程大致進行分析細節,以方便大家更加詳細地認識整體的過程,但是有些過程沒有能理解透徹并且過程比較復雜未能提煉通俗易懂語言給大家分析,不過后續會不斷分析給大家的。
1.域名解析
我們以www.cnblogs.com為例:請問www.cnblogs.com這個域名的IP地址是多少?
目的是通過域名地址轉化到資源URL的IP地址,對用戶使用域名是為了方便記憶,但是為了讓計算機理解這個地址還需要把它解析為IP地址,當用戶在地址欄輸入URL中,瀏覽器會首先搜索瀏覽器自身的DNS緩存,先看自身的緩存中是否存在沒有過期對應的條目,如果找到且沒有過期則停止搜索解析到此結束,如果沒有瀏覽器會搜索操作系統的DNS緩存,在操作系統也沒有找到,那么嘗試讀hosts文件,看看里面是否配置對應域名的IP地址,如果在hosts文件中也沒有找到對應的條目,瀏覽器就會發起一次DNS的系統調用,這過程是通過UDP協議向DNS的53端口發起請求遞歸迭代請求,這過程有運營商DNS服務提供給我們,運營商的DNS服務器必須得提供給我們對應域名的IP地址,先向本地配置的首選DNS服務器發起域名解析請求(一般是由電信運營商提供或者各大互聯網廠商提供的DNS服務器)運營商的DNS服務器首先查找自身的緩存,找到對應的條目,且沒有過期,則解析成功。如果沒有找到對應的條目,則運營商的DNS代瀏覽器發起迭代DNS解析請求,它首先是會找根域的DNS的IP地址(這臺DNS服務器都內置13臺根域的DNS的IP地址),找到根域的DNS地址,就會向其發起請求,來一場尋址之旅:
運營商DNS:請問www.cnblogs.com這個域名的IP地址是多少呢?
根域DNS:你一個頂級域com域的一個域名,我不知道這個域名的IP地址,但是我知道com域的IP地址,你去找它去問一問呢?
運營商DNS:請問www.cnblogs.com這個域名的IP地址是多少呢?
COM域:我不知道www.cnblogs.com這個域名的IP地址,但是我知道cnblogs.com這個域的DNS地址,你去找它去去問一問呢?
cnblogs.com域名的DNS:這個時候cnblogs.com域的DNS服務器一查,誒,果真在我這里,一般就是由域名注冊商提供的,像萬網,新網等。
于是就把找到的結果發送給運營商的DNS服務器,這個時候運營商的DNS服務器就拿到了域名對應的IP地址,并返回給操作系統內核,內核又把結果返回給瀏覽器,終于瀏覽器拿到了。
域名解析流程
備注:
瀏覽器:可以使用 chrome://net-internals/#dns 來進行查看
操作系統:Mac的dns緩存查詢 nslookup www.baidu.com
擊上方藍色“Go語言中文網”關注我們,領全套Go資料,每天學習 Go 語言
如果你有運行的 HTTP 服務,你可能想記錄 HTTP 請求。
請求日志有助于診斷問題。(哪些請求失敗了?我們一天處理多少請求?哪些請求比較慢?)
這對于分析是必需的。(哪個頁面受歡迎?網頁的瀏覽者都來自哪里?)
這篇文章介紹了在 Go Web 服務器中,記錄 HTTP 請求日志相關的全部內容。
這不是關于可復用的庫,而是關于實現你自己的解決方案需要知道的事情,以及關于我日志記錄的選擇的描述。
你可以在示例應用上查看詳細內容:https://github.com/essentialbooks/books/tree/master/code/go/logging_http_requests
我在 Web 服務 OnePage[1] 中用到了這個記錄系統。
記錄什么信息[2]
獲取要記錄的信息[3]
日志文件的格式[4]
每日滾動日志[5]
長期存儲以及分析[6]
更多的 Go 資源[7]
招聘 Go 開發者[8]
為了展示通常會記錄什么信息,這里有一條 Apache 的擴展日志文件格式的日志記錄樣本。
111.222.333.123 HOME - [01/Feb/1998:01:08:39 -0800] "GET /bannerad/ad.htm HTTP/1.0" 200 198 "http://www.referrer.com/bannerad/ba_intro.htm" "Mozilla/4.01 (Macintosh; I; PPC)"
我們能看到:
我們可以記錄更多的信息,或者選擇不去記錄上面的某些信息。
個人而言:
Go 中標準 HTTP 處理函數的簽名如下:
func(w http.ResponseWriter, r *http.Request)
我們會把日志記錄作為所謂的中間件,這是一種向 HTTP 服務管道中添加可復用功能的一個方法。
我們有 logReqeustHandler 函數,它以 http.Handler 接口作為參數,然后返回另一個包裝了原有處理器并添加了日志記錄功能的 http.Handler。
func logRequestHandler(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// 在我們包裝的時候調用原始的 http.Handle
h.ServeHTTP(w, r)
// 得到請求的有關信息,并記錄之
uri := r.URL.String()
method := r.Method
// ... 更多信息
logHTTPReq(uri, method, ....)
}
// 用 http.HandlerFunc 包裝函數,這樣就實現了 http.Handler 接口
return http.HandlerFunc(fn)
}
我們可以把中間件處理器嵌套到每一個(HTTP 處理器)的頂部,這樣所有(處理器)都會擁有這些功能。
下面介紹了我們如何使用它來把日志記錄功能添加到所有的請求函數:
func makeHTTPServer() *http.Server {
mux := &http.ServeMux{}
mux.HandleFunc("/", handleIndex)
// ... 可能會添加更多處理器
var handler http.Handler = mux
// 用我們的日志記錄器包裝 mux 。this will (譯者注:應當是注釋沒寫全)
handler = logRequestHandler(handler)
// ... 可能會添加更多中間件處理器
srv := &http.Server{
ReadTimeout: 120 * time.Second,
WriteTimeout: 120 * time.Second,
IdleTimeout: 120 * time.Second, // Go 1.8 開始引進
Handler: handler,
}
return srv
}
首先,我們定義一個 struct 封裝所有需要記錄的信息:
// LogReqInfo 描述了有關 HTTP 請求的信息(譯者注:此處為作者筆誤,應當是 HTTPReqInfo)
type HTTPReqInfo struct {
// GET 等方法
method string
uri string
referer string
ipaddr string
// 響應狀態碼,如 200,204
code int
// 所發送響應的字節數
size int64
// 處理花了多長時間
duration time.Duration
userAgent string
}
下面是 logRequestHandler 的全部實現:
func logRequestHandler(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ri := &HTTPReqInfo{
method: r.Method,
uri: r.URL.String(),
referer: r.Header.Get("Referer"),
userAgent: r.Header.Get("User-Agent"),
}
ri.ipaddr = requestGetRemoteAddress(r)
// this runs handler h and captures information about
// HTTP request
// 這里運行處理器 h 并捕獲有關 HTTP 請求的信息
m := httpsnoop.CaptureMetrics(h, w, r)
ri.code = m.Code
ri.size = m.BytesWritten
ri.duration = m.Duration
logHTTPReq(ri)
}
return http.HandlerFunc(fn)
}
我們復盤下這個簡單的例子:
其他的信息則比較難獲取。
獲取客戶端 IP 地址的問題是有可能涉及到 HTTP 代理。客戶端向代理發起請求,然后代理向我們請求。于是,我們拿到了代理的 IP 地址,而不是客戶端的。
因為這樣,代理通常在請求的 HTTP 頭部信息中以 X-Real-Ip 或者 X-Forwarded-For 來攜帶客戶端真正的 IP 地址。
下面展示了如何提取這個信息:
// Request.RemoteAddress 包含了端口,我們需要把它刪掉,比如: "[::1]:58292" => "[::1]"
func ipAddrFromRemoteAddr(s string) string {
idx := strings.LastIndex(s, ":")
if idx == -1 {
return s
}
return s[:idx]
}
// requestGetRemoteAddress 返回發起請求的客戶端 ip 地址,這是出于存在 http 代理的考量
func requestGetRemoteAddress(r *http.Request) string {
hdr := r.Header
hdrRealIP := hdr.Get("X-Real-Ip")
hdrForwardedFor := hdr.Get("X-Forwarded-For")
if hdrRealIP == "" && hdrForwardedFor == "" {
return ipAddrFromRemoteAddr(r.RemoteAddr)
}
if hdrForwardedFor != "" {
// X-Forwarded-For 可能是以","分割的地址列表
parts := strings.Split(hdrForwardedFor, ",")
for i, p := range parts {
parts[i] = strings.TrimSpace(p)
}
// TODO: 應當返回第一個非本地的地址
return parts[0]
}
return hdrRealIP
}
捕獲響應寫對象(ResponseWriter)的狀態碼以及響應的大小更為困難。
http.ResponseWriter 并沒有給我們這些信息。但幸運的是,這是一個簡單的接口:
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
寫一個包裝了原始響應的接口實現,并記錄我們想要了解的信息,這是可行的。幸運如我們,已經有人在包 httpsnoop[9] 中實現了。
Apache 的日志格式比較緊湊,雖然具備人類可讀性但卻難于解析。
有的時候,我們也需要閱讀日志分析,然后我不贊成為這個格式的實現解析器的想法。
從實現的角度來看,一個簡單的方式是用 JSON 來記錄,并且換行隔開。
對于這種方法我不喜歡的是:JSON 不易于閱讀。
作為一個中間層,我創建了 siser 庫,它實現了一個可擴展,易于實現和人類可讀的序列化格式。它非常適合用于記錄結構化信息,我已經在多個項目用到它了。
下面展示了一個簡單請求是如何被序列化的:
171 1567185903788 httplog
method: GET
uri: /favicon.ico
ipaddr: 204.14.239.58
code: 404
size: 758
duration: 0
ua: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:68.0) Gecko/20100101 Firefox/68.0
每個記錄的第一行包含了以下信息:
然后第一行之后的數據都是 key:value 格式。
下面展示了我們如何序列化一條記錄并把它寫到日志文件:
var (
muLogHTTP sync.Mutex
)
func logHTTPReq(ri *HTTPReqInfo) {
var rec siser.Record
rec.Name = "httplog"
rec.Append("method", ri.method)
rec.Append("uri", ri.uri)
if ri.referer != "" {
rec.Append("referer", ri.referer)
}
rec.Append("ipaddr", ri.ipaddr)
rec.Append("code", strconv.Itoa(ri.code))
rec.Append("size", strconv.FormatInt(ri.size, 10))
durMs := ri.duration / time.Millisecond
rec.Append("duration", strconv.FormatInt(int64(durMs), 10))
rec.Append("ua", ri.userAgent)
muLogHTTP.Lock()
defer muLogHTTP.Unlock()
_, _ = httpLogSiser.WriteRecord(&rec)
}
我通常在 Ubuntu 上部署服務器,并把日志記錄到 /data/<service-name./log 目錄。
我們不能一直往同一個日志文件里寫。否則到最后會用完所有空間。
對于長時間的日志,我通常每天一個日志文件,以日期命名。如 2019-09-23.txt, 2019-09-24.txt 等等。
這有時稱為日志滾動 ( log rotate).
為了避免重復實現這個功能,我寫了一個庫 dailyrotate[10]。
它實現了 Write, Close 以及 Flush 方法,所以它易于接入到現有已使用 io.Reader 等的代碼。
你要指定使用哪個目錄,以及日志命名的格式。這個格式通過 Go 的時間格式化函數來實現的。我通常使用 2006-01-02.txt 每天生成一個唯一的時間,并根據日期來排序,txt 則是工具識別文本文件而不是二進制文件的標志。
接著就和寫普通的文件一樣,以及確保代碼會每天創建文件。
你也可以提供一個通知的回調,當發生日志滾動時會通知你,這樣就可以做一些動作,例如把剛剛關閉的文件上傳線上存儲,或者對它做分析。
下面是代碼:
pathFormat := filepath.Join("dir", "2006-01-02.txt")
func onClose(path string, didRotate bool) {
fmt.Printf("we just closed a file '%s', didRotate: %v\n", path, didRotate)
if !didRotate {
return
}
// process just closed file e.g. upload to backblaze storage for backup
go func() {
// if processing takes a long time, do it in a background goroutine
}()
}
w, err := dailyrotate.NewFile(pathFormat, onClose)
panicIfErr(err)
_, err = io.WriteString(w, "hello\n")
panicIfErr(err)
err = w.Close()
panicIfErr(err)
為了長期存儲我把它們壓縮成 gzip 并把文件上傳到線上存儲。這有很多選擇:S3, Google Storage, Digital Ocean Spaces, BackBlaze。
我傾向于使用 Digital Ocean Spaces 或者 BackBlaze,因為他們足夠廉價(存儲成本和貸款成本)。
它們均支持 S3 協議,所以我使用 go-minio[11] 庫。
為了分析,我每天都會運行代碼,生成大部分有用信息的總結。
還有其他的做法,可以把數據引入到如 BigQuery[12] 的系統。
如果你正在尋找程序員一起工作,希望一起談一下[17]。
由 Krzysztof Kowalczyk[18] 所著。
via: https://onepage.nopub.io/p/Logging-HTTP-requests-in-Go-233de7fe59a747078b35b82a1b035d36
作者:Krzysztof Kowalczyk[19]譯者:LSivan[20]校對:JYSDeveloper[21]
本文由 GCTT[22] 原創編譯,Go 中文網[23] 榮譽推出
[1]
OnePage: https://onepage.nopub.io/
[2]
記錄什么信息: https://onepage.nopub.io/p/Logging-HTTP-requests-in-Go-233de7fe59a747078b35b82a1b035d36#63fd0006-6ebd-442c-a463-d11862e8c33c
[3]
獲取要記錄的信息: https://onepage.nopub.io/p/Logging-HTTP-requests-in-Go-233de7fe59a747078b35b82a1b035d36#c8a27402-1650-402a-8679-69214078b88a
[4]
日志文件的格式: https://onepage.nopub.io/p/Logging-HTTP-requests-in-Go-233de7fe59a747078b35b82a1b035d36#97da9f14-289e-42f6-94fd-936a4eb88f26
[5]
每日滾動日志: https://onepage.nopub.io/p/Logging-HTTP-requests-in-Go-233de7fe59a747078b35b82a1b035d36#99565a90-2f57-4aab-a5e7-5eb9a9194adc
[6]
長期存儲以及分析: https://onepage.nopub.io/p/Logging-HTTP-requests-in-Go-233de7fe59a747078b35b82a1b035d36#a099947d-2079-4d1d-a996-41e4ed1ff02a
[7]
更多的 Go 資源: https://onepage.nopub.io/p/Logging-HTTP-requests-in-Go-233de7fe59a747078b35b82a1b035d36#4405e240-bd60-45a8-ba47-65e175eb7f8f
[8]
招聘 Go 開發者: https://onepage.nopub.io/p/Logging-HTTP-requests-in-Go-233de7fe59a747078b35b82a1b035d36#5076eef2-d176-43f3-bab5-c0d3030efa23
[9]
httpsnoop: https://github.com/felixge/httpsnoop
[10]
dailyrotate: https://github.com/kjk/dailyrotate
[11]
go-minio: https://github.com/minio/minio-go
[12]
BigQuery: https://cloud.google.com/bigquery/what-is-bigquery
[13]
Essential Go: https://www.programming-books.io/essential/go/
[14]
siser: https://github.com/kjk/siser
[15]
深度文章: https://blog.kowalczyk.info/article/fc9203f7c72a4532b1ae51d018fef7b3/trade-offs-in-designing-versatile-log-format.html
[16]
dailyrotate: https://github.com/kjk/dailyrotate
[17]
希望一起談一下: https://blog.kowalczyk.info/goconsultantforhire.html
[18]
Krzysztof Kowalczyk: https://blog.kowalczyk.info/
[19]
Krzysztof Kowalczyk: https://onepage.nopub.io/u/bb760e2dd6794b64b2a903005b21870a
[20]
LSivan: https://github.com/LSivan
[21]
JYSDeveloper: https://github.com/JYSDeveloper
[22]
GCTT: https://github.com/studygolang/GCTT
[23]
Go 中文網: https://studygolang.com/
*請認真填寫需求信息,我們會在24小時內與您取得聯系。