Why
很多框架中接口函數第一個參數統一是ctx context.Context接口,如net/http中conn.serve(ctx context.Context)方法,爲什麼要這麼設計呢?
因爲一般一個網絡請求Request,會在多個Goroutine中處理,而這些Goroutine可能需要共享Request的一些信息;同時當Request被取消或者超時的時候,所有從這個Request創建的所有Goroutine也應該被結束。上下文則幾乎已經成爲傳遞與請求同生存週期變量的標準方法。
What
context用於Goroutine之間共享狀態變量,另一個gorutine通過設置ctx變量值,傳遞過期或撤銷信號給被調用的程序單元。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
- Deadline會返回一個超時時間,Goroutine獲得了超時時間後,例如可以對某些io操作設定超時時間。
- Done方法返回一個信道(channel),傳遞是否已關閉的信號。
- Err方法表明Context被撤的原因。
- Value可以讓Goroutine共享一些數據,當然獲得數據是協程安全的。
How
Goroutine的創建和調用關係總是像層層調用的,而更靠頂部的Goroutine應有辦法主動關閉其下屬的Goroutine。
Context結構也應該像一棵樹,要創建Context樹,第一步就是要得到根節點,context.Background函數的返回值就是根節點。
創建子context 函數 |
解釋 |
---|---|
|
該Context一般由接收請求的第一個Goroutine創建,即根節點,一般返回emptyCtx |
|
創建子context,返回取消方法,父Goroutine可以調用取消子goroutine |
|
創建子context,到了dealine或者被父gorutine調用返回的取消方法,終止 |
|
創建子context,調用時間過了timeout或者被父gorutine調用返回的取消方法,終止 WithDeadline(parent, time.Now().Add(timeout)) |
|
返回valueCtx,傳遞了kv對到子context |
子節點需要類似如下代碼來接收是否已結束,並退出該Goroutine:
select {
case <-cxt.Done():
// do some clean...
}
Example
ctx := context.WithValue(baseCtx, ServerContextKey, srv) // 服務創建子context
for {
rw, err := l.Accept() // 接收請求
connCtx := ctx
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(connCtx) // 子routine傳入子ctx
}
Summary
context通過構建樹型關係的Context,達到上一層Goroutine給下層Goroutine
父子傳遞控制信號&共享變量
- Context對象生存週期一般僅爲一個請求的處理週期。即對一個請求創建一個Context變量(它爲Context樹結構的根);在請求處理結束後,撤銷此ctx變量,釋放資源。
- 每次創建Goroutine,可將原Context傳遞給Goroutine,也可創建一個子Context
- Context能存儲不同類型、不同數目的值KV,Goroutines安全讀寫。
- 當通過父Context對象創建子Context對象時,可同時獲得子Context的一個撤銷函數,這樣父Context對象的創建環境就獲得了對子Context將要被傳遞到的Goroutine的撤銷權。
使用原則
- 不作爲結構體字段,按需顯式地函數間傳參,作爲第一個參數使用,一般命名爲ctx;
- 不要傳入一個nil的Context,如果你不確定你要用什麼Context的時候傳一個context.TODO;
- 共享變量Values只用於請求scope的數據,不傳可選的參數;
- 同個Context可傳到不同的goroutine中,在多個goroutine中是線程安全的
- 在子Context被傳遞到的goroutine中,應該對該子Context的Done信道(channel)進行監控,一旦該信道被關閉,應主動終止對當前請求信息的處理,釋放資源並返回。