從Context到go設(shè)計理念輕松上手教程
引言
context包比較小,是閱讀源碼比較理想的一個入手,并且里面也涵蓋了許多go設(shè)計理念可以學(xué)習(xí)。
go的Context作為go并發(fā)方式的一種,無論是在源碼net/http中,開源框架例如gin中,還是內(nèi)部框架trpc-go中都是一個比較重要的存在,而整個 context 的實現(xiàn)也就不到600行,所以也想借著這次機(jī)會來學(xué)習(xí)學(xué)習(xí),本文基于go 1.18.4。
話不多說,例:
為了使可能對context不太熟悉的同學(xué)有個熟悉,先來個example ,摘自源碼:
我們利用WithCancel創(chuàng)建一個可取消的Context,并且遍歷頻道輸出,當(dāng) n==5時,主動調(diào)用cancel來取消。
而在gen func中有個協(xié)程來監(jiān)聽ctx當(dāng)監(jiān)聽到ctx.Done()即被取消后就退出協(xié)程。
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() // 實際使用中應(yīng)該在這里調(diào)用 cancel for n := range gen(ctx) { fmt.Println(n) if n == 5 { cancel() // 這里為了使不熟悉 go 的更能明白在這里調(diào)用了 cancel() break } } // Output: // 1 // 2 // 3 // 4 // 5 }
這是最基本的使用方法。
概覽
對于context包先上一張圖,便于大家有個初步了解(內(nèi)部函數(shù)并未全列舉,后續(xù)會逐一講解):
最重要的就是右邊的接口部分,可以看到有幾個比較重要的接口,下面逐一來說下:
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 ,用來應(yīng)用監(jiān)聽任務(wù)是否已經(jīng)完成
Err() error // 返回取消原因 例如:Canceled\DeadlineExceeded Value(key any) any // 根據(jù)指定的 key 獲取是否存在其 value 有則返回
可以看到這個接口非常清晰簡單明了,并且沒有過多的Method,這也是go 設(shè)計理念,接口盡量簡單、小巧,通過組合來實現(xiàn)豐富的功能,后面會看到如何組合的。
再來看另一個接口canceler,這是一個取消接口,其中一個非導(dǎo)出 method cancel,接收一個bool和一個error,bool用來決定是否將其從父Context中移除,err用來標(biāo)明被取消的原因。還有個Done()和Context接口一樣,這個接口為何這么設(shè)計,后面再揭曉。
type canceler interface{ cancel(removeFromParent bool, err error) Done() <-chan struct{} }
接下來看這兩個接口的實現(xiàn)者都有誰,首先Context直接實現(xiàn)者有 valueCtx(比較簡單放最后講)和emptyCtx
而canceler直接實現(xiàn)者有cancelCtx和timerCtx ,并且這兩個同時也實現(xiàn)了Context接口(記住我前面說得另外兩個是直接實現(xiàn),這倆是嵌套接口實現(xiàn)松耦合,后面再說具體好處),下面逐一講解每個實現(xiàn)。
空的
見名知義,這是一個空實現(xiàn),事實也的確如此,可以看到啥啥都沒有,就是個空實現(xiàn),為何要寫呢?
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"}
再往下讀源碼會發(fā)現(xiàn)兩個有意思的變量,底層一模一樣,一個取名叫 background,一個取名叫todo,為何呢?耐心的可以看看解釋,其實是為了方便大家區(qū)分使用,背景 是在入口處來傳遞最初始的context,而todo 則是當(dāng)你不知道用啥,或者你的函數(shù)雖然接收ctontext參數(shù),但是并沒有做任何實現(xiàn)時,那么就使用todo即可。后續(xù)如果有具體實現(xiàn)再傳入具體的上下文。所以上面才定義了一個空實現(xiàn),就為了給這倆使用呢,這倆也是我們最常在入口處使用的。
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{}, 懶創(chuàng)建, // 只有第一次被 cancel 時才會關(guān)閉 done atomic.Value // children 存放的是子 Context canceler ,并且當(dāng)?shù)谝淮伪?cancel 時會被 // 設(shè)為 nil children map[canceler]struct{} // 第一次被調(diào)用 cancel 時,會被設(shè)置 err error } type timerCtx struct{ cancelCtx timer *time.Timer // 定時器,用來監(jiān)聽是否超時該取消 deadline time.Time // 終止時間 } type valueCtx struct { Context key, val any }
這里就看出來為何cancelCtx為非導(dǎo)出了,因為它通過內(nèi)嵌Context接口也也是實現(xiàn)了Context的。并且通過這種方式實現(xiàn)了松耦合,可以通過 WithCancel(父Context) (ctx Context,cancel CancelFunc) 來傳遞任何自定義的Context實現(xiàn)。
而timerCtx是嵌套的cancelCtx,同樣他也可以同時調(diào)用Context接口所有 method與cancelCtx所有method ,并且還可以重寫部分方法。而 valueCtx和上面兩個比較獨立,所以直接嵌套的Context。
這里應(yīng)該也看明白了為何canceler為何一個可導(dǎo)出Done一個不可導(dǎo)出 cancel,Done是重寫Context的method會由上層調(diào)用,所以要可導(dǎo)出, cancel則是由return func(){c.cancel(false,DeadlineExeceed) 類似的封裝導(dǎo)出,所以不應(yīng)該導(dǎo)出。
這是go中推崇的通過組合而非繼承來編寫代碼。其中字段解釋我已在后面注明,后面也會講到??炊舜蟮囊粋€設(shè)計理念,下面我們就逐一擊破,通過上面可以看到timerCtx其實是復(fù)用了cancelCtx能力,所以cancelCtx最為重要,下面我們就先將cancelCtx實現(xiàn)。
取消
它非導(dǎo)出,是通過一個方法來直接返回Context類型的,這也是go理念之一,不暴露實現(xiàn)者,只暴露接口(前提是實現(xiàn)者中的可導(dǎo)出method不包含接口之外的method, 否則導(dǎo)出的method外面也無法調(diào)用)。
先看看外部構(gòu)造函數(shù)WithCancel,
- 先判斷parent是否為nil,如果為nil就panic,這是為了避免到處判斷是否為nil。所以永遠(yuǎn)不要使用nil來作為一個Context傳遞。
- 接著將父Context封裝到cancelCtx并返回,這沒啥說得,雖然只有一行代碼,但是多處使用,所以做了封裝,并且后續(xù)如果要更改行為調(diào)用者也無需更改。很方便。
- 調(diào)用propagateCancel,這個函數(shù)作用就是當(dāng)parent是可以被取消的時候就會對子Context也進(jìn)行取消的取消或者準(zhǔn)備取消動作。
- 返回Context與CancelFunc type >CancelFunc func()就是一個 type func別名,底層封裝的是c.cancel方法,為何這么做呢?這是為了給上層應(yīng)用一個統(tǒng)一的調(diào)用,cancelCtx與timerCtx以及其他可以實現(xiàn)不同的cancel但是對上層是透明并且一致的行為就可。這個func應(yīng)該是協(xié)程安全并且多次調(diào)用只有第一次調(diào)用才有效果。
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從來不會被取消的話,那就沒必要進(jìn)行下面動作。這也表名我們使用.與貓(上下文。Background()) 這個函數(shù)是不會做任何動作的。
done := parent.Done() if done == nil { return // parent is never canceled }
接下里就是一個select ,如果父Context已經(jīng)被取消了的話,那就直接取消子Context就好了,這個也理所應(yīng)當(dāng),父親都被取消了,兒子當(dāng)然也應(yīng)該取消,沒有存在必要了。
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,可以接收任何實現(xiàn)取消器 的類型。這里為何要加鎖呢?因為要對p.err以及p.children進(jìn)行讀取與寫入操作,要確保協(xié)程安全所以才加的鎖。
- 如果不是*cancelCtx類型就說明parent是個被封裝的其他實現(xiàn) Context 接口的類型,則會將goroutines是個int加1這是為了測試使用的,可以不管它。并且會啟動個協(xié)程,監(jiān)聽父Context ,如果父Context被取消,則取消子Context,如果監(jiān)聽到子Context已經(jīng)結(jié)束(可能是上層主動調(diào)用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的實現(xiàn):它是為了找尋parent底下的 *cancelCtx,
它首先檢查parent.Done()如果是一個closedchan這個頻道 在初始化時已經(jīng)是個一個被關(guān)閉的通道或者未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,這里調(diào)用了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,繼續(xù)下一個循環(huán)
- 如果是*cancelCtx并且key==&cancelCtxKey則說明找到了,直接返回,否則c= ctx.上下文繼續(xù)
- 如果是timerCtx,并且key== &cancelCtxKey則會返回內(nèi)部的cancelCtx
- 如果是*emptyCtx 則直接返回nil,
- 默認(rèn)即如果是用戶自定義實現(xiàn)則調(diào)用對應(yīng)的Value找尋
可以發(fā)現(xiàn)如果嵌套實現(xiàn)過多的話這個方法其實是一個遞歸調(diào)用。
如果是則要繼續(xù)判斷p.done與parent.Done()是否相等,如果沒有則說明:*cancelCtx已經(jīng)被包裝在一個自定義實現(xiàn)中,提供了一個不同的包裝,在這種情況下就返回nil,false:
pdone, _ := p.done.Load().(chan struct{}) if pdone != done { return nil, false } return p, true
構(gòu)造算是結(jié)束了,接下來看看如何取消的:
- 檢查err是否為nil
if err == nil { panic("context: internal error: missing cancel error") }
- 由于要對err、cancelCtx.done以及children進(jìn)行操作,所以要加鎖
- 如果c.err不為nil則說明已經(jīng)取消過了,直接返回。否則將c.err=err賦值,這里看到只有第一次調(diào)用才會賦值,多次調(diào)用由于已經(jīng)有 != nil+鎖的檢查,所以會直接返回,不會重復(fù)賦值
c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled } c.err = err
- 會嘗試從c.done獲取,如果為nil,則保存一個closedchan,否則就關(guān)閉d,這樣當(dāng)你context.Done()方法返回的channel才會返回。
d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) }
- 循環(huán)遍歷c.children去關(guān)閉子Context,可以看到釋放子context時會獲取 子Context的鎖,同時也會獲取父Context的鎖。所以才是線程安全的。結(jié)束后釋放鎖
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也比較簡單,當(dāng)為*cancelCtx就將其從Children內(nèi)刪除,為了保證線程安全也是加鎖的。
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用于告知應(yīng)用程序任務(wù)已經(jīng)終止:這一步是只讀沒有加鎖,如果沒有讀取到則嘗試加鎖,再讀一次,還沒讀到則創(chuàng)建一個chan,可以看到這是一個懶創(chuàng)建的過程。所以當(dāng)用戶主動調(diào)用CancelFunc時,其實根本就是將c.done內(nèi)存儲的chan close掉,這其中可能牽扯到父關(guān)閉,也要循環(huán)關(guān)閉子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主要內(nèi)容就這么多,接下里就是timerCtx了
計時器
回顧下timerCtx定義,就是內(nèi)嵌了一個cancelCtx另外多了兩個字段timer和deadline,這也是組合的體現(xiàn)。
type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time }
下面就看看兩個構(gòu)造函數(shù),WithDeadline與WithTimeout,WithTimeout就是對WithDealine的一層簡單封裝。
檢查不多說了, 第二個檢查如果父context的截止時間比傳遞進(jìn)來的早的話,這個時間就無用了,那么就退化成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)) }
構(gòu)造timerCtx并調(diào)用propagateCancel,這個已經(jīng)在上面介紹過了。
c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c)
接著會看,會先利用time.直到(d.分時。Now()) 來判斷傳入的 deadlineTime與當(dāng)前時間差值,如果在當(dāng)前時間之前的話說明已經(jīng)該取消了,所以會直接調(diào)用cancel函數(shù)進(jìn)行取消,并且將其從父Context中刪除。否則就創(chuàng)建一個定時器,當(dāng)時間到達(dá)會調(diào)用取消函數(shù),這里是定時調(diào)用,也可能用戶主動調(diào)用。
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實現(xiàn)吧,相比較cancelCtx就比較簡單了,先取消 cancelCtx,也要加鎖,將c.timer停止并賦值nil,這里也是第一次調(diào)用才會賦值nil,因為外層還有個c.timer !=nil的判斷,所以多次調(diào)用只有一次賦值。
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還覆蓋實現(xiàn)了一個Deadline(),就是返回當(dāng)前 Context的終止時間。
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { return c.deadline, true }
下面就到了最后一個內(nèi)置的valueCtx了。
值
結(jié)構(gòu)器就更加加單,就多了key,val
type valueCtx struct { Context key, val any }
也就有個Value method不同,可以看到底層使用的就是我們上面介紹的value函數(shù),重復(fù)復(fù)用
func (c *valueCtx) Value(key any) any { if c.key == key { return c.val } return value(c.Context, key) }
幾個主要的講解完了,可以看到不到600行代碼,就實現(xiàn)了這么多功能,其中蘊含了組合、封裝、結(jié)構(gòu)體嵌套接口等許多理念,值得好好琢磨。下面我們再看看其中有些有意思的地方。我們一般打印字符串都是使用 fmt 包,那么不使用fmt包該如何打印呢?context包里就有相應(yīng)實現(xiàn),也很簡單,就是 switch case來判斷v類型并返回,它這么做的原因也有說:
“因為我們不希望上下文依賴于unicode表”,這句話我還沒理解,有知道的小伙伴可以在底下評論,或者等我有時間看看fmt包實現(xiàn)。
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的幾個原則
直接在函數(shù)參數(shù)傳遞,不要在struct傳遞,要明確傳遞,并且作為第一個參數(shù),因為這樣可以由調(diào)用方來傳遞不同的上下文在不同的方法上,如果你在 struct內(nèi)使用context則一個實例是公用一個context也就導(dǎo)致了協(xié)程不安全,這也是為何net包Request要拷貝一個新的Request WithRequest(context go 1.7 才被引入),net包牽扯過多,要做到兼容才嵌入到 struct內(nèi)。
不要使用nil而當(dāng)你不知道使用什么時則使用TODO,如果你用了nil則會 panic。避免到處判斷是否為nil。
WithValue不應(yīng)該傳遞業(yè)務(wù)信息,只應(yīng)該傳遞類似request-id之類的請求信息。
無論用哪個類型的Context,在構(gòu)建后,一定要加上:defer cancel(),因為這個函數(shù)是可以多次調(diào)用的,但是如果沒有調(diào)用則可能導(dǎo)致Context沒有被取消繼而其關(guān)聯(lián)的上下文資源也得不到釋放。
在使用WithValue時,包應(yīng)該將鍵定義為未導(dǎo)出的類型以避免發(fā)生碰撞,這里貼個官網(wǎng)的例子:
// package user 這里為了演示直接在 main 包定義 // User 是存儲在 Context 值 type User struct { Name string Age int } // key 是非導(dǎo)出的,可以防止碰撞 type key int // userKey 是存儲 User 類型的鍵值,也是非導(dǎo)出的。 var userKey key // NewContext 創(chuàng)建一個新的 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怎么實現(xiàn)的么?你每調(diào)用一次都會包一層,并且一層一層解析,而且它會比較c.key==key,這里記住go的==比較是比較值和類型的,二者都相等才為true,而我們使用type key int所以userKey與0底層值雖然一樣,但是類型已經(jīng)不一樣了(這里就是main.userKey與0),所以外部無論定義何種類型都無法影響包內(nèi)的類型。這也是容易令人迷惑的地方
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} }
以上就是從Context到go設(shè)計理念輕松上手教程的詳細(xì)內(nèi)容,更多關(guān)于Context到go設(shè)計理念的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于Go語言構(gòu)建RESTful API服務(wù)
在實際開發(fā)項目中,你編寫的服務(wù)可以被其他服務(wù)使用,這樣就組成了微服務(wù)的架構(gòu);也可以被前端調(diào)用,這樣就可以前后端分離。那么,本文主要介紹什么是 RESTful API,以及 Go 語言是如何玩轉(zhuǎn) RESTful API 的2021-07-07GoLang RabbitMQ實現(xiàn)六種工作模式示例
這篇文章主要介紹了GoLang RabbitMQ實現(xiàn)六種工作模式,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-12-12