對於 Golang 開發人員來說context
(上下文)包一定不會陌生。但很多時候,我們懶惰的只是見過它,或能起到什麼作用,並不會去深究它。
應用情境:在 Go http 包的 Server 中,每一個請求在都有一個對應的goroutine
去處理。請求處理函數通常會啟動額外的goroutine
用來訪問後端服務,比如資料庫和 RPC 服務。用來處理一個請求的goroutine
通常需要訪問一些與請求特定的資料,比如終端使用者的身份認證資訊、驗證相關的 token、請求的截止時間。當一個請求被取消或逾時時,所有用來處理該請求的goroutine
都應該迅速退出,然後系統才能釋放這些goroutine
佔用的資源,官方部落格。
注意:go1.6
及之前版本請使用golang.org/x/net/context
。go1.7
及之後已移到標準庫context
。
Context 原理
Context 的調用應該是鏈式的,通過WithCancel
,WithDeadline
,WithTimeout
或WithValue
派生出新的 Context。當父 Context 被取消時,其派生的所有 Context 都將取消。
通過context.WithXXX
都將返回新的 Context 和 CancelFunc。調用 CancelFunc 將取消子代,移除父代對子代的引用,並且停止所有定時器。未能調用 CancelFunc 將泄漏子代,直到父代被取消或定時器觸發。go vet
工具檢查所有流程式控制制路徑上使用 CancelFuncs。
遵循規則
遵循以下規則,以保持包之間的介面一致,並啟用靜態分析工具以檢查上下文傳播。
- 不要將 Contexts 放入結構體,相反
context
應該作為第一個參數傳入,命名為ctx
。 func DoSomething(ctx context.Context,arg Arg)error { // ... use ctx ... }
- 即使函數允許,也不要傳入
nil
的 Context。如果不知道用哪種 Context,可以使用context.TODO()
。
- 使用context的Value相關方法只應該用於在程式和介面中傳遞的和請求相關的中繼資料,不要用它來傳遞一些可選的參數
- 相同的 Context 可以傳遞給在不同的
goroutine
;Context 是並發安全的。
Context 包
Context 結構體。
// A Context carries a deadline, cancelation signal, and request-scoped values// across API boundaries. Its methods are safe for simultaneous use by multiple// goroutines.type Context interface { // Done returns a channel that is closed when this Context is canceled // or times out. Done() <-chan struct{} // Err indicates why this context was canceled, after the Done channel // is closed. Err() error // Deadline returns the time when this Context will be canceled, if any. Deadline() (deadline time.Time, ok bool) // Value returns the value associated with key or nil if none. Value(key interface{}) interface{}}
- Done(),返回一個channel。當times out或者調用cancel方法時,將會close掉。
- Err(),返回一個錯誤。該context為什麼被取消掉。
- Deadline(),返回截止時間和ok。
- Value(),傳回值。
所有方法
func Background() Contextfunc TODO() Contextfunc WithCancel(parent Context) (ctx Context, cancel CancelFunc)func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)func WithValue(parent Context, key, val interface{}) Context
上面可以看到Context是一個介面,想要使用就得實現其方法。在context包內部已經為我們實現好了兩個空的Context,可以通過調用Background()和TODO()方法擷取。一般的將它們作為Context的根,往下派生。
WithCancel 例子
WithCancel 以一個新的 Done channel 返回一個父 Context 的拷貝。
229 func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { 230 c := newCancelCtx(parent) 231 propagateCancel(parent, &c) 232 return &c, func() { c.cancel(true, Canceled) } 233 } 234 235 // newCancelCtx returns an initialized cancelCtx. 236 func newCancelCtx(parent Context) cancelCtx { 237 return cancelCtx{ 238 Context: parent, 239 done: make(chan struct{}), 240 } 241 }
此樣本示範使用一個可取消的上下文,以防止 goroutine 泄漏。樣本函數結束時,defer 調用 cancel 方法,gen goroutine 將返回而不泄漏。
package mainimport ( "context" "fmt")func main() { // gen generates integers in a separate goroutine and // sends them to the returned channel. // The callers of gen need to cancel the context once // they are done consuming generated integers not to leak // the internal goroutine started by gen. gen := func(ctx context.Context) <-chan int { dst := make(chan int) n := 1 go func() { for { select { case <-ctx.Done(): return // returning not to leak the goroutine case dst <- n: n++ } } }() return dst } ctx, cancel := context.WithCancel(context.Background()) defer cancel() // cancel when we are finished consuming integers for n := range gen(ctx) { fmt.Println(n) if n == 5 { break } }}
WithDeadline 例子
369 func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) { 370 if cur, ok := parent.Deadline(); ok && cur.Before(deadline) { 371 // The current deadline is already sooner than the new one. 372 return WithCancel(parent) 373 } 374 c := &timerCtx{ 375 cancelCtx: newCancelCtx(parent), 376 deadline: deadline, 377 } ......
可以清晰的看到,當派生出的子 Context 的deadline在父Context之後,直接返回了一個父Context的拷貝。故語義上等效為父。
WithDeadline 的期限調整為不晚於 d 返回父內容相關的副本。如果父母的到期日已經早於 d,WithDeadline (父,d) 是在語義上等效為父。返回的上下文完成的通道關閉的期限期滿後,返回的取消函數調用時,或當父上下文完成的通道關閉,以先發生者為準。
看看官方例子:
package mainimport ( "context" "fmt" "time")func main() { d := time.Now().Add(50 * time.Millisecond) ctx, cancel := context.WithDeadline(context.Background(), d) // Even though ctx will be expired, it is good practice to call its // cancelation function in any case. Failure to do so may keep the // context and its parent alive longer than necessary. defer cancel() select { case <-time.After(1 * time.Second): fmt.Println("overslept") case <-ctx.Done(): fmt.Println(ctx.Err()) }}
WithTimeout 例子
WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))。
436 func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { 437 return WithDeadline(parent, time.Now().Add(timeout)) 438 }
看看官方例子:
package mainimport ( "context" "fmt" "time")func main() { // Pass a context with a timeout to tell a blocking function that it // should abandon its work after the timeout elapses. ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() select { case <-time.After(1 * time.Second): fmt.Println("overslept") case <-ctx.Done(): fmt.Println(ctx.Err()) // prints "context deadline exceeded" }}
WithValue 例子
454 func WithValue(parent Context, key, val interface{}) Context { 454 if key == nil { 455 panic("nil key") 456 } 457 if !reflect.TypeOf(key).Comparable() { 458 panic("key is not comparable") 459 } 460 return &valueCtx{parent, key, val} 461 }
WithValue 返回的父與鍵關聯的值在 val 的副本。
使用上下文值僅為過渡進程和 Api 的請求範圍的資料,而不是將選擇性參數傳遞給函數。
提供的鍵必須是可比性和應該不是字串類型或任何其他內建的類型以避免包使用的上下文之間的碰撞。WithValue 使用者應該定義自己的鍵的類型。為了避免分配分配給介面 {} 時,上下文鍵經常有具體類型結構 {}。另外,匯出的上下文關鍵變數靜態類型應該是一個指標或介面。
看看官方例子:
package mainimport ( "context" "fmt")func main() { type favContextKey string f := func(ctx context.Context, k favContextKey) { if v := ctx.Value(k); v != nil { fmt.Println("found value:", v) return } fmt.Println("key not found:", k) } k := favContextKey("language") ctx := context.WithValue(context.Background(), k, "Go") f(ctx, k) f(ctx, favContextKey("color"))}
參考串連
[1] https://segmentfault.com/a/1190000006744213
[2] http://www.01happy.com/golang-context-reading/
我的部落格即將入駐“雲棲社區”,誠邀技術同仁一同入駐。