Golang Package Context
Request-scoped context
在 Go 服務中, 每個傳入的請求都在其自己的goroutine 中處理. 請求處理程序通常啟動額外的 goroutine 來訪問其他後端, 如數據庫和 RPC服務. 處理請求的 goroutine 通常需要訪問特定於請求(request-specific context)的值, 例如最終用戶的身份、授權令牌和請求的截止日期(deadline). 當一個請求被取消或超時時, 處理該請求的所有 goroutine 都應該快速退出(fail fast), 這樣系統就可以回收它們正在使用的任何資源.
Go 1.7 引入一個 context 包, 它使得跨 API 邊界的請求範圍元數據、取消信號和截止日期很容易傳遞給處理請求所涉及的所有 goroutine(顯示傳遞).
其他語言: Thread Local Storage(TLS), XXXContext
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.
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.
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.
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
// }
Value(key any) any
}
如何將 context 集成到 API 中?
在將 context 集成到 API 中時, 要記住的最重要的一點是, 它的作用域是請求級別的。
例如, 沿單個數據庫查詢存在是有意義的, 但沿數據庫對象存在則沒有意義。
目前有兩種方法可以將 context 對象集成到 API 中:

- The first parameter of a function call 首參數傳遞 context 對象, 比如, 參考 net 包 Dialer.DialContext. 此函數執行正常的 Dial 操作, 但可以通過 context 對象取消函數調用.
func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error) {...}
- Optional config on a request structure
在第一個 request 對像中攜帶一個可選的 context 對象.
例如 net/http 庫的 Request.WithContext, 通過攜帶給定的 context 對象,
返回一個新的 Request 對象.
func (r *Request) WithContext(ctx context.Context) *Request {...}
Do not store Contexts inside a struct type
Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx:
func DoSomething(ctx context.Context, arg Arg) error {
// ... user ctx ...
}
Incoming requests to a server should create a Context.
使用 context 的一個很好的心智模型是它應該在程序中流動, 應該貫穿你的代碼。 這通常意味著您不希望將其存儲在結構體之中。 它從一個函數傳遞到另一個函數, 並根據需要進行擴展。 理想情況下, 每個請求都會創建一個 context 對象, 並在請求結束時過期。
不存儲上下文的一個例外是, 當您需要將它放入一個結構中時, 該結構純粹用作通過通道(channel)傳遞的消息。如下例所示。
// A message processes parameter and returns the result on responseChan,
// ctx is places in a struct, but this is ok to do.
type Message struct {
responseChan chan<- int
parameter string
ctx context.Context
}
context.WithValue
context.WithValue 內部基於 valueCtx 實現:
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
Context
key, val any
}
為了實現不斷的 WithValue, 構建新的 context,
內部在查找 key 時候, 使用遞歸方式不斷從當前, 從父節點尋找匹配的 key, 直到 root context(Backgrond 和 TODO Value 函數會返回 nil)。

func WithValue(parent Context, key, val any) 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}
}
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
case *emptyCtx:
return nil
default:
return c.Value(key)
}
}
}
Debugging or tracing data is safe to pass in a Context
context.WithValue 方法允許上下文攜帶請求範圍的數據。
這些數據必須是安全的, 以便多個 goroutine 同時使用。
這裡的數據, 更多是面向請求的元數據, 不應該作為函數的可選參數來使用(比如 context 裡面掛了一個sql.Tx 對象,
傳遞到 DAO(decentralized autonomous organization) 層使用),
因為元數據相對函數參數更加是隱含的, 面向請求的。而參數是更加顯示的。
同一個 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
Use context values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions. 比如 染色, API 重要性, Trace https://github.com/go-kratos/kratos/blob/master/pkg/net/metadata/key.go
Immutable(不變性) Go objects:
- interfaces
- booleans, numeric values (including values of type int)
- strings
- pointers
- function pointers, and closures which can be reduced to function pointers
- structs having a single field
- 具有併發安全
Mutable(變性) Go objects:
- (可以被make 都是)
- arrays and slices
- maps
- channels
- closures which are capturing at least 1 variable from the outer scope
- 不具有併發安全
比如我們新建了一個基於 context.Background() 的 ctx1, 攜帶了一個 map 的數據, map 中包含了 "k1": "v1" 的一個鍵值對,
ctx1 被兩個 goroutine 同時使用作為函數簽名傳入,
如果我們修改了這個map, 會導致另外進行讀 context.Value 的 goroutine 和修改 map 的 goroutine, 在 map 對像上產生 data race。
因此我們要使用 copy-on-write 的思路, 解決跨多個 goroutine 使用數據、修改數據的場景。
Replace a Context using WithCancel, WithDeadline, WithTimeout, or WithValue.

COW: 從 ctx1 中獲取 map1(可以理解為 v1 版本的 map 數據)。 構建一個新的 map 對象 map2, 複製所有 map1 數據, 同時追加新的數據 "k2": "v2" 鍵值對, 使用 context.WithValue 創建新的 ctx2, ctx2 會傳遞到其他的 goroutine 中。這樣各自讀取的副本都是自己的數據, 寫行為追加的數據, 在 ctx2 中也能完整讀取到, 同時也不會污染 ctx1 中的數據。
The chain of function calls between them must propagate the Context.

When a Context is canceled, all Contexts derived from it are also canceled
當一個 context 被取消時,從它派生的所有 context 也將被取消。
WithCancel(ctx) 參數 ctx 認為是 parent ctx,在內部會進行一個傳播關係鏈的關聯。
Done() 返回 一個 chan,當我們取消某個parent context,
實際上上會遞歸層層 cancel 掉自己的 child context 的 done chan 從而讓整個調用鏈中所有監聽 cancel 的 goroutine退出

example
package main
import (
"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
}
}
}
/*
1
2
3
4
5
*/
All blocking/long operations should be cancelable
如果要實現一個超時控制,通過上面的context 的parent/child 機制,
其實我們只需要啟動一個定時器,然後在超時的時候,直接將當前的 context 給 cancel 掉,
就可以實現監聽在當前和下層的額context.Done() 的 goroutine 的退出。

package main
import (
"context"
"fmt"
"time"
)
const shortDuration = 1 * time.Millisecond
func main() {
d := time.Now().Add(shortDuration)
ctx, cancel := context.WithDeadline(context.Background(), d)
// Even though ctx will be expired, it is good practice to call its
// cancellation 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())
}
}
/*
context deadline exceeded
*/
context.go
// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
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) }
}
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
Final Notes
- Incoming requests to a server should create a Context.
- Outgoing calls to servers should accept a Context.
- Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it.
- The chain of function calls between them must propagate the Context.
- Replace a Context using WithCancel, WithDeadline, WithTimeout, or WithValue.
- When a Context is canceled, all Contexts derived from it are also canceled.
- The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.
- Do not pass a nil Context, even if a function permits it. Pass a TODO context if you are unsure about which Context to use.
- Use context values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
- All blocking/long operations should be cancelable.
- Context.Value obscures your program’s flow.
- Context.Value should inform, not control.
- Try not to use context.Value.