1. 簡(jiǎn)介
go 1.7 開始引入context(上下文),準(zhǔn)確地說是goroutine 的上下文。主要在goroutine 間傳遞上下文消息,包括了取消信號(hào), 超時(shí)時(shí)間,截止時(shí)間和k-v 等。
在web程序中,每個(gè)Request都需要開啟一個(gè)goroutine做一些事情,這些goroutine又可能會(huì)開啟其他的 goroutine去訪問后端資源,比如數(shù)據(jù)庫、RPC服務(wù)等,它們需要訪問一些共享的資源,比如用戶身份信息、認(rèn)證token、請(qǐng)求截止時(shí)間等 這時(shí)候可以通過Context,來跟蹤這些goroutine,并且通過Context來控制它們, 這就是Go語言為我們提供的Context,中文可以稱之為“上下文”。
2. 接口定義
主要包括了如下兩個(gè)接口定義
- Context
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }-
Deadline方法是獲取設(shè)置的截止時(shí)間的意思,第一個(gè)返回值是截止時(shí)間,到了這個(gè)時(shí)間點(diǎn),Context會(huì)自動(dòng)發(fā)起取消請(qǐng)求; 第二個(gè)返回值ok==false時(shí)表示沒有設(shè)置截止時(shí)間,如果需要取消的話,需要調(diào)用取消函數(shù)進(jìn)行取消。 -
Done方法返回一個(gè)只讀的chan,類型為struct{},在goroutine中,如果該方法返回的chan可以讀取,則意味著parent context已經(jīng)發(fā)起了取消請(qǐng)求, 我們通過Done方法收到這個(gè)信號(hào)后,就應(yīng)該做清理操作,然后退出goroutine,釋放資源。之后,Err 方法會(huì)返回一個(gè)錯(cuò)誤,告知為什么 Context 被取消 -
Err方法返回取消的錯(cuò)誤原因,因?yàn)槭裁碈ontext被取消,例如 被取消或者超時(shí) -
Value方法獲取該Context上綁定的值,是一個(gè)鍵值對(duì),通過一個(gè)Key才可以獲取對(duì)應(yīng)的值
-
-
canceler
type canceler interface { cancel(removeFromParent bool, err error) Done() <-chan struct{} }實(shí)現(xiàn)了canceler 接口的context說明是可取消的,在context 包中有兩個(gè)類型實(shí)現(xiàn)了該接口,分別是:
*cancelCtx和*timerCtx
3. 接口實(shí)現(xiàn)類型
官方提供了如下四種的 context 類型,分別是 emptyCtx,cancelCtx,timerCtx,valueCtx。
3.1 emptyCtx
實(shí)現(xiàn)了Context接口,但都是直接 return 默認(rèn)值,沒有具體功能代碼, 底層類型是int, 同時(shí)實(shí)現(xiàn)了Stringer接口(String方法)
type emptyCtx int
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key any) any {
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 使用, 官方定義了兩個(gè)可導(dǎo)出的empptCty,通過函數(shù)Background()和TODO()返回
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
3.2 cancelCtx
實(shí)現(xiàn)了Context和canceler 接口, 結(jié)構(gòu)體定義如下
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of 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
}
-
接口實(shí)現(xiàn)
cancelCtx組合了匿名Context 字段,所以也是一個(gè)Context, 同時(shí)它又實(shí)現(xiàn)了
canceler接口,
其中對(duì)于Context, 重寫了Done(),Value()和Err()方法,其他的繼承父類的
具體代碼如下:// `Done()` 方法 采用`懶漢式`方式, 當(dāng)調(diào)用該方法時(shí),`c.done`才會(huì)存儲(chǔ)一個(gè)新創(chuàng)建的一個(gè)只讀 `channel d`,并返回 func (c *cancelCtx) Done() <-chan struct{} { d := c.done.Load() if d != nil { return d.(chan struct{}) } c.mu.Lock() defer c.mu.Unlock() d = c.done.Load() if d == nil { d = make(chan struct{}) c.done.Store(d) } return d.(chan struct{}) } func (c *cancelCtx) Value(key any) any { if key == &cancelCtxKey { return c } 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) } } } func (c *cancelCtx) Err() error { c.mu.Lock() err := c.err c.mu.Unlock() return err }重點(diǎn)講一下重寫的
func (c *cancelCtx) Value(key any) any方法, 如果傳入的key 是cancelCtxKey就會(huì)直接返回當(dāng)前的cancelCtx, 反之就會(huì)通過包內(nèi)置的value()函數(shù),逐層向上父節(jié)點(diǎn)傳遞,知道找到對(duì)應(yīng)的key 或者找不到對(duì)于canceler具體實(shí)現(xiàn)的
cancel()方法如下,Done()同上:func (c *cancelCtx) cancel(removeFromParent bool, err error) { // 必須傳入err,即導(dǎo)致ctx cancel 的error if err == nil { panic("context: internal error: missing cancel error") } c.mu.Lock() // 如果ctx 中已經(jīng)有err了,說明該ctx 已經(jīng)被 cancel了 if c.err != nil { c.mu.Unlock() return // already canceled } // 關(guān)閉done 中的channel d c.err = err d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) } // 遞歸遍歷子節(jié)點(diǎn) 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() // 是否從父節(jié)點(diǎn)中刪除 if removeFromParent { removeChild(c.Context, c) } }簡(jiǎn)單的來說cancel 方法就是關(guān)閉c.done 中的channel,遞歸取消子節(jié)點(diǎn)。
其中removeChild函數(shù)就是將子節(jié)點(diǎn)從父節(jié)點(diǎn)中的childrenmap 中刪除, 代碼如下: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() } // 向上遍歷找到可取消的父節(jié)點(diǎn),因?yàn)楫?dāng)前的父節(jié)點(diǎn)可能不是可取消的類型, 即不是*cancelCtx 和 *timerCtx 類型 func parentCancelCtx(parent Context) (*cancelCtx, bool) { done := parent.Done() // 父節(jié)點(diǎn)已經(jīng)取消或者不存在 if done == closedchan || done == nil { return nil, false } // 向上找到可取消的父節(jié)點(diǎn)并返回,*cancelCtx 或者*timerCtx 中的*cancelCtx p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) if !ok { return nil, false } pdone, _ := p.done.Load().(chan struct{}) if pdone != done { return nil, false } return p, true } -
創(chuàng)建cancelCtc函數(shù):
通過如下
WithCancel函數(shù)創(chuàng)建, 傳入一個(gè)父節(jié)點(diǎn),返回子節(jié)點(diǎn)和對(duì)應(yīng)的取消函數(shù)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) } }其中
propagateCancel函數(shù)主要是向上遍歷可cancel的父節(jié)點(diǎn), 并掛靠在該父節(jié)點(diǎn)上func propagateCancel(parent Context, child canceler) { // 父節(jié)點(diǎn)是不可取消的類型即 emptyCtx 或者是valueCtx done := parent.Done() if done == nil { return // parent is never canceled } // 監(jiān)聽下父節(jié)點(diǎn)的Done() channel 判斷是否已經(jīng)取消,是的會(huì)此時(shí)直接取消當(dāng)前子節(jié)點(diǎn) select { case <-done: // parent is already canceled child.cancel(false, parent.Err()) return default: } // 向上找到可取消的父節(jié)點(diǎn) if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() // 父節(jié)點(diǎn)已經(jīng)取消 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) // parentCancelCtx 函數(shù)只能找到cancelCtx 或者timerCtx 類型的父類,下面是監(jiān)聽其他類型的可取消的父類,當(dāng)父類取消,則關(guān)聯(lián)取消子類 go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) // 該case 防止父節(jié)點(diǎn)未取消,而當(dāng)前節(jié)點(diǎn)已經(jīng)取消導(dǎo)致的goroutine 泄漏 case <-child.Done(): } }() } }
3.3 timerCtx
timerCtx 其實(shí)繼承類cancelCtx 并擴(kuò)展了功能, 結(jié)構(gòu)體定義如下:
type timerCtx struct {
cancelCtx
timer *time.Timer
deadline time.Time
}
-
接口實(shí)現(xiàn)
timerCtx 重寫了DeadLine和cancel方法: 具體如下
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
-
創(chuàng)建timerCtc函數(shù):
// 創(chuàng)建超時(shí)時(shí)間段的timerCtx func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) } // 創(chuàng)建截止時(shí)間的timerCtx func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if parent == nil { panic("cannot create context from nil parent") } // 如果當(dāng)前的截止時(shí)間早于父節(jié)點(diǎn)的截止時(shí)間, 則直接返貨cancelCtx 類型的context, 因?yàn)橐欢ㄊ歉腹?jié)點(diǎn)早于該節(jié)點(diǎn)取消,或者是該節(jié)點(diǎn)主動(dòng)調(diào)用子節(jié)點(diǎn) 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() // 開起計(jì)時(shí)器,到時(shí)間自動(dòng)執(zhí)行取消 if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) } }
3.4 valueCtx
用來傳遞K-V鍵值對(duì)的context, 底層結(jié)構(gòu)體定義如下:
type valueCtx struct {
// 通過組合匿名字段,繼承了Context, 在這邊可以理解為上層父節(jié)點(diǎn)
Context
// 鍵值對(duì)數(shù)據(jù)
key, val any
}
valueCtx 重寫了Value方法, 即向上遍歷父節(jié)點(diǎn)的key ,直到找到對(duì)應(yīng)的key 或者找不到
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
valueCtx的通過如下WithValue創(chuàng)建:
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}
}
注意:
key 不能為nil, 同時(shí)key的類型必須是可比較類型, 函數(shù)返回的是*valueCtx類型
4. 注意事項(xiàng)
官方提出了如下的幾個(gè)注意事項(xiàng)
- 不要將context 放在結(jié)構(gòu)體里,直接將context 作為函數(shù)的第一個(gè)參數(shù)傳入,而且一般命名為ctx
- 不要向函數(shù)傳入nil 屬性的context, 如果不知道傳什么就傳
context.TODO()- 不要將應(yīng)該作為函數(shù)參數(shù)的類型塞到ctx中,ctx 應(yīng)該存儲(chǔ)的是一些共有的數(shù)據(jù),例如登錄的token 或者cookie等
- 同一個(gè)context 是并發(fā)安全的,可以傳遞到多個(gè)goroutine 中
5. 使用實(shí)例代碼
// cancelCtx
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
}
}
// timerCtx
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
// Even though ctx will be expired, it is good practice to call its
// cancelation 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())
}
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
----------------------------------
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}
// valueCtx
type favContextKey string
f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))