這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
概述
Golang 的 context Package 提供了一種簡潔又強大方式來管理 goroutine 的生命週期,同時提供了一種 Requst-Scope K-V Store。但是對於新手來說,Context 的概念不算非常的直觀,這篇文章來帶領大家瞭解一下 Context 包的基本作用和使用方法。
1. 包的引入
在 go1.7 及以上版本 context 包被正式列入官方庫中,所以我們只需要import "context"就可以了,而在 go1.6 及以下版本,我們要 import "golang.org/x/net/context"
2. Context 基本資料結構
Context interface
Context interface 是最基本的介面
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{}}
Deadline()返回一個time.Time,是當前 Context 的應該結束的時間,ok 表示是否有 deadline
Done()返回一個struct{}類型的唯讀 channel
Err()返回 Context 被取消時的錯誤
Value(key interface{}) 是 Context 內建的 K-V 儲存功能
canceler interface
canceler interface 定義了提供 cancel 函數的 context,當然要求資料結構要同時實現 Context interface
type canceler interface { cancel(removeFromParent bool, err error) Done() <-chan struct{}}
Structs
除了以上兩個 interface 之外,context 包中還定義了若干個struct,來實現上面的 interface
emptyCtx
emptyCtx是空的Context,只實現了Context interface,只能作為 root context 使用。
type emptyCtx int
cancelCtx
cancelCtx繼承了Context並實現了cancelerinterface,從WithCancel()函數產生
type cancelCtx struct { Context done chan struct{} // closed by the first cancel call. mu sync.Mutex children map[canceler]bool // set to nil by the first cancel call err error // set to non-nil by the first cancel call}
timerCtx
timerCtx繼承了cancelCtx,所以也自然實現了Context和canceler這兩個interface,由WithDeadline()函數產生
type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu.deadline time.Time}
valueCtx
valueCtx包含key、val field,可以儲存一對索引值對,由WithValue()函數產生
type valueCtx struct { Context key, val interface{}}
3. Context 執行個體化和派生
Context 只定義了 interface,真正使用時需要執行個體化,官方首先定義了一個 emptyCtx struct 來實現 Context interface,然後提供了Backgroud()函數來便利的產生一個 emptyCtx 執行個體。
實現代碼如下
// An emptyCtx is never canceled, has no values, and has no deadline. It is not// struct{}, since vars of this type must have distinct addresses.type emptyCtx intfunc (*emptyCtx) Deadline() (deadline time.Time, ok bool) { return}func (*emptyCtx) Done() <-chan struct{} { return nil}func (*emptyCtx) Err() error { return nil}func (*emptyCtx) Value(key interface{}) interface{} { return nil}func (e *emptyCtx) String() string { switch e { case background: return "context.Background" case todo: return "context.TODO" } return "unknown empty Context"}var ( background = new(emptyCtx) todo = new(emptyCtx))func Background() Context { return background}
Backgroud() 產生的 emptyCtx 執行個體是不能取消的,因為emptyCtx沒有實現canceler interface,要正常取消功能的話,還需要對 emptyCtx 執行個體進行派生。常見的兩種派生用法是WithCancel() 和 WithTimeout。
WithCancel
調用WithCancel()可以將基礎的 Context 進行繼承,返回一個cancelCtx樣本,並返回一個函數,可以在外層直接調用cancelCtx.cancel()來取消 Context
代碼如下:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) }}// newCancelCtx returns an initialized cancelCtx.func newCancelCtx(parent Context) cancelCtx { return cancelCtx{ Context: parent, done: make(chan struct{}), }}
WithTimeout
調用WithTimeout,需要傳一個逾時時間。來指定過多長時間後逾時結束 Context,原始碼中可以得知WithTimeout是WithDeadline的一層皮,WithDeadline傳的是具體的結束時間點,這個在工程中並不實用,WithTimeout會根據運行時的時間做轉換。
原始碼如下:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout))func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) { if cur, ok := parent.Deadline(); ok && cur.Before(deadline) { // The current deadline is already sooner than the new one. return WithCancel(parent) } c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: deadline, } propagateCancel(parent, c) d := deadline.Sub(time.Now()) if d <= 0 { c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(true, Canceled) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(d, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) }}
在WithDeadline中,將 timeCtx.timer 掛上結束時的回呼函數,回呼函數的內容是調用cancel來結束 Context。
WithValue
WithValue的具體使用方法在下面的用例中會講。
原始碼如下:
func WithValue(parent Context, key, val interface{}) Context { if key == nil { panic("nil key") } if !reflect.TypeOf(key).Comparable() { panic("key is not comparable") } return &valueCtx{parent, key, val}}
4. 實際用例
(1)逾時結束樣本
我們起一個本地的 http serice,名字叫"lazy",這個 http server 會隨機的發出一些慢請求,要等6秒以上才返回,我們使用這個程式來類比我們的被呼叫者 hang 住的情況
package mainimport ( "net/http" "math/rand" "fmt" "time")func lazyHandler(w http.ResponseWriter, req *http.Request) { ranNum := rand.Intn(2) if ranNum == 0 { time.Sleep(6 * time.Second) fmt.Fprintf(w, "slow response, %d\n", ranNum) fmt.Printf("slow response, %d\n", ranNum) return } fmt.Fprintf(w, "quick response, %d\n", ranNum) fmt.Printf("quick response, %d\n", ranNum) return}func main() { http.HandleFunc("/", lazyHandler) http.ListenAndServe(":9200", nil)}
然後我們寫一個主動調用的 http service,他會調用我們剛才寫的"lazy",我們使用 context,來解決超過2秒的慢請求問題,如下代碼:
package mainimport ( "context" "net/http" "fmt" "sync" "time" "io/ioutil")var ( wg sync.WaitGroup)type ResPack struct { r *http.Response err error}func work(ctx context.Context) { tr := &http.Transport{} client := &http.Client{Transport: tr} defer wg.Done() c := make(chan ResPack, 1) req, _ := http.NewRequest("GET", "http://localhost:9200", nil) go func() { resp, err := client.Do(req) pack := ResPack{r: resp, err: err} c <- pack }() select { case <-ctx.Done(): tr.CancelRequest(req) <-c fmt.Println("Timeout!") case res:= <-c: if res.err != nil { fmt.Println(res.err) return } defer res.r.Body.Close() out, _ := ioutil.ReadAll(res.r.Body) fmt.Printf("Server Response: %s", out) } return}func main() { ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second) defer cancel() wg.Add(1) go work(ctx) wg.Wait() fmt.Println("Finished")}
在 main 函數中,我們定義了一個逾時時間為2秒的 context,傳給真正做事的work(),work接收到這個 ctx 的時候,需要等待 ctx.Done() 返回,因為 channel 關閉的時候,ctx.Done() 會受到空值,當 ctx.Done()返回時,就意味著 context 已經逾時結束,要做一些掃尾工作然後 return 即可。
(2)使用 WithValue 製作產生 Request ID 中介軟體
在 Golang1.7 中,"net/http"原生支援將Context嵌入到 *http.Request中,並且提供了http.Request.Conext() 和 http.Request.WithContext()這兩個函數來建立一個 context 和 將 context 加入到一個http.Request執行個體中。下面的程式示範了一下利用WithValue()建立一個可以儲存 K-V 的 context,然後寫一個中介軟體來自動擷取 http頭部的 "X-Rquest-ID"值,加入到 context 中,使業務函數可以直接取到該值。
package mainimport ( "net/http" "context" "fmt")const requestIDKey = "rid"func newContextWithRequestID(ctx context.Context, req *http.Request) context.Context { reqID := req.Header.Get("X-Request-ID") if reqID == "" { reqID = "0" } return context.WithValue(ctx, requestIDKey, reqID)}func requestIDFromContext(ctx context.Context) string { return ctx.Value(requestIDKey).(string)}func middleWare(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { ctx := newContextWithRequestID(req.Context(), req) next.ServeHTTP(w, req.WithContext(ctx)) })}func h(w http.ResponseWriter, req *http.Request) { reqID := requestIDFromContext(req.Context()) fmt.Fprintln(w, "Request ID: ", reqID) return}func main() { http.Handle("/", middleWare(http.HandlerFunc(h))) http.ListenAndServe(":9201", nil)}