Golang Context分析

[TOC]

Golang Context分析

Context背景 和 適用場景

golang在1.6.2的時(shí)候還沒有自己的context,在1.7的版本中就把golang.org/x/net/context包被加入到了官方的庫中。golang 的 Context包,是專門用來簡化對于處理單個(gè)請求的多個(gè)goroutine之間與請求域的數(shù)據(jù)、取消信號、截止時(shí)間等相關(guān)操作,這些操作可能涉及多個(gè) API 調(diào)用。

比如有一個(gè)網(wǎng)絡(luò)請求Request,每個(gè)Request都需要開啟一個(gè)goroutine做一些事情,這些goroutine又可能會(huì)開啟其他的goroutine。這樣的話, 我們就可以通過Context,來跟蹤這些goroutine,并且通過Context來控制他們的目的,這就是Go語言為我們提供的Context,中文可以稱之為“上下文”。

另外一個(gè)實(shí)際例子是,在Go服務(wù)器程序中,每個(gè)請求都會(huì)有一個(gè)goroutine去處理。然而,處理程序往往還需要?jiǎng)?chuàng)建額外的goroutine去訪問后端資源,比如數(shù)據(jù)庫、RPC服務(wù)等。由于這些goroutine都是在處理同一個(gè)請求,所以它們往往需要訪問一些共享的資源,比如用戶身份信息、認(rèn)證token、請求截止時(shí)間等。而且如果請求超時(shí)或者被取消后,所有的goroutine都應(yīng)該馬上退出并且釋放相關(guān)的資源。這種情況也需要用Context來為我們?nèi)∠羲術(shù)oroutine

如果要使用可以通過 go get golang.org/x/net/context 命令獲取這個(gè)包。

Context 定義

ontext的主要數(shù)據(jù)結(jié)構(gòu)是一種嵌套的結(jié)構(gòu)或者說是單向的繼承關(guān)系的結(jié)構(gòu),比如最初的context是一個(gè)小盒子,里面裝了一些數(shù)據(jù),之后從這個(gè)context繼承下來的children就像在原本的context中又套上了一個(gè)盒子,然后里面裝著一些自己的數(shù)據(jù)?;蛘哒fcontext是一種分層的結(jié)構(gòu),根據(jù)使用場景的不同,每一層context都具備有一些不同的特性,這種層級式的組織也使得context易于擴(kuò)展,職責(zé)清晰。

context 包的核心是 struct Context,聲明如下:

type Context interface {

Deadline() (deadline time.Time, ok bool)

Done() <-chan struct{}

Err() error

Value(key interface{}) interface{}

}

可以看到Context是一個(gè)interface,在golang里面,interface是一個(gè)使用非常廣泛的結(jié)構(gòu),它可以接納任何類型。Context定義很簡單,一共4個(gè)方法,我們需要能夠很好的理解這幾個(gè)方法

  1. Deadline方法是獲取設(shè)置的截止時(shí)間的意思,第一個(gè)返回式是截止時(shí)間,到了這個(gè)時(shí)間點(diǎn),Context會(huì)自動(dòng)發(fā)起取消請求;第二個(gè)返回值ok==false時(shí)表示沒有設(shè)置截止時(shí)間,如果需要取消的話,需要調(diào)用取消函數(shù)進(jìn)行取消。

  2. Done方法返回一個(gè)只讀的chan,類型為struct{},我們在goroutine中,如果該方法返回的chan可以讀取,則意味著parent context已經(jīng)發(fā)起了取消請求,我們通過Done方法收到這個(gè)信號后,就應(yīng)該做清理操作,然后退出goroutine,釋放資源。之后,Err 方法會(huì)返回一個(gè)錯(cuò)誤,告知為什么 Context 被取消。

  3. Err方法返回取消的錯(cuò)誤原因,因?yàn)槭裁碈ontext被取消。

  4. Value方法獲取該Context上綁定的值,是一個(gè)鍵值對,所以要通過一個(gè)Key才可以獲取對應(yīng)的值,這個(gè)值一般是線程安全的。

Context 的實(shí)現(xiàn)方法

Context 雖然是個(gè)接口,但是并不需要使用方實(shí)現(xiàn),golang內(nèi)置的context 包,已經(jīng)幫我們實(shí)現(xiàn)了2個(gè)方法,一般在代碼中,開始上下文的時(shí)候都是以這兩個(gè)作為最頂層的parent context,然后再衍生出子context。這些 Context 對象形成一棵樹:當(dāng)一個(gè) Context 對象被取消時(shí),繼承自它的所有 Context 都會(huì)被取消。兩個(gè)實(shí)現(xiàn)如下:

var (
    background = new(emptyCtx)

    todo = new(emptyCtx)
)

func Background() Context {

    return background

}

func TODO() Context {

    return todo
}

一個(gè)是Background,主要用于main函數(shù)、初始化以及測試代碼中,作為Context這個(gè)樹結(jié)構(gòu)的最頂層的Context,也就是根Context,它不能被取消。

一個(gè)是TODO,如果我們不知道該使用什么Context的時(shí)候,可以使用這個(gè),但是實(shí)際應(yīng)用中,暫時(shí)還沒有使用過這個(gè)TODO。

他們兩個(gè)本質(zhì)上都是emptyCtx結(jié)構(gòu)體類型,是一個(gè)不可取消,沒有設(shè)置截止時(shí)間,沒有攜帶任何值的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 interface{}) interface{} {

    return nil
}

Context 的 繼承

有了如上的根Context,那么是如何衍生更多的子Context的呢?這就要靠context包為我們提供的With系列的函數(shù)了。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

func WithValue(parent Context, key, val interface{}) Context

通過這些函數(shù),就創(chuàng)建了一顆Context樹,樹的每個(gè)節(jié)點(diǎn)都可以有任意多個(gè)子節(jié)點(diǎn),節(jié)點(diǎn)層級可以有任意多個(gè)。

WithCancel函數(shù),傳遞一個(gè)父Context作為參數(shù),返回子Context,以及一個(gè)取消函數(shù)用來取消Context。

WithDeadline函數(shù),和WithCancel差不多,它會(huì)多傳遞一個(gè)截止時(shí)間參數(shù),意味著到了這個(gè)時(shí)間點(diǎn),會(huì)自動(dòng)取消Context,當(dāng)然我們也可以不等到這個(gè)時(shí)候,可以提前通過取消函數(shù)進(jìn)行取消。

WithTimeout和WithDeadline基本上一樣,這個(gè)表示是超時(shí)自動(dòng)取消,是多少時(shí)間后自動(dòng)取消Context的意思。

WithValue函數(shù)和取消Context無關(guān),它是為了生成一個(gè)綁定了一個(gè)鍵值對數(shù)據(jù)的Context,這個(gè)綁定的數(shù)據(jù)可以通過Context.Value方法訪問到,這是我們實(shí)際用經(jīng)常要用到的技巧,一般我們想要通過上下文來傳遞數(shù)據(jù)時(shí),可以通過這個(gè)方法,如我們需要tarce追蹤系統(tǒng)調(diào)用棧的時(shí)候。

With 系列函數(shù)詳解

WithCancel

context.WithCancel生成了一個(gè)withCancel的實(shí)例以及一個(gè)cancelFuc,這個(gè)函數(shù)就是用來關(guān)閉ctxWithCancel中的 Done channel 函數(shù)。

下面來分析下源碼實(shí)現(xiàn),首先看看初始化,如下:

func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{
        Context: parent,
        done:    make(chan struct{}),
    }
}

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

newCancelCtx返回一個(gè)初始化的cancelCtx,cancelCtx結(jié)構(gòu)體繼承了Context,實(shí)現(xiàn)了canceler方法:

//*cancelCtx 和 *timerCtx 都實(shí)現(xiàn)了canceler接口,實(shí)現(xiàn)該接口的類型都可以被直接canceled
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}


type cancelCtx struct {
    Context
    done chan struct{} // closed by the first cancel call.
    mu       sync.Mutex
    children map[canceler]bool // set to nil by the first cancel call
    err      error             // 當(dāng)其被cancel時(shí)將會(huì)把err設(shè)置為非nil
}

func (c *cancelCtx) Done() <-chan struct{} {
    return c.done
}

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.err
}

func (c *cancelCtx) String() string {
    return fmt.Sprintf("%v.WithCancel", c.Context)
}

//核心是關(guān)閉c.done
//同時(shí)會(huì)設(shè)置c.err = err, c.children = nil
//依次遍歷c.children,每個(gè)child分別cancel
//如果設(shè)置了removeFromParent,則將c從其parent的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
    close(c.done)
    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) // 從此處可以看到 cancelCtx的Context項(xiàng)是一個(gè)類似于parent的概念
    }
}

可以看到,所有的children都存在一個(gè)map中;Done方法會(huì)返回其中的done channel, 而另外的cancel方法會(huì)關(guān)閉Done channel并且逐層向下遍歷,關(guān)閉children的channel,并且將當(dāng)前canceler從parent中移除。

WithCancel初始化一個(gè)cancelCtx的同時(shí),還執(zhí)行了propagateCancel方法,最后返回一個(gè)cancel function。

propagateCancel 方法定義如下:

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
    if parent.Done() == nil {
        return // parent is never canceled
    }
    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 {
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}

propagateCancel 的含義就是傳遞cancel,從當(dāng)前傳入的parent開始(包括該parent),向上查找最近的一個(gè)可以被cancel的parent, 如果找到的parent已經(jīng)被cancel,則將方才傳入的child樹給cancel掉,否則,將child節(jié)點(diǎn)直接連接為找到的parent的children中(Context字段不變,即向上的父親指針不變,但是向下的孩子指針變直接了); 如果沒有找到最近的可以被cancel的parent,即其上都不可被cancel,則啟動(dòng)一個(gè)goroutine等待傳入的parent終止,則cancel傳入的child樹,或者等待傳入的child終結(jié)。

WithDeadLine

在withCancel的基礎(chǔ)上進(jìn)行的擴(kuò)展,如果時(shí)間到了之后就進(jìn)行cancel的操作,具體的操作流程基本上與withCancel一致,只不過控制cancel函數(shù)調(diào)用的時(shí)機(jī)是有一個(gè)timeout的channel所控制的。

Context 使用原則 和 技巧

  • 不要把Context放在結(jié)構(gòu)體中,要以參數(shù)的方式傳遞,parent Context一般為Background
  • 應(yīng)該要把Context作為第一個(gè)參數(shù)傳遞給入口請求和出口請求鏈路上的每一個(gè)函數(shù),放在第一位,變量名建議都統(tǒng)一,如ctx。
  • 給一個(gè)函數(shù)方法傳遞Context的時(shí)候,不要傳遞nil,否則在tarce追蹤的時(shí)候,就會(huì)斷了連接
  • Context的Value相關(guān)方法應(yīng)該傳遞必須的數(shù)據(jù),不要什么數(shù)據(jù)都使用這個(gè)傳遞
  • Context是線程安全的,可以放心的在多個(gè)goroutine中傳遞
  • 可以把一個(gè) Context 對象傳遞給任意個(gè)數(shù)的 gorotuine,對它執(zhí)行 取消 操作時(shí),所有 goroutine 都會(huì)接收到取消信號。

Context的常用方法實(shí)例

  1. 調(diào)用Context Done方法取消

    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:
            }
        }
    }
    
    
  2. 通過 context.WithValue 來傳值

    func main() {
        ctx, cancel := context.WithCancel(context.Background())
    
        valueCtx := context.WithValue(ctx, key, "add value")
    
        go watch(valueCtx)
        time.Sleep(10 * time.Second)
        cancel()
    
        time.Sleep(5 * time.Second)
    }
    
    func watch(ctx context.Context) {
        for {
            select {
            case <-ctx.Done():
                //get value
                fmt.Println(ctx.Value(key), "is cancel")
    
                return
            default:
                //get value
                fmt.Println(ctx.Value(key), "int goroutine")
    
                time.Sleep(2 * time.Second)
            }
        }
    }
    
    
  3. 超時(shí)取消 context.WithTimeout

    package main
    
    import (
        "fmt"
        "sync"
        "time"
    
        "golang.org/x/net/context"
    )
    
    var (
        wg sync.WaitGroup
    )
    
    func work(ctx context.Context) error {
        defer wg.Done()
    
        for i := 0; i < 1000; i++ {
            select {
            case <-time.After(2 * time.Second):
                fmt.Println("Doing some work ", i)
    
            // we received the signal of cancelation in this channel
            case <-ctx.Done():
                fmt.Println("Cancel the context ", i)
                return ctx.Err()
            }
        }
        return nil
    }
    
    func main() {
        ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
        defer cancel()
    
        fmt.Println("Hey, I'm going to do some work")
    
        wg.Add(1)
        go work(ctx)
        wg.Wait()
    
        fmt.Println("Finished. I'm going home")
    }
    
    
  4. 截止時(shí)間 取消 context.WithDeadline

    package main
    
    import (
        "context"
        "fmt"
        "time"
    )
    
    func main() {
        d := time.Now().Add(1 * time.Second)
        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(2 * time.Second):
            fmt.Println("oversleep")
        case <-ctx.Done():
            fmt.Println(ctx.Err())
        }
    }
    
    

參考

飛雪無情的博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 為什么需要context 在并發(fā)程序中,由于超時(shí)、取消操作或者一些異常情況,往往需要進(jìn)行搶占操作或者中斷后續(xù)操作。...
    陳二狗想吃肉閱讀 756評論 0 2
  • context context包定義了上下文類型,該類型在API邊界之間以及進(jìn)程之間傳遞截止日期,取消信號和其他請...
    DevilRoshan閱讀 474評論 0 0
  • 輸入與輸出-fmt包 時(shí)間與日期-time包 命令行參數(shù)解析-flag包 日志-log包 IO操作-os包 IO操...
    思考的山羊閱讀 6,747評論 0 5
  • 在工程化的Go語言開發(fā)項(xiàng)目中,Go語言的源碼復(fù)用是建立在包(package)基礎(chǔ)之上的。本文介紹了Go語言中如何定...
    雪上霜閱讀 291評論 0 0
  • 在GO中,我們需要有能力管理并發(fā)運(yùn)行中的goroutine,主要是指它的生命周期。那些失去控制的goroutine...
    浩軒01閱讀 331評論 0 0

友情鏈接更多精彩內(nèi)容