Go中Context使用源碼解析
前言
本篇內(nèi)容的主題是Go中Context,想必已學(xué)習(xí)Go語(yǔ)言的大家在熟悉不過(guò)了。工作中我們也常會(huì)用到,但有時(shí)很少去注意它。
本打算將相關(guān)知識(shí)點(diǎn)歸總一下,發(fā)現(xiàn)其源碼不多,就打算對(duì)其源碼進(jìn)行分析一下。
context包是在go1.17是引入到標(biāo)準(zhǔn)庫(kù)中,且標(biāo)準(zhǔn)庫(kù)中大部分接口都將context.Context作為第一個(gè)參數(shù)。
context中文譯為“上下文”,實(shí)際代表的是goroutine的上下文。且多用于超時(shí)控制和多個(gè)goroutine間的數(shù)據(jù)傳遞。
本篇文章將帶領(lǐng)大家深入了解其內(nèi)部的工作原理。
1、Context定義
Context 接口定義如下
type Context interface {
// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)
// Done returns a channel that is closed when this Context is canceled
// or times out.
Done() <-chan struct{}
// Err indicates why this context was canceled, after the Done channel
// is closed.
Err() error
// Value returns the value associated with key or nil if none.
Value(key any) any
}
Deadline(): 返回的第一個(gè)值是 截止時(shí)間,到了這個(gè)時(shí)間點(diǎn),Context 會(huì)自動(dòng)觸發(fā) Cancel 動(dòng)作。返回的第二個(gè)值是 一個(gè)布爾值,true 表示設(shè)置了截止時(shí)間,false 表示沒(méi)有設(shè)置截止時(shí)間,如果沒(méi)有設(shè)置截止時(shí)間,就要手動(dòng)調(diào)用 cancel 函數(shù)取消 Context。
Done(): 返回一個(gè)只讀的通道(只有在被cancel后才會(huì)返回),類型為 struct{}。當(dāng)這個(gè)通道可讀時(shí),意味著parent context已經(jīng)發(fā)起了取消請(qǐng)求,根據(jù)這個(gè)信號(hào),開(kāi)發(fā)者就可以做一些清理動(dòng)作,退出goroutine。這里就簡(jiǎn)稱信號(hào)通道吧!
Err():返回Context 被取消的原因
Value: 從Context中獲取與Key關(guān)聯(lián)的值,如果沒(méi)有就返回nil
2、Context的派生
2.1、創(chuàng)建Context對(duì)象
context包提供了四種方法來(lái)創(chuàng)建context對(duì)象,具體方法如下:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {}
func WithValue(parent Context, key, val any) Context {}
由以上方法可知:新的context對(duì)象都是基于父context對(duì)象衍生的.
WithCancel:創(chuàng)建可以取消的ContextWithDeadline: 創(chuàng)建帶有截止時(shí)間的ContextWithTimeout:創(chuàng)建帶有超時(shí)時(shí)間的Context,底層調(diào)用的是WithDeadline方法WithValue:創(chuàng)建可以攜帶KV型數(shù)據(jù)的Context
簡(jiǎn)單的樹(shù)狀關(guān)系如下(實(shí)際可衍生很多中):

2.2、parent Context
context包默認(rèn)提供了兩個(gè)根context 對(duì)象background和todo;看實(shí)現(xiàn)兩者都是由emptyCtx創(chuàng)建的,兩者的區(qū)別主要在語(yǔ)義上,
- context.Background 是上下文的默認(rèn)值,所有其他的上下文都應(yīng)該從它衍生出來(lái);
- context.TODO 應(yīng)該僅在不確定應(yīng)該使用哪種上下文時(shí)使用
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// Background 創(chuàng)建background context
func Background() Context {
return background
}
// TODO 創(chuàng)建todo context
func TODO() Context {
return todo
}
3、context 接口四種實(shí)現(xiàn)

具體結(jié)構(gòu)如下,我們大致看下相關(guān)結(jié)構(gòu)體中包含的字段,具體字段的含義及作用將在下面分析中會(huì)提及。
- emptyCtx
type emptyCtx int // 空context
- cancelCtx
type cancelCtx struct {
Context // 父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 // cancel的原因
}
- timerCtx
type timerCtx struct {
cancelCtx //父context
timer *time.Timer // 定時(shí)器
deadline time.Time // 截止時(shí)間
}
- valueCtx
type valueCtx struct {
Context // 父context
key, val any // kv鍵值對(duì)
}
4、 emptyCtx 源碼分析
emptyCtx實(shí)現(xiàn)非常簡(jiǎn)單,具體代碼如下,我們簡(jiǎn)單看看就可以了
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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"
}
5、 cancelCtx 源碼分析
cancelCtx 的實(shí)現(xiàn)相對(duì)復(fù)雜點(diǎn),比如下面要介紹的timeCtx 底層也依賴它,所以弄懂cancelCtx的工作原理就能很好的理解context.
cancelCtx 不僅實(shí)現(xiàn)了Context接口也實(shí)現(xiàn)了canceler接口
5.1、對(duì)象創(chuàng)建withCancel()
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil { // 參數(shù)校驗(yàn)
panic("cannot create context from nil parent")
}
// cancelCtx 初始化
c := newCancelCtx(parent)
propagateCancel(parent, &c) // cancelCtx 父子關(guān)系維護(hù)及傳播取消信號(hào)
return &c, func() { c.cancel(true, Canceled) } // 返回cancelCtx對(duì)象及cannel方法
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
用戶調(diào)用WithCancel方法,傳入一個(gè)父 Context(這通常是一個(gè) background,作為根節(jié)點(diǎn)),返回新建的 context,并通過(guò)閉包的形式返回了一個(gè) cancel 方法。如果想要取消context時(shí)需手動(dòng)調(diào)用cancel方法。
5.1.1、newCancelCtx
cancelCtx對(duì)象初始化, 其結(jié)構(gòu)如下:
type cancelCtx struct {
// 父 context
Context // parent context
// 鎖 并發(fā)場(chǎng)景下保護(hù)cancelCtx結(jié)構(gòu)中字段屬性的設(shè)置
mu sync.Mutex // protects following fields
// done里存儲(chǔ)的是信號(hào)通道,其創(chuàng)建方式采用的是懶加載的方式
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
// 記錄與父子cancelCtx對(duì)象,
children map[canceler]struct{} // set to nil by the first cancel call
// 記錄ctx被取消的原因
err error // set to non-nil by the first cancel call
}
5.1.2、propagateCancel
propagateCancel
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
done := parent.Done() // 獲取parent ctx的信號(hào)通道 done
if done == nil { // nil 代表 parent ctx 不是canelctx 類型,不會(huì)被取消,直接返回
return // parent is never canceled
}
select { // parent ctx 是cancelCtx類型,判斷其是否被取消
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
//parentCancelCtx往樹(shù)的根節(jié)點(diǎn)方向找到最近的context是cancelCtx類型的
if p, ok := parentCancelCtx(parent); ok { // 查詢到
p.mu.Lock() // 加鎖
if p.err != nil { // 祖父 ctx 已經(jīng)被取消了,則 子cancelCtx 也需要調(diào)用cancel 方法來(lái)取消
// parent has already been canceled
child.cancel(false, p.err)
} else { // 使用map結(jié)構(gòu)來(lái)維護(hù) 將child加入到祖父context中
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()// 解鎖
} else { // 開(kāi)啟協(xié)程監(jiān)聽(tīng) parent Ctx的取消信號(hào) 來(lái)通知child ctx 取消
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
// parentCancelCtx returns the underlying *cancelCtx for parent.
// It does this by looking up parent.Value(&cancelCtxKey) to find
// the innermost enclosing *cancelCtx and then checking whether
// parent.Done() matches that *cancelCtx. (If not, the *cancelCtx
// has been wrapped in a custom implementation providing a
// different done channel, in which case we should not bypass it.)
// parentCancelCtx往樹(shù)的根節(jié)點(diǎn)方向找到最近的context是cancelCtx類型的
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
// closedchan 代表此時(shí)cancelCtx 已取消, nil 代表 ctx不是cancelCtx 類型的且不會(huì)被取消
if done == closedchan || done == nil {
return nil, false
}
// 向上遍歷查詢canelCtx 類型的ctx
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if !ok { // 沒(méi)有
return nil, false
}
// 存在判斷信號(hào)通道是不是相同
pdone, _ := p.done.Load().(chan struct{})
if pdone != done {
return nil, false
}
return p, true
}
5.2 canceler
cancelCtx也實(shí)現(xiàn)了canceler接口,實(shí)現(xiàn)可以 取消上下文的功能。
canceler接口定義如下:
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
cancel(removeFromParent bool, err error) // 取消
Done() <-chan struct{} // 只讀通道,簡(jiǎn)稱取消信號(hào)通道
}
cancelCtx 接口實(shí)現(xiàn)如下:
整體邏輯不復(fù)雜,邏輯簡(jiǎn)化如下:
- 當(dāng)前 cancelCtx 取消 且 與之其關(guān)聯(lián)的子 cancelCtx 也取消
- 根據(jù)removeFromParent標(biāo)識(shí)來(lái)判斷是否將子 cancelCtx 移除
注意
由于信號(hào)通道的初始化采用的懶加載方式,所以有未初始化的情況;
已初始化的:調(diào)用close 函數(shù)關(guān)閉channel
未初始化的:用 closedchan 初始化,其closedchan是已經(jīng)關(guān)閉的channel。
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
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()
if removeFromParent {
removeChild(c.Context, c)
}
}
// removeChild removes a context from its parent.
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()
}
closedchan
可重用的關(guān)閉通道,該channel通道默認(rèn)已關(guān)閉
// closedchan is a reusable closed channel.
var closedchan = make(chan struct{})
func init() {
close(closedchan) // 調(diào)用close 方法關(guān)閉
}
6、timerCtx 源碼分析
cancelCtx源碼已經(jīng)分析完畢,那timerCtx理解起來(lái)就很容易。
關(guān)注點(diǎn):timerCtx是如何取消上下文的,以及取消上下文的方式
6.1、對(duì)象創(chuàng)建 WithDeadline和WithTimeout
WithTimeout 底層調(diào)用是WithDeadline 方法 ,截止時(shí)間是 now+timeout;
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
WithDeadline 整體邏輯并不復(fù)雜,從源碼中可分析出timerCtx取消上下文 采用兩種方式 自動(dòng)和手動(dòng);其中自動(dòng)方式采用定時(shí)器去處理,到達(dá)觸發(fā)時(shí)刻,自動(dòng)調(diào)用cancel方法。
deadline: 截止時(shí)間
timer *time.Timer : 定時(shí)器
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
}
6.2 timerCtx的cancel
- 調(diào)用cancelCtx的cancel 方法
- 根據(jù)removeFromParent標(biāo)識(shí),為true 調(diào)用removeChild 方法 從它的父cancelCtx的children中移除
- 關(guān)閉定時(shí)器 ,防止內(nèi)存泄漏(著重點(diǎn))
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()
}
7、valueCtx 源碼分析
7.1、對(duì)象創(chuàng)建WithValue
valueCtx 結(jié)構(gòu)體中有key 和val兩個(gè)字段,WithValue 方法也是將數(shù)據(jù)存放在該字段上
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}
}
// 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
}
7.2、獲取value值
func (c *valueCtx) Value(key any) any {
if c.key == key { // 判斷當(dāng)前valuectx對(duì)象中的key是否匹配
return c.val
}
return value(c.Context, key)
}
// value() 向根部方向遍歷,直到找到與key對(duì)應(yīng)的值
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 { // 獲取cancelCtx對(duì)象
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)
}
}
}
總結(jié):從Context 中獲取對(duì)應(yīng)的值需要通過(guò)遍歷的方式來(lái)獲取,這里告誡我們嵌套太多的context反而對(duì)性能會(huì)有影響
8、規(guī)范&注意事項(xiàng)
- 不要把context存在一個(gè)結(jié)構(gòu)體當(dāng)中,顯式地傳入函數(shù)。context變量需要作為第一個(gè)參數(shù)使用,一般命名為ctx;
- 即使方法允許,也不要傳入一個(gè)nil的Context,如果你不確定你要用什么Context的時(shí)候傳一個(gè)context.TODO
- 使用context的Value相關(guān)方法只應(yīng)該用于在程序和接口中傳遞的和請(qǐng)求相關(guān)的元數(shù)據(jù),不要用它來(lái)傳遞一些可選的參數(shù)
- 同樣的Context可以用來(lái)傳遞到不同的goroutine中,Context在多個(gè)goroutine中是安全的
以上就是Go中Context使用源碼解析的詳細(xì)內(nèi)容,更多關(guān)于Go Context源碼解析的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang http 連接超時(shí)和傳輸超時(shí)的例子
今天小編就為大家分享一篇golang http 連接超時(shí)和傳輸超時(shí)的例子,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-07-07
Golang定制化zap日志庫(kù)使用過(guò)程分析
Zap是我個(gè)人比較喜歡的日志庫(kù),是uber開(kāi)源的,有較好的性能,在項(xiàng)目開(kāi)發(fā)中,經(jīng)常需要把程序運(yùn)行過(guò)程中各種信息記錄下來(lái),有了詳細(xì)的日志有助于問(wèn)題排查和功能優(yōu)化,但如何選擇和使用性能好功能強(qiáng)大的日志庫(kù),這個(gè)就需要我們從多角度考慮2023-03-03
B站新一代 golang規(guī)則引擎gengine基礎(chǔ)語(yǔ)法
這篇文章主要為大家介紹了B站新一代 golang規(guī)則引擎gengine基礎(chǔ)語(yǔ)法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
詳解Golang利用反射reflect動(dòng)態(tài)調(diào)用方法
這篇文章主要介紹了詳解Golang利用反射reflect動(dòng)態(tài)調(diào)用方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11
golang如何實(shí)現(xiàn)三元運(yùn)算符功能
這篇文章主要介紹了在其他一些編程語(yǔ)言中,如?C?語(yǔ)言,三元運(yùn)算符是一種可以用一行代碼實(shí)現(xiàn)條件選擇的簡(jiǎn)便方法,那么在Go語(yǔ)言中如何實(shí)現(xiàn)類似功能呢,下面就跟隨小編一起學(xué)習(xí)一下吧2024-02-02

