Context通常被稱為上下文,在go中,理解為goroutine的運行狀態、現場,存在上下層goroutine context的傳遞,上層goroutine會把context傳遞給下層goroutine。
每個goroutine在運行前,都要事先知道程序當前的執行狀態,通常將這些狀態封裝在一個 context變量,傳遞給要執行的goroutine中。
在網絡編程中,當接收到一個網絡請求的request,處理request時,可能會在多個goroutine中處理。而這些goroutine可能需要共享Request的一些信息;當request被取消或者超時時,所有從這個request創建的goroutine也要被結束。
go context包不僅實現了在程序單元之間共享狀態變量的方法,同時能通過簡單的方法,在被調用程序單元外部,通過設置ctx變量的值,將過期或撤銷等信號傳遞給被調用的程序單元。在網絡編程中,如果存在A調用B的API,B調用C的 API,如果A調用B取消,那么B調用C也應該被取消,通過在A、B、C調用之間傳遞context,以及判斷其狀態,就能解決此問題。
通過context包,可以非常方便地在請求goroutine之間傳遞請求數據、取消信號和超時信息。
context包的核心時Context接口
// A Context carries a deadline, a cancellation signal, and other values across // API boundaries. // // Context's methods may be called by multiple goroutines simultaneously. type Context interface { // Deadline returns the time when work done on behalf of this context // should be canceled. Deadline returns ok==false when no deadline is // set. Successive calls to Deadline return the same results.
// 返回一個超時時間,到期則取消context。在代碼中,可以通過 deadline 為io操作設置超時時間 Deadline() (deadline time.Time, ok bool) // Done returns a channel that's closed when work done on behalf of this // context should be canceled. Done may return nil if this context can // never be canceled. Successive calls to Done return the same value. // The close of the Done channel may happen asynchronously, // after the cancel function returns. // // WithCancel arranges for Done to be closed when cancel is called; // WithDeadline arranges for Done to be closed when the deadline // expires; WithTimeout arranges for Done to be closed when the timeout // elapses. // // Done is provided for use in select statements: // // // Stream generates values with DoSomething and sends them to out // // until DoSomething returns an error or ctx.Done is closed. // func Stream(ctx context.Context, out chan<- Value) error { // for { // v, err := DoSomething(ctx) // if err != nil { // return err // } // select { // case <-ctx.Done(): // return ctx.Err() // case out <- v: // } // } // } // // See https://blog.golang.org/pipelines for more examples of how to use // a Done channel for cancellation.
// 返回一個channel, 用於接收context的取消或者deadline信號。當channel關閉,監聽done信號的函數會立即放棄當前正在執行的操作並返回。如果 context實例是不可取消的,那么
// 返回 nil, 比如空 context, valueCtx Done() <-chan struct{} // If Done is not yet closed, Err returns nil. // If Done is closed, Err returns a non-nil error explaining why: // Canceled if the context was canceled // or DeadlineExceeded if the context's deadline passed. // After Err returns a non-nil error, successive calls to Err return the same error.
// 返回一個error變量,從其中可以知道為什么context會被取消。 Err() error // Value returns the value associated with this context for key, or nil // if no value is associated with key. Successive calls to Value with // the same key returns the same result. // // Use context values only for request-scoped data that transits // processes and API boundaries, not for passing optional parameters to // functions. // // A key identifies a specific value in a Context. Functions that wish // to store values in Context typically allocate a key in a global // variable then use that key as the argument to context.WithValue and // Context.Value. A key can be any type that supports equality; // packages should define keys as an unexported type to avoid // collisions. // // Packages that define a Context key should provide type-safe accessors // for the values stored using that key: // // // Package user defines a User type that's stored in Contexts. // package user // // import "context" // // // User is the type of value stored in the Contexts. // type User struct {...} // // // key is an unexported type for keys defined in this package. // // This prevents collisions with keys defined in other packages. // type key int // // // userKey is the key for user.User values in Contexts. It is // // unexported; clients use user.NewContext and user.FromContext // // instead of using this key directly. // var userKey key // // // NewContext returns a new Context that carries value u. // func NewContext(ctx context.Context, u *User) context.Context { // return context.WithValue(ctx, userKey, u) // } // // // FromContext returns the User value stored in ctx, if any. // func FromContext(ctx context.Context) (*User, bool) { // u, ok := ctx.Value(userKey).(*User) // return u, ok // }
// 讓context在goroutine之間共享數據,當然,這些數據需要時協程並發安全的。比如,共享了一個map,那么這個map的讀寫要加鎖。 Value(key interface{}) interface{} }
context的使用:
對於goroutine,他們的創建和調用關系總是像層層調用進行的,就像一個樹狀結構,而更靠頂部的context應該有辦法主動關閉下屬的goroutine的執行。為了實現這種關系,context也是一個樹狀結構,葉子節點總是由根節點衍生出來的。
要創建context樹,第一步應該得到根節點,context.Backupgroup函數的返回值就是根節點。
// Background returns a non-nil, empty Context. It is never canceled, has no // values, and has no deadline. It is typically used by the main function, // initialization, and tests, and as the top-level Context for incoming // requests. func Background() Context { return background }
該函數返回空的context,該context一般由接收請求的第一個goroutine創建,是與進入請求對應的context根節點,他不能被取消,也沒有值,也沒有過期時間。他常常作為處理request的頂層的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) {} func WithValue(parent Context, key, val interface{}) Context {}
函數都接收一個Context類型的parent,並返回一個context類型的值,這樣就層層創建除不同的context,子節點是從復制父節點得到,並且根據接收參數設定子節點的一些狀態值,接着就可以將子節點傳遞給下層的goroutine了。
怎么通過context傳遞改變后的狀態呢?
在父goroutine中可以通過Withxx方法獲取一個cancel方法,從而獲得了操作子context的權力。
WithCancel函數,是將父節點復制到子節點,並且返回一個額外的CancelFunc函數類型變量,該函數類型的定義為:type CancelFunc func()
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
// 懶漢式創建,只有在調用Done()方法時,才會創建;該函數返回的是一個只讀的 chan,沒有地方向這個chan中寫數據, 直接讀取協程會被block住;所以一般搭配select來使用;一旦關閉,會立即讀取出零值。
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) 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
// 關閉channel,通知其他協程
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
// 遍歷他的所有子節點 children,
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
// 遞歸的取消所有子 ctx
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
// 從父ctx中移除自己
removeChild(c.Context, c)
}
}
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { c := newCancelCtx(parent)
// 傳播取消行為,根據 parent的情況,進行cancel還是將c添加的parent的children中 propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) } }
// 父context是否是可取消的context
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
if done == closedchan || done == nil {
return nil, false
}
// 如果是 cancelCtx 或者 timerCtx, 則返回 parent,true; 否則返回 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
}}
// propagateCancel arranges for child to be canceled when parent is.
// 把child ctx cancel()關聯到 parent節點上
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
// 如果父類 context 不可取消,直接return
if done == nil {
return // parent is never canceled
}
select {
case <-done:
// parent is already canceled
// 父類context已經canceled, child直接cancel()
child.cancel(false, parent.Err())
return
default:
}
// parent是否是可取消的cancelContext, 如果是,則掛靠上去
if p, ok := parentCancelCtx(parent); ok {
// 如果有
p.mu.Lock()
// err != nil,說明掛靠的parent已經被關閉,child直接cancel()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
// err == nil ,掛靠的parent沒有被關閉 ;將child放入掛靠的parent的children數組中
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
// 走到這里,說明樹上沒有cancelCtx
atomic.AddInt32(&goroutines, +1)
// 新起一個goruntine
go func() {
select {
case <-parent.Done():
// 如果收到取消信號,child cancel
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
調用 CancelFunc 將撤銷對應的子context對象。在父goroutine中,通過 WithCancel 可以創建子節點的 Context, 還獲得了子goroutine的控制權,一旦執行了 CancelFunc函數,子節點Context就結束了,子節點需要如下代碼來判斷是否已經結束,並退出goroutine:
select { case <- ctx.Done(): fmt.Println("do some clean work ...... ") }
WithDeadline函數作用和WithCancel差不多,也是將父節點復制到子節點,但是其過期時間是由deadline和parent的過期時間共同決定。當parent的過期時間早於deadline時,返回的過期時間與parent的過期時間相同。父節點過期時,所有的子孫節點必須同時關閉。
WithTimeout函數和WithDeadline類似,只不過,他傳入的是從現在開始Context剩余的生命時長。他們都同樣也都返回了所創建的子Context的控制權,一個CancelFunc類型的函數變量。
當頂層的Request請求函數結束時,我們可以cancel掉某個context,而子孫的goroutine根據select ctx.Done()來判斷結束。
// 使用WithDeadline 和 WithTimeout 都會生成一個 timerCtx, WithTimeout就是用 WithDeadline實現的。
type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time }
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// 如果父節點的deadline更靠前,那么該 d就可以丟棄,使用父節點的 deadline
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,
}
// 把當前 c節點ctx cancel函數關聯到 parent節點上
propagateCancel(parent, c)
// 獲取到 d的時間
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()
// parent節點到現在還沒有取消
if c.err == nil {
// 到時間,自動退出
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
WithValue函數,返回parent的一個副本,調用該副本的Value(key) 方法將得到value。這樣,我們不僅將根節點原有的值保留了, 還在子孫節點中加入了新的值;注意如果存在key相同,則會覆蓋。
func WithValue(parent Context, key, val interface{}) Context {
// key必須為非空,且可比較 if key == nil { panic("nil key") } if !reflectlite.TypeOf(key).Comparable() { panic("key is not comparable") } return &valueCtx{parent, key, val} }
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
// 這里使用遞歸,c.Context就是 c.Parent
return c.Context.Value(key)
}
小結:
1. context包通過構建樹形關系的context,來達到上一層goroutine對下一層goroutine的控制。對於處理一個request請求操作,需要通過goroutine來層層控制goroutine,以及傳遞一些變量來共享。
2. context變量的請求周期一般為一個請求的處理周期。即針對一個請求創建context對象;在請求處理結束后,撤銷此ctx變量,釋放資源。
3. 每創建一個goroutine,要不將原有context傳遞給子goroutine,要么創建一個子context傳遞給goroutine.
4. Context能靈活地存儲不同類型、不同數目的值,並且使多個Goroutine安全地讀寫其中的值。
5. 當通過父 Context對象創建子Context時,可以同時獲得子Context的撤銷函數,這樣父goroutine就獲得了子goroutine的撤銷權。
原則:
1. 不要把context放到一個結構體中,應該作為第一個參數顯式地傳入函數
2. 即使方法允許,也不要傳入一個nil的context,如果不確定需要什么context的時候,傳入一個context.TODO
3. 使用context的Value相關方法應該傳遞和請求相關的元數據,不要用它來傳遞一些可選參數
4. 同樣的context可以傳遞到多個goroutine中,Context在多個goroutine中是安全的
5. 在子context傳入goroutine中后,應該在子goroutine中對該子context的Done channel進行監控,一旦該channel被關閉,應立即終止對當前請求的處理,並釋放資源。