對Go語言中的context包源碼分析
一、包說明分析
context包:這個包分析的是1.15
context
包定義瞭一個Context類型(接口類型),通過這個Context
接口類型, 就可以跨api邊界/跨進程傳遞一些deadline/cancel信號/request-scoped
值.
發給server
的請求中需要包含Context,server需要接收Context. 在整個函數調用鏈中,Context都需要進行傳播. 期間是可以選擇將Context替換為派生Context(由With-系列函數生成). 當一個Context是canceled
狀態時,所有派生的Context都是canceled狀態.
With-系列函數(不包含WithValue)會基於父Context來生成一個派生Context, 還有一個CancelFunc函數,調用這個CancelFun函數可取消派生對象和 "派生對象的派生對象的…",並且會刪除父Context和派生Context的引用關系, 最後還會停止相關定時器.如果不調用CancelFunc
,直到父Context被取消或 定時器觸發,派生Context和"派生Context的派生Context…"才會被回收, 否則就是泄露leak. go vet工具可以檢測到泄露.
使用Context包的程序需要遵循以下以下規則,目的是保持跨包兼容, 已經使用靜態分析工具來檢查context的傳播:
Context
不要存儲在struct內,直接在每個函數中顯示使用,作為第一個參數,名叫ctx- 即使函數允許,也不要傳遞
nil Context
,如果實在不去確定就傳context.TODO
- 在跨進程和跨api時,要傳
request-scoped
數據時用context Value,不要傳函數的可選參數 - 不同協程可以傳遞同一Context到函數,多協程並發使用Context是安全的
二、包結構分析
核心的是:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) type CancelFunc type Context
從上可以看出,核心的是Context
接口類型,圍繞這個類型出現瞭With-系列函數, 針對派生Context,還有取消函數CancelFunc.
還有兩個暴露的變量:
Canceled
context取消時由Context.Err方法返回
DeadlineExceeded
context超過deadline時由Context.Err方法返回
三、Context接口類型分析
context也稱上下文.
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
先看說明:
跨api時,Context
可以攜帶一個deadline/一個取消信號/某些值. 並發安全.
方法集分析:
Deadline
- 返回的是截至時間
- 這個時間表示的是任務完成時間
- 到這個時間點,Context的狀態已經是be canceled(完成狀態)
- ok為false表示沒有設置deadline
- 連續調用,返回的結果是相同的
Done
- 返回的隻讀信道
- 任務完成,信道會被關閉,
Context
狀態是be canceled Conetext
永遠不be canceled,Done可能返回nil- 連續調用,返回的結果是相同的
- 信道的關閉會異步發生,且會在取消函數CancelFunc執行完之後發生
- 使用方面,Done需要配合select使用
- 更多使用Done的例子在這個博客
Err
- Done還沒關閉(此處指Done返回的隻讀信道),Err返回nil
- Done關閉瞭,Err返回non-nil的error
- Context是be canceled,Err返回Canceled(這是之前分析的一個變量)
- 如果是超過瞭截至日期deadline,Err返回DeadlineExceeded
- 如果Err返回non-nil的error,後續再次調用,返回的結果是相同的
Value
- 參數和返回值都是interface{}類型(這種解耦方式值得學習)
- Value就是通過key找value,如果沒找到,返回nil
- 連續調用,返回的結果是相同的
- 上下文值,隻適用於跨進程/跨api的request-scoped數據
- 不適用於代替函數可選項
- 一個上下文中,一個key對應一個value
- 典型用法:申請一個全局變量來放key,在context.WithValue/Context.Value中使用
- key應該定義為非暴露類型,避免沖突
- 定義key時,應該支持類型安全的訪問value(通過key)
- key不應該暴露
- 表示應該通過暴露函數來進行隔離(具體可以查看源碼中的例子)
四、後續分析規劃
看完Context的接口定義後,還需要查看With-系列函數才能知道context的定位, 在With-系列中會涉及到Context
的使用和內部實現,那就先看WithCancel.
withCancel:
- CancelFunc
- newCancelCtx
- cancelCtx
- canceler
- propagateCancel
- parentCancelCtx
以下是分析出的通過規則:很多包對外暴露的是接口類型和幾個針對此類型的常用函數. 接口類型暴露意味可擴展,但是想擴展之後繼續使用常用函數,那擴展部分就不能 修改常用函數涉及的部分,當然也可以通過額外的接口繼續解耦. 針對"暴露接口和常用函數"這種套路,實現時會存在一個非暴露的實現類型, 常用函數就是基於這個實現類型實現的.在context.go中的實現類型是emptyCtx. 如果同時需要擴展接口和常用函數,最好是重新寫一個新包.
下面的分析分成兩部分:基於實現類型到常用函數;擴展功能以及如何擴展.
五、基於實現類型到常用函數
Context接口的實現類型是emptyCtx.
type emptyCtx int func (*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" }
可以看到emptyCtx
除瞭實現瞭context.Context接口,還實現瞭context.stringer
接口, 註意下面的String不是實現的fmt.Stringer接口,而是未暴露的context.stringer
接口. 正如empty的名字,對Context接口的實現都是空的,後續需要針對emptyCtx做擴展.
var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo }
這裡通過兩個暴露的函數創建兩個空的emptyCtx
實例,後續會根據不同場景來擴展. 在註釋中,background實例的使用場景是:main函數/初始化/測試/或者作為top-level 的Context(派生其他Context);todo實例的使用場景是:不確定時用todo. 到此emptyCtx的構造就理順瞭,就是Background()/TODO()
兩個函數,之後是針對她們 的擴展和Context派生.
Context
派生是基於With-系列函數實現的,我們先看對emptyCtx的擴展, 這些擴展至少會覆蓋一部分函數,讓空的上下文變成支持某些功能的上下文, 取消信號/截至日期/值,3種功能的任意組合.
從源碼中可以看出,除瞭emptyCtx,還有cancelCtx/myCtx/myDoneCtx/otherContext/ timeCtx/valueCtx
,他們有個共同特點:基於Context組合的新類型, 我們尋找的對emptyCtx
的擴展,就是在這些新類型的方法中.
小技巧:emptyCtx已經實現瞭context.Context
,如果要修改方法的實現, 唯一的方法就是利用Go的內嵌進行方法的覆蓋.簡單點說就是內嵌到struct, struct再定義同樣簽名的方法,如果不需要數據,內嵌到接口也是一樣的.
cancelCtx
支持取消信號的上下文
type cancelCtx struct { Context mu sync.Mutex done chan struct{} children map[canceler]struct{} err error }
看下方法:
var cancelCtxKey int func (c *cancelCtx) Value(key interface{}) interface{} { if key == &cancelCtxKey { return c } return c.Context.Value(key) } func (c *cancelCtx) Done() <-chan struct{} { c.mu.Lock() if c.done == nil { c.done = make(chan struct{}) } d := c.done c.mu.Unlock() return d } func (c *cancelCtx) Err() error { c.mu.Lock() err := c.err c.mu.Unlock() return err }
cancelCtxKey
默認是0,Value()要麼返回自己,要麼調用上下文Context.Value(), 具體使用後面再分析;Done()返回cancelCtx.done;Err()返回cancelCtx.err;
func contextName(c Context) string { if s, ok := c.(stringer); ok { return s.String() } return reflectlite.TypeOf(c).String() } func (c *cancelCtx) String() string { return contextName(c.Context) + ".WithCancel" }
internal/reflectlite.TypeOf
是獲取接口動態類型的反射類型, 如果接口是nil就返回nil,此處是獲取Context的類型, 從上面的分析可知,頂層Context要麼是background,要麼是todo, cancelCtx實現的context.stringer要麼是context.Background.WithCancel, 要麼是context.TODO.WithCancel.這裡說的隻是頂層Context下的, 多層派生Context的結構也是類似的.
值得註意的是String()不屬於Context接口的方法集,而是emptyCtx對 context.stringer接口的實現,cancelCxt內嵌的Context,所以不會覆蓋 emptyCtx對String()的實現.
var closedchan = make(chan struct{}) func init() { close(closedchan) } func (c *cancelCtx) cancel(removeFromParent bool, err error) { if err == nil { panic("context: internal error: missing cancel error") } c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled } c.err = err if c.done == nil { c.done = closedchan } else { close(c.done) } for child := range c.children { // NOTE: acquiring the child's lock while holding parent's lock. child.cancel(false, err) } c.children = nil c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) } }
cancel()
,具體的取消信令對應的操作,err不能為nil,err會存到cancelCtx.err, 如果已經存瞭,表示取消操作已經執行.關閉done信道,如果之前沒有調用Done() 來獲取done信道,就返回一個closedchan(這是要給已關閉信道,可重用的), 之後是調用children的cancel(),最後就是在Context樹上移除當前派生Context.
func parentCancelCtx(parent Context) (*cancelCtx, bool) { done := parent.Done() if done == closedchan || done == nil { return nil, false } p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) if !ok { return nil, false } p.mu.Lock() ok = p.done == done p.mu.Unlock() if !ok { return nil, false } return p, true } func removeChild(parent Context, child canceler) { p, ok := parentCancelCtx(parent) if !ok { return } p.mu.Lock() if p.children != nil { delete(p.children, child) } p.mu.Unlock() }
removeChild
首先判斷父Context是不是cancelCtx類型, 再判斷done信道和當前Context的done信道是不是一致的, (如果不一致,說明:done信道是diy實現的,就不能刪掉瞭).
到此,cancelCtx覆蓋瞭cancelCtx.Context
的Done/Err/Value, 同時實現瞭自己的打印函數String(),還實現瞭cancel(). 也就是說cancelCtx還實現瞭接口canceler:
type canceler interface { cancel(removeFromParent bool, err error) Done() <-chan struct{} } // cancelCtx.children的定義如下: // children map[canceler]struct{}
執行取消信號對應的操作時,其中有一步就是執行children的cancel()
, children
的key是canceler接口類型,所以有對cancel()的實現. cancelCtx實現瞭canceler接口,那麼在派生Context就可以嵌套很多層, 或派生很多個cancelCtx.
func newCancelCtx(parent Context) cancelCtx { return cancelCtx{Context: parent} }
非暴露的構造函數.
回顧一下:cancelCtx添加瞭Context對取消信號的支持. 隻要觸發瞭"取消信號",使用方隻需要監聽done信道即可.
myCtx myDoneCtx otherContext屬於測試,等分析測試的時候再細說.
timerCtx
前面說到瞭取消信號對應的上下文cancelCtx,timerCtx就是基於取消信號上下擴展的
type timerCtx struct { cancelCtx timer *time.Timer deadline time.Time }
註釋說明:內嵌cancelCtx是為瞭復用Done和Err,擴展瞭一個定時器和一個截至時間, 在定時器觸發時觸發cancelCtx.cancel()即可.
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { return c.deadline, true } func (c *timerCtx) String() string { return contextName(c.cancelCtx.Context) + ".WithDeadline(" + c.deadline.String() + " [" + time.Until(c.deadline).String() + "])" } func (c *timerCtx) cancel(removeFromParent bool, err error) { c.cancelCtx.cancel(false, err) if removeFromParent { removeChild(c.cancelCtx.Context, c) } c.mu.Lock() if c.timer != nil { c.timer.Stop() c.timer = nil } c.mu.Unlock() }
timerCtx內嵌瞭cancelCtx,說明timerCtx也實現瞭canceler接口, 從源碼中可以看出,cancel()是重新實現瞭,String/Deadline都重新實現瞭.
cancel()中額外添加瞭定時器的停止操作.
這裡沒有deadline
設置和定時器timer開啟的操作,會放在With-系列函數中.
回顧一下: Context的deadline是機會取消信號實現的.
valueCtx
valueCtx
和timerCtx不同,是直接基於Context的.
type valueCtx struct { Context key, val interface{} }
一個valueCtx附加瞭一個kv對.實現瞭Value
和String
.
func stringify(v interface{}) string { switch s := v.(type) { case stringer: return s.String() case string: return s } return "<not Stringer>" } func (c *valueCtx) String() string { return contextName(c.Context) + ".WithValue(type " + reflectlite.TypeOf(c.key).String() + ", val " + stringify(c.val) + ")" } func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key) }
因為valueCtx.val
類型是接口類型interface{},所以獲取具體值時, 使用瞭switch type.
六、With-系列函數
支持取消信號 WithCancel:
var Canceled = errors.New("context canceled") func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { if parent == nil { panic("cannot create context from nil parent") } c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) } }
派生一個支持取消信號的Context
,類型是cancelCtx
,CancelFunc
是取消操作, 具體是調用cancelCtx.cancel()函數,err參數是Canceled.
func propagateCancel(parent Context, child canceler) { done := parent.Done() if done == nil { return // parent is never canceled } select { case <-done: // parent is already canceled child.cancel(false, parent.Err()) return default: } if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() if p.err != nil { // parent has already been canceled child.cancel(false, p.err) } else { if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() } else { atomic.AddInt32(&goroutines, +1) go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): } }() } }
傳播取消信號.如果父Context
不支持取消信號,那就不傳播. 如果父Context的取消信號已經觸發(就是父Context的done信道已經觸發或關閉), 之後判斷父Context是不是cancelCtx,如果是就將此Context丟到children中, 如果父Context不是cancelCtx,那就起協程監聽父子Context的done信道.
小技巧:
select { case <-done: child.cancel(false, parent.Err()) return default: }
不加default
,會等到done信道有動作;加瞭會立馬判斷done信道,done沒操作就結束select.
select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): }
這個會等待,因為沒有加default.
因為頂層Context目前隻能是background
和todo,不是cancelCtx, 所以頂層Context的直接派生Context不會觸發propagateCancel中的和children相關操作, 至少得3代及以後才有可能.
WithCancel
的取消操作會釋放相關資源,所以在上下文操作完之後,最好盡快觸發取消操作. 觸發的方式是:done信道觸發,要麼有數據,要麼被關閉.
支持截至日期 WithDeadline:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if parent == nil { panic("cannot create context from nil parent") } if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) } c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c) dur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(false, Canceled) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) } }
With-系列函數用於生成派生Context,函數內部第一步都是判斷父Context是否為nil, WithDeadline第二步是判斷父Context是否支持deadline
,支持就將取消信號傳遞給派生Context, 如果不支持,就為當前派生Context支持deadline.
先理一下思路,目前Context的實現類型有4個:emptyCtx/cancelCtx/timerCtx/valueCtx,
除瞭emptyCtx,實現Deadline()方法的隻有timerCtx,(ps:這裡的實現特指有業務意義的實現), 唯一可以構造timerCtx的隻有WithDeadline
的第二步中. 這麼說來,頂層Context不支持deadline,最多第二層派生支持deadline的Context, 第三層派生用於將取消信號進行傳播.
WithCancel上面已經分析瞭,派生一個支持取消信號的Context,並將父Context的取消信號 傳播到派生Context(ps:這麼說有點繞,簡單點講就是將派生Context添加到父Context的children), 下面看看第一個構造支持deadline的過程.
構造timerCtx,傳播取消信號,判斷截至日期是否已過,如果沒過,利用time.AfterFunc
創建定時器, 設置定時觸發的協程處理,之後返回派生Context和取消函數.
可以看到,整個WithDeadline
是基於WithCancel實現的,截至日期到期後,利用取消信號來做後續處理.
因為timerCtx是內嵌瞭cancelCtx,所以有一個派生Context是可以同時支持取消和deadline的, 後面的value支持也是如此.
WithDeadline的註釋說明: 派生Context的deadline不晚於參數,如果參數晚於父Context支持的deadline,使用父Context的deadline, 如果參數指定的比父Context早,或是父Context不支持deadline,那麼派生Context會構造一個新的timerCtx. 父Context的取消/派生Context的取消/或者deadline的過期,都會觸發取消信號對應的操作執行, 具體就是Done()信道會被關閉.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) }
WitchTimeout
是基於WithDeadline
實現的,是一種擴展,從設計上可以不加,但加瞭會增加調用者的便捷. WithTimeout可用在"慢操作"上.上下文使用完之後,應該立即調用取消操作來釋放資源.
支持值WitchValue:
func WithValue(parent Context, key, val interface{}) Context { if parent == nil { panic("cannot create context from nil parent") } if key == nil { panic("nil key") } if !reflectlite.TypeOf(key).Comparable() { panic("key is not comparable") } return &valueCtx{parent, key, val} }
隻要是key能比較,就構造一個valueCtx
,用Value()獲取值時,如果和當前派生Context的key不匹配, 就會和父Context的key做匹配,如果不匹配,最後頂層Context會返回nil.
總結一下:如果是Value(),會一直通過派生Context
找到頂層Context
; 如果是deadline
,會返回當前派生Context的deadline,但會受到父Context的deadline和取消影響; 如果是取消函數,會將傳播取消信號的相關Context都做取消操作. 最重要的是Context是一個樹形結構,可以組成很復雜的結構.
到目前為止,隻瞭解瞭包的內部實現(頂層Context的構造/With-系列函數的派生), 具體使用,需要看例子和實際測試.
ps:一個包內部如何復雜,對外暴露一定要簡潔.一個包是無法設計完美的,但是約束可以, 當大傢都接受一個包,並接受使用包的規則時,這個包就成功瞭,context就是典型.
對於值,可以用WithValue派生,用Value取; 對於cancel/deadline,可以用WithDeadline/WithTimeout派生,通過Done信號獲取結束信號, 也可以手動用取消函數來觸發取消操作.整個包的功能就這麼簡單.
七、擴展功能以及如何擴展
擴展功能現在支持取消/deadline/value,擴展這個層級不應該放在這個包, 擴展Context,也就是新建Context的實現類型,這個是可以的, 同樣實現類型需要承載擴展功能,也不合適.
type canceler interface { cancel(removeFromParent bool, err error) Done() <-chan struct{} }
接口canceler
是保證取消信號可以在鏈上傳播,cancel方法由cancelCtx/timerCtx
實現, Done隻由cancelCtx創建done信道,不管是從功能上還是方法上都沒有擴展的必要.
剩下的就是Value擴展成多kv對,這個主要還是要看應用場景.
八、補充
Context被取消後Err返回Canceled錯誤,超時之後Err返回DeadlineExceeded
錯誤, 這個DeadlineExceeded
還有些說法:
var DeadlineExceeded error = deadlineExceededError{} type deadlineExceededError struct{} func (deadlineExceededError) Error() string { return "context deadline exceeded" } func (deadlineExceededError) Timeout() bool { return true } func (deadlineExceededError) Temporary() bool { return true }
再看看net.Error接口:
type Error interface { error Timeout() bool // Is the error a timeout? Temporary() bool // Is the error temporary? }
context中的DeadlineExceeded默認是實現瞭net.Error接口的實例. 這個是為後面走網絡超時留下的擴展.
到此這篇關於對Go語言中的context包源碼分析的文章就介紹到這瞭,更多相關Go語言context包源碼分析內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Go gRPC超時控制Deadlines用法詳解
- Go語言context上下文管理的使用
- Go語言並發編程基礎上下文概念詳解
- Go 協程超時控制的實現
- golang在GRPC中設置client的超時時間