context
在 Go 服務中,每個傳入的請求都在其自己的goroutine 中處理。請求處理程式通常啟動額外的 goroutine 來通路其他後端,如資料庫和 RPC 服務。處理請求的 goroutine 通常需要通路特定于請求(request-specific context)的值,例如最終使用者的身份、授權令牌和請求的截止日期(deadline)。
當一個請求被取消或逾時時,處理該請求的所有 goroutine 都應該快速退出(fail fast),這樣系統就可以回收它們正在使用的任何資源。
Go 1.7 引入一個 context 包,它使得跨 API 邊界的請求範圍中繼資料、取消信号和截止日期很容易傳遞給處理請求所涉及的所有 goroutine(顯示傳遞)。
其他語言: Thread Local Storage(TLS),XXXContext

如何将 context 內建到 API 中? 在将 context 內建到 API 中時,要記住的最重要的一點是,它的作用域是請求級别的。
例如,沿單個資料庫查詢存在是有意義的,但沿資料庫對象存在則沒有意義。 目前有兩種方法可以将 context 對象內建到 API 中:
- 首參數傳遞 context 對象,比如,參考 net 包 Dialer.DialContext。此函數執行正常的 Dial 操作,但可以通過 context 對象取消函數調用。
go并行程式設計4-context - 在第一個 request 對象中攜帶一個可選的 context 對象。例如 net/http 庫的 Request.WithContext,通過攜帶給定的 context 對象,傳回一個新的 Request 對象。
go并行程式設計4-context
不要在結構類型中存儲context;相反,顯式地将Context傳遞給每個需要它的函數。上下文應該是第一個參數,通常命名為ctx:
對伺服器的傳入請求應該建立一個Context。
使用 context 的一個很好的心智模型是它應該在程式中流動,應該貫穿你的代碼。這通常意味着您不希望将其存儲在結構體之中。它從一個函數傳遞到另一個函數,并根據需要進行擴充。理想情況下,每個請求都會建立一個 context 對象,并在請求結束時過期。
不存儲上下文的一個例外是,當您需要将它放入一個結構中時,該結構純粹用作通過通道傳遞的消息。如下例所示。
context.WithValue
context.WithValue 内部基于 valueCtx 實作:
為了實作不斷的 WithValue,建構新的 context,内部在查找 key 時候,使用遞歸方式不斷從目前,從父節點尋找比對的 key,直到 root context(Backgrond 和 TODO Value 函數會傳回 nil)。
在上下文中傳遞調試或跟蹤資料是安全的
context.WithValue 方法允許上下文攜帶請求範圍的資料。這些資料必須是安全的,以便多個 goroutine 同時使用。這裡的資料,更多是面向請求的中繼資料,不應該作為函數的可選參數來使用(比如 context 裡面挂了一個sql.Tx 對象,傳遞到 data 層使用),因為中繼資料相對函數參數更加是隐含的,面向請求的。而參數是更加顯示的。
同一個 context 對象可以傳遞給在不同 goroutine 中運作的函數;上下文對于多個 goroutine 同時使用是安全的。對于值類型最容易犯錯的地方,在于 context value 應該是 immutable 的,每次重新指派應該是新的 context,即: context.WithValue(ctx, oldvalue) https://pkg.go.dev/google.golang.org/grpc/metadata Context.Value should inform, not control
比如我們建立了一個基于 context.Background() 的 ctx1,攜帶了一個 map 的資料,map 中包含了 “k1”: “v1” 的一個鍵值對,ctx1 被兩個 goroutine 同時使用作為函數簽名傳入,如果我們修改了 這個map,會導緻另外進行讀 context.Value 的 goroutine 和修改 map 的 goroutine,在 map 對象上産生 data race。是以我們要使用 copy-on-write 的思路,解決跨多個 goroutine 使用資料、修改資料的場景。
從 ctx1 中擷取 map1(可以了解為 v1 版本的 map 資料)。建構一個新的 map 對象 map2,複制所有 map1 資料,同時追加新的資料 “k2”: “v2” 鍵值對,使用 context.WithValue 建立新的 ctx2,ctx2 會傳遞到其他的 goroutine 中。這樣各自讀取的副本都是自己的資料,寫行為追加的資料,在 ctx2 中也能完整讀取到,同時也不會污染 ctx1 中的資料。
當一個上下文被取消時,從它派生的所有上下文也被取消。
當一個 context 被取消時,從它派生的所有 context 也将被取消。WithCancel(ctx) 參數 ctx 認為是 parent ctx,在内部會進行一個傳播關系鍊的關聯。Done() 傳回 一個 chan,當我們取消某個parent context, 實際上上會遞歸層層 cancel 掉自己的 child context 的 done chan 進而讓整個調用鍊中所有監聽 cancel 的 goroutine 退出。
如果要實作一個逾時控制,通過上面的 context 的 parent/child 機制,其實我們隻需要啟動一個定時器,然後在逾時的時候,直接将目前的 context 給 cancel 掉,就可以實作監聽在目前和下層的額 context.Done() 的 goroutine 的退出。
- 對伺服器的傳入請求應該建立一個Context。
- 向伺服器發出的調用應該接受一個Context。
- 不要在結構類型中存儲context;相反,顯式地将Context傳遞給每個需要它的函數。
- 它們之間的函數調用鍊必須傳播上下文。
- 使用WithCancel、WithDeadline、WithTimeout或WithValue替換上下文。
- 當一個上下文被取消時,從它派生的所有上下文也被取消。
- 相同的上下文可以傳遞給運作在不同goroutine中的函數;上下文對于多個goroutine同時使用是安全的。
- 不要傳遞空上下文,即使函數允許這樣做。如果您不确定要使用哪個上下文,則傳遞一個TODO上下文。
- 上下文值隻用于傳遞程序和api的請求範圍内的資料,而不是傳遞可選參數給函數。
- 所有阻塞/長時間操作都應該是可取消的。
- 上下文。值模糊了程式的流。
- 上下文。價值應該告知,而不是控制。
- 盡量不要使用context.Value。