網站首頁 編程語言 正文
引言
context包比較小,是閱讀源碼比較理想的一個入手,并且里面也涵蓋了許多go設計理念可以學習。
go的Context作為go并發方式的一種,無論是在源碼net/http中,開源框架例如gin中,還是內部框架trpc-go中都是一個比較重要的存在,而整個 context 的實現也就不到600行,所以也想借著這次機會來學習學習,本文基于go 1.18.4。
話不多說,例:
為了使可能對context不太熟悉的同學有個熟悉,先來個example ,摘自源碼:
我們利用WithCancel創建一個可取消的Context,并且遍歷頻道輸出,當 n==5時,主動調用cancel來取消。
而在gen func中有個協程來監聽ctx當監聽到ctx.Done()即被取消后就退出協程。
func main(){ gen := func(ctx context.Context) <-chan int { dst := make(chan int) n := 1 go func() { for { select { case <-ctx.Done(): close(dst) return // returning not to leak the goroutine case dst <- n: n++ } } }() return dst } ctx, cancel := context.WithCancel(context.Background()) // defer cancel() // 實際使用中應該在這里調用 cancel for n := range gen(ctx) { fmt.Println(n) if n == 5 { cancel() // 這里為了使不熟悉 go 的更能明白在這里調用了 cancel() break } } // Output: // 1 // 2 // 3 // 4 // 5 }
這是最基本的使用方法。
概覽
對于context包先上一張圖,便于大家有個初步了解(內部函數并未全列舉,后續會逐一講解):
最重要的就是右邊的接口部分,可以看到有幾個比較重要的接口,下面逐一來說下:
type Context interface{ Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any }
首先就是Context接口,這是整個context包的核心接口,就包含了四個 method,分別是:
Deadline() (deadline time.Time, ok bool) // 獲取 deadline 時間,如果沒有的話 ok 會返回 false
Done() <-chan struct{} // 返回的是一個 channel ,用來應用監聽任務是否已經完成
Err() error // 返回取消原因 例如:Canceled\DeadlineExceeded Value(key any) any // 根據指定的 key 獲取是否存在其 value 有則返回
可以看到這個接口非常清晰簡單明了,并且沒有過多的Method,這也是go 設計理念,接口盡量簡單、小巧,通過組合來實現豐富的功能,后面會看到如何組合的。
再來看另一個接口canceler,這是一個取消接口,其中一個非導出 method cancel,接收一個bool和一個error,bool用來決定是否將其從父Context中移除,err用來標明被取消的原因。還有個Done()和Context接口一樣,這個接口為何這么設計,后面再揭曉。
type canceler interface{ cancel(removeFromParent bool, err error) Done() <-chan struct{} }
接下來看這兩個接口的實現者都有誰,首先Context直接實現者有 valueCtx(比較簡單放最后講)和emptyCtx
而canceler直接實現者有cancelCtx和timerCtx ,并且這兩個同時也實現了Context接口(記住我前面說得另外兩個是直接實現,這倆是嵌套接口實現松耦合,后面再說具體好處),下面逐一講解每個實現。
空的
見名知義,這是一個空實現,事實也的確如此,可以看到啥啥都沒有,就是個空實現,為何要寫呢?
type emptyCtx int func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { return} 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"}
再往下讀源碼會發現兩個有意思的變量,底層一模一樣,一個取名叫 background,一個取名叫todo,為何呢?耐心的可以看看解釋,其實是為了方便大家區分使用,背景?是在入口處來傳遞最初始的context,而todo 則是當你不知道用啥,或者你的函數雖然接收ctontext參數,但是并沒有做任何實現時,那么就使用todo即可。后續如果有具體實現再傳入具體的上下文。所以上面才定義了一個空實現,就為了給這倆使用呢,這倆也是我們最常在入口處使用的。
var ( background = new(emptyCtx) todo = new(emptyCtx) ) // 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 } // TODO returns a non-nil, empty Context. Code should use context.TODO when // it's unclear which Context to use or it is not yet available (because the // surrounding function has not yet been extended to accept a Context // parameter). func TODO() Context { return todo }
下面再看看具體的定義吧。
cancelCtx與timerCtx、valueCtx
type cancelCtx struct{ Context mu sync.Mutex // 鎖住下面字段的操作 // 存放的是 chan struct{}, 懶創建, // 只有第一次被 cancel 時才會關閉 done atomic.Value // children 存放的是子 Context canceler ,并且當第一次被 cancel 時會被 // 設為 nil children map[canceler]struct{} // 第一次被調用 cancel 時,會被設置 err error } type timerCtx struct{ cancelCtx timer *time.Timer // 定時器,用來監聽是否超時該取消 deadline time.Time // 終止時間 } type valueCtx struct { Context key, val any }
這里就看出來為何cancelCtx為非導出了,因為它通過內嵌Context接口也也是實現了Context的。并且通過這種方式實現了松耦合,可以通過 WithCancel(父Context) (ctx Context,cancel CancelFunc) 來傳遞任何自定義的Context實現。
而timerCtx是嵌套的cancelCtx,同樣他也可以同時調用Context接口所有 method與cancelCtx所有method ,并且還可以重寫部分方法。而 valueCtx和上面兩個比較獨立,所以直接嵌套的Context。
這里應該也看明白了為何canceler為何一個可導出Done一個不可導出 cancel,Done是重寫Context的method會由上層調用,所以要可導出, cancel則是由return func(){c.cancel(false,DeadlineExeceed) 類似的封裝導出,所以不應該導出。
這是go中推崇的通過組合而非繼承來編寫代碼。其中字段解釋我已在后面注明,后面也會講到。看懂了大的一個設計理念,下面我們就逐一擊破,通過上面可以看到timerCtx其實是復用了cancelCtx能力,所以cancelCtx最為重要,下面我們就先將cancelCtx實現。
取消
它非導出,是通過一個方法來直接返回Context類型的,這也是go理念之一,不暴露實現者,只暴露接口(前提是實現者中的可導出method不包含接口之外的method, 否則導出的method外面也無法調用)。
先看看外部構造函數WithCancel,
- 先判斷parent是否為nil,如果為nil就panic,這是為了避免到處判斷是否為nil。所以永遠不要使用nil來作為一個Context傳遞。
- 接著將父Context封裝到cancelCtx并返回,這沒啥說得,雖然只有一行代碼,但是多處使用,所以做了封裝,并且后續如果要更改行為調用者也無需更改。很方便。
- 調用propagateCancel,這個函數作用就是當parent是可以被取消的時候就會對子Context也進行取消的取消或者準備取消動作。
- 返回Context與CancelFunc type >CancelFunc func()就是一個 type func別名,底層封裝的是c.cancel方法,為何這么做呢?這是為了給上層應用一個統一的調用,cancelCtx與timerCtx以及其他可以實現不同的cancel但是對上層是透明并且一致的行為就可。這個func應該是協程安全并且多次調用只有第一次調用才有效果。
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) } } func newCancelCtx(parent Context) cancelCtx { return cancelCtx{Context: parent} }
接下來就來到比較重要的func? propagateCancel,我們看看它做了啥,
首先是判斷父context的Done()方法返回的channel是否為nil,如果是則直接返回啥也不做了。這是因為父Context從來不會被取消的話,那就沒必要進行下面動作。這也表名我們使用.與貓(上下文。Background()) 這個函數是不會做任何動作的。
done := parent.Done() if done == nil { return // parent is never canceled }
接下里就是一個select ,如果父Context已經被取消了的話,那就直接取消子Context就好了,這個也理所應當,父親都被取消了,兒子當然也應該取消,沒有存在必要了。
select { case <-done: // parent is already canceled child.cancel(false, parent.Err()) return default: }
如果父 Context 沒有被取消,這里就會做個判斷,
- 看看parent是否是一個*cancelCtx,如果是的話就返回其p,再次檢查 p.err是否為nil,如果不為nil就說明parent被取消,接著取消 子 Context,如果沒被取消的話,就將其加入到p.children中,看到這里的 map是個canceler,可以接收任何實現取消器 的類型。這里為何要加鎖呢?因為要對p.err以及p.children進行讀取與寫入操作,要確保協程安全所以才加的鎖。
- 如果不是*cancelCtx類型就說明parent是個被封裝的其他實現 Context 接口的類型,則會將goroutines是個int加1這是為了測試使用的,可以不管它。并且會啟動個協程,監聽父Context ,如果父Context被取消,則取消子Context,如果監聽到子Context已經結束(可能是上層主動調用CancelFunc)則就啥也不用做了。
if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() 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) go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): } }() }
接下來看看parentCancelCtx的實現:它是為了找尋parent底下的 *cancelCtx,
它首先檢查parent.Done()如果是一個closedchan這個頻道 在初始化時已經是個一個被關閉的通道或者未nil的話(emptyCtx)那就直接返回 nil,false。
func parentCancelCtx(parent Context) (*cancelCtx, bool) { done := parent.Done() if done == closedchan || done == nil { return nil, false }
var closedchan = make(chan struct{}) func init() { close(closedchan)
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) if !ok { return nil, false }
接著判斷是否parent是*cancelCtx類型,如果不是則返回nil,false,這里調用了parent.Value方法,并最終可能會落到value方法:
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) } } }
- 如果是*valueCtx,并且key==ctx.key則返回,否則會將c賦值為 ctx.Context,繼續下一個循環
- 如果是*cancelCtx并且key==&cancelCtxKey則說明找到了,直接返回,否則c= ctx.上下文繼續
- 如果是timerCtx,并且key== &cancelCtxKey則會返回內部的cancelCtx
- 如果是*emptyCtx 則直接返回nil,
- 默認即如果是用戶自定義實現則調用對應的Value找尋
可以發現如果嵌套實現過多的話這個方法其實是一個遞歸調用。
如果是則要繼續判斷p.done與parent.Done()是否相等,如果沒有則說明:*cancelCtx已經被包裝在一個自定義實現中,提供了一個不同的包裝,在這種情況下就返回nil,false:
pdone, _ := p.done.Load().(chan struct{}) if pdone != done { return nil, false } return p, true
構造算是結束了,接下來看看如何取消的:
- 檢查err是否為nil
if err == nil { panic("context: internal error: missing cancel error") }
- 由于要對err、cancelCtx.done以及children進行操作,所以要加鎖
- 如果c.err不為nil則說明已經取消過了,直接返回。否則將c.err=err賦值,這里看到只有第一次調用才會賦值,多次調用由于已經有 != nil+鎖的檢查,所以會直接返回,不會重復賦值
c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled } c.err = err
- 會嘗試從c.done獲取,如果為nil,則保存一個closedchan,否則就關閉d,這樣當你context.Done()方法返回的channel才會返回。
d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) }
- 循環遍歷c.children去關閉子Context,可以看到釋放子context時會獲取 子Context的鎖,同時也會獲取父Context的鎖。所以才是線程安全的。結束后釋放鎖
d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) }
- 如果要將其從父Context刪除為true,則將其從父上下文刪除
if removeFromParent { removeChild(c.Context, c) }
removeChild也比較簡單,當為*cancelCtx就將其從Children內刪除,為了保證線程安全也是加鎖的。
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() }
Done就是返回一個channel用于告知應用程序任務已經終止:這一步是只讀沒有加鎖,如果沒有讀取到則嘗試加鎖,再讀一次,還沒讀到則創建一個chan,可以看到這是一個懶創建的過程。所以當用戶主動調用CancelFunc時,其實根本就是將c.done內存儲的chan close掉,這其中可能牽扯到父關閉,也要循環關閉子Context過程。
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{}) }
cancelCtx主要內容就這么多,接下里就是timerCtx了
計時器
回顧下timerCtx定義,就是內嵌了一個cancelCtx另外多了兩個字段timer和deadline,這也是組合的體現。
type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time }
下面就看看兩個構造函數,WithDeadline與WithTimeout,WithTimeout就是對WithDealine的一層簡單封裝。
檢查不多說了, 第二個檢查如果父context的截止時間比傳遞進來的早的話,這個時間就無用了,那么就退化成cancelCtx了。
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) { return WithCancel(parent) }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) }
構造timerCtx并調用propagateCancel,這個已經在上面介紹過了。
c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c)
接著會看,會先利用time.直到(d.分時。Now()) 來判斷傳入的 deadlineTime與當前時間差值,如果在當前時間之前的話說明已經該取消了,所以會直接調用cancel函數進行取消,并且將其從父Context中刪除。否則就創建一個定時器,當時間到達會調用取消函數,這里是定時調用,也可能用戶主動調用。
dur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded) 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) }
下面看看cancel實現吧,相比較cancelCtx就比較簡單了,先取消 cancelCtx,也要加鎖,將c.timer停止并賦值nil,這里也是第一次調用才會賦值nil,因為外層還有個c.timer !=nil的判斷,所以多次調用只有一次賦值。
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() }
相比較于cancelCtx還覆蓋實現了一個Deadline(),就是返回當前 Context的終止時間。
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { return c.deadline, true }
下面就到了最后一個內置的valueCtx了。
值
結構器就更加加單,就多了key,val
type valueCtx struct { Context key, val any }
也就有個Value method不同,可以看到底層使用的就是我們上面介紹的value函數,重復復用
func (c *valueCtx) Value(key any) any { if c.key == key { return c.val } return value(c.Context, key) }
幾個主要的講解完了,可以看到不到600行代碼,就實現了這么多功能,其中蘊含了組合、封裝、結構體嵌套接口等許多理念,值得好好琢磨。下面我們再看看其中有些有意思的地方。我們一般打印字符串都是使用 fmt 包,那么不使用fmt包該如何打印呢?context包里就有相應實現,也很簡單,就是 switch case來判斷v類型并返回,它這么做的原因也有說:
“因為我們不希望上下文依賴于unicode表”,這句話我還沒理解,有知道的小伙伴可以在底下評論,或者等我有時間看看fmt包實現。
func stringify(v any) string { switch s := v.(type) { case stringer: return s.String() case string: return s } return "<not Stringer>" } func (c *valueCtx) String() string { return contextName(c.Context) + ".WithValue(type " + reflectlite.TypeOf(c.key).String() + ", val " + stringify(c.val) + ")" }
使用Context的幾個原則
直接在函數參數傳遞,不要在struct傳遞,要明確傳遞,并且作為第一個參數,因為這樣可以由調用方來傳遞不同的上下文在不同的方法上,如果你在 struct內使用context則一個實例是公用一個context也就導致了協程不安全,這也是為何net包Request要拷貝一個新的Request WithRequest(context go 1.7 才被引入),net包牽扯過多,要做到兼容才嵌入到 struct內。
不要使用nil而當你不知道使用什么時則使用TODO,如果你用了nil則會 panic。避免到處判斷是否為nil。
WithValue不應該傳遞業務信息,只應該傳遞類似request-id之類的請求信息。
無論用哪個類型的Context,在構建后,一定要加上:defer cancel(),因為這個函數是可以多次調用的,但是如果沒有調用則可能導致Context沒有被取消繼而其關聯的上下文資源也得不到釋放。
在使用WithValue時,包應該將鍵定義為未導出的類型以避免發生碰撞,這里貼個官網的例子:
// package user 這里為了演示直接在 main 包定義 // User 是存儲在 Context 值 type User struct { Name string Age int } // key 是非導出的,可以防止碰撞 type key int // userKey 是存儲 User 類型的鍵值,也是非導出的。 var userKey key // NewContext 創建一個新的 Context,攜帶 *User func NewContext(ctx context.Context, u *User) context.Context { return context.WithValue(ctx, userKey, u) } // FromContext 返回存儲在 ctx 中的 *User func FromContext(ctx context.Context) (*User, bool) { u, ok := ctx.Value(userKey).(*User) return u, ok }
那怎么能夠防止碰撞呢?可以做個示例:看最后輸出,我們在第一行就用 userKey的值0,存儲了一個值“a”。
然后再利用NewContext存儲了&User,底層實際用的是 context.WithValue(ctx,userKey,u)
讀取時用的是FromContext,兩次存儲即使底層的key值都為0, 但是互不影響,這是為什么呢?
還記得WithValue怎么實現的么?你每調用一次都會包一層,并且一層一層解析,而且它會比較c.key==key,這里記住go的==比較是比較值和類型的,二者都相等才為true,而我們使用type key int所以userKey與0底層值雖然一樣,但是類型已經不一樣了(這里就是main.userKey與0),所以外部無論定義何種類型都無法影響包內的類型。這也是容易令人迷惑的地方
package main import ( "context" "fmt" ) func main() { ctx := context.WithValue(context.Background(), , "a") ctx = NewContext(ctx, &User{}) v, _ := FromContext(ctx) fmt.Println(ctx.Value(0), v) // Output: a, &{ 0} }
原文鏈接:https://juejin.cn/post/7147618996525727774
相關推薦
- 2022-08-14 C++學習之算術運算符使用詳解_C 語言
- 2022-10-23 C#中const,readonly和static關鍵字的用法介紹_C#教程
- 2022-09-27 linux?shell中Grep命令查找多個字符串(grep同時匹配多個關鍵字或任意關鍵字)_lin
- 2022-11-06 Golang常用包使用介紹_Golang
- 2023-09-18 Mybatis新增數據,存在就更新,不存在就添加
- 2022-09-12 Python提取Word中圖片的實現步驟_python
- 2022-06-21 Oracle新增和刪除用戶_oracle
- 2023-05-23 pytorch中backward()方法如何自動求梯度_python
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支