GoLang Context包詳解

原文鏈接:http://studygolang.com/articles/5131

簡(jiǎn)介##

Context是一個(gè)在go中經(jīng)常用到的程序包蝙昙,谷歌官方開(kāi)發(fā)嫩与。特別常見(jiàn)的一個(gè)應(yīng)用場(chǎng)景是有一個(gè)請(qǐng)求衍生出來(lái)的各個(gè)goroutine之間需要滿足一定的約束條件侨核,以實(shí)現(xiàn)一些諸如有效期岛都、終止routline樹(shù)、傳遞請(qǐng)求全局變量之類(lèi)的功能。使用Context實(shí)現(xiàn)上下文功能約定需要在你的方法中傳入?yún)?shù)的第一個(gè)傳入context.Context類(lèi)型的變量稚字,我們將通過(guò)源碼的閱讀和一些示例代碼來(lái)說(shuō)明context的用法。

核心接口##

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

默認(rèn)錯(cuò)誤:

var Canceled = errors.New("context canceled")
var DeadlineExceeded error = deadlineExceededError{}
type deadlineExceededError struct{}

func (deadlineExceededError) Error() string   { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool   { return true }
func (deadlineExceededError) Temporary() bool { return true }

空Context

// 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 interface{}) interface{} {
    return nil
}

func (e *emptyCtx) String() string {
    switch e {
    case background:
        return "context.Background"
    case todo:
        return "context.TODO"
    }
    return "unknown empty Context"
}

源碼包中提供了兩個(gè)空Context

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). TODO is recognized by static analysis tools that determine
// whether Contexts are propagated correctly in a program.
func TODO() Context {
    return todo
}

canceler##

cancelCtx結(jié)構(gòu)體繼承了Context厦酬,實(shí)現(xiàn)了canceler方法:

//*cancelCtx 和 *timerCtx都實(shí)現(xiàn)了canceler接口胆描,實(shí)現(xiàn)該接口的類(lèi)型都可以直接被cancel
// 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{}
}

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
    Context

    done chan struct{} // closed by the first cancel call.

    mu       sync.Mutex
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error                 // set to non-nil by the first cancel call
}

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中刪除
// 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
    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)
    }
}

再來(lái)看一些cancel相關(guān)的方法:

type CancelFunc func()

// WithCancel方法返回一個(gè)繼承自parent的Context對(duì)象昌讲,同時(shí)返回的cancel方法可以用來(lái)關(guān)閉返回的Context當(dāng)中的Done channel
// 其將新建立的節(jié)點(diǎn)掛載在最近的可以被cancel的父節(jié)點(diǎn)下(向下方向)
// 如果傳入的parent是不可被cancel的節(jié)點(diǎn),則直接只保留向上關(guān)系
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

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

// 傳遞cancel
// 從當(dāng)前傳入的parent開(kāi)始(包括該parent)霹菊,向上查找最近的一個(gè)可以被cancel的parent
// 如果找到的parent已經(jīng)被cancel剧蚣,則將方才傳入的child樹(shù)給cancel掉
// 否則,將child節(jié)點(diǎn)直接連接為找到的parent的children中(Context字段不變旋廷,即向上的父親指針不變鸠按,但是向下的孩子指針變直接了)
// 
// 如果沒(méi)有找到最近的可以被cancel的parent,即其上都不可被cancel饶碘,則啟動(dòng)一個(gè)goroutine等待傳入的parent終止目尖,則cancel傳入的child樹(shù),或者等待傳入的child終結(jié)扎运。
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]bool)
            }
            p.children[child] = true
        }
        p.mu.Unlock()
    } else {
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}

// 從傳入的parent對(duì)象開(kāi)始瑟曲,依次往上找到一個(gè)最近的可以被cancel的對(duì)象,即cancelCtx或者timerCtx
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
    for {
        switch c := parent.(type) {
        case *cancelCtx:
            return c, true
        case *timerCtx:
            return &c.cancelCtx, true
        case *valueCtx:
            parent = c.Context
        default:
            return nil, false
        }
    }
}

//從parent開(kāi)始往上找到最近的一個(gè)可以cancel的父對(duì)象
// 從父對(duì)象的children map中刪除這個(gè)child
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()
}

Deadline 和 TimeOut
首先看一個(gè)繼承自cancelCtx的結(jié)構(gòu)體:

type timerCtx struct {
    cancelCtx //此處的封裝為了繼承來(lái)自于cancelCtx的方法豪治,cancelCtx.Context才是父親節(jié)點(diǎn)的指針
    timer *time.Timer // Under cancelCtx.mu. 是一個(gè)計(jì)時(shí)器
    deadline time.Time
}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
    return c.deadline, true
}

func (c *timerCtx) String() string {
    return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, c.deadline.Sub(time.Now()))
}

// 與cencelCtx有所不同洞拨,其除了處理cancelCtx.cancel,還回對(duì)c.timer進(jìn)行Stop()负拟,并將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()
}

由此結(jié)構(gòu)體衍生出的兩個(gè)方法:

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
    // 如果parent的deadline比新傳入的deadline已經(jīng)要早烦衣,則直接WithCancel,因?yàn)樾聜魅氲膁eadline沒(méi)有效掩浙,父親的deadline會(huì)先到期花吟。
    if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  deadline,
    }
    // 接入樹(shù)
    propagateCancel(parent, c)

    // 檢查如果已經(jīng)過(guò)期,則cancel新的子樹(shù)
    d := deadline.Sub(time.Now())
    if d <= 0 {
        c.cancel(true, DeadlineExceeded) // deadline has already passed
        return c, func() { c.cancel(true, Canceled) }
    }

    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        // 還沒(méi)有被cancel的話厨姚,就設(shè)置deadline之后cancel的計(jì)時(shí)器
        c.timer = time.AfterFunc(d, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}


// timeout和deadline本質(zhì)一樣
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

Value
valueCtx主要用來(lái)傳遞一些元數(shù)據(jù)衅澈,通過(guò)WithValue()來(lái)傳入繼承,通過(guò)Value()來(lái)讀取谬墙,簡(jiǎn)單今布,不贅述.

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

type valueCtx struct {
    Context
    key, val interface{}
}

func (c *valueCtx) String() string {
    return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

使用原則:##

  • Programs that use Contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation:
    使用Context的程序包需要遵循如下的原則來(lái)滿足接口的一致性以及便于靜態(tài)分析

  • Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx:
    不要把Context存在一個(gè)結(jié)構(gòu)體當(dāng)中经备,顯式地傳入函數(shù)。Context變量需要作為第一個(gè)參數(shù)使用部默,一般命名為ctx

func DoSomething(ctx context.Context, arg Arg) error {
 ... use ctx ...
 }
  • Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.
    即使方法允許弄喘,也不要傳入一個(gè)nil的Context,如果你不確定你要用什么Context的時(shí)候傳一個(gè)context.TODO

  • Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
    使用context的Value相關(guān)方法只應(yīng)該用于在程序和接口中傳遞的和請(qǐng)求相關(guān)的元數(shù)據(jù)甩牺,不要用它來(lái)傳遞一些可選的參數(shù)

  • The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.
    同樣的Context可以用來(lái)傳遞到不同的goroutine中,Context在多個(gè)goroutine中是安全的

使用要點(diǎn):##

其實(shí)本身非常簡(jiǎn)單累奈,在導(dǎo)入這個(gè)包之后贬派,初始化Context對(duì)象,在每個(gè)資源訪問(wèn)方法中都調(diào)用它澎媒,然后在使用時(shí)檢查Context對(duì)象是否已經(jīng)被Cancel搞乏,如果是就釋放綁定的資源。如下所示(下面的代碼截取自http://blog.golang.org/context中的示例程序)
初始化并設(shè)置最終cancel:

func handleSearch(w http.ResponseWriter, req *http.Request) {
    // ctx is the Context for this handler. Calling cancel closes the
    // ctx.Done channel, which is the cancellation signal for requests
    // started by this handler.
    var (
        ctx    context.Context
        cancel context.CancelFunc
    )
    timeout, err := time.ParseDuration(req.FormValue("timeout"))
    if err == nil {
        // The request has a timeout, so create a context that is
        // canceled automatically when the timeout expires.
        ctx, cancel = context.WithTimeout(context.Background(), timeout)
    } else {
        ctx, cancel = context.WithCancel(context.Background())
    }
    defer cancel() // Cancel ctx as soon as handleSearch returns.

在中間過(guò)程傳遞戒努,并在資源相關(guān)操作時(shí)判斷是否ctx.Done()傳出了值请敦,關(guān)于Done()的使用應(yīng)該參考http://blog.golang.org/pipelines 意外終結(jié)的程序返回值應(yīng)該為對(duì)應(yīng)的ctx.Err()

func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
    // Run the HTTP request in a goroutine and pass the response to f.
    tr := &http.Transport{}
    client := &http.Client{Transport: tr}
    c := make(chan error, 1)
    go func() { c <- f(client.Do(req)) }()
    select {
    case <-ctx.Done():
        tr.CancelRequest(req)
        <-c // Wait for f to return.
        return ctx.Err()
    case err := <-c:
        return err
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市储玫,隨后出現(xiàn)的幾起案子侍筛,更是在濱河造成了極大的恐慌,老刑警劉巖撒穷,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件匣椰,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡端礼,警方通過(guò)查閱死者的電腦和手機(jī)禽笑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蛤奥,“玉大人佳镜,你說(shuō)我怎么就攤上這事》睬牛” “怎么了蟀伸?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)唬血。 經(jīng)常有香客問(wèn)我望蜡,道長(zhǎng),這世上最難降的妖魔是什么拷恨? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任脖律,我火速辦了婚禮,結(jié)果婚禮上腕侄,老公的妹妹穿的比我還像新娘小泉。我一直安慰自己芦疏,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布微姊。 她就那樣靜靜地躺著酸茴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪兢交。 梳的紋絲不亂的頭發(fā)上薪捍,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音配喳,去河邊找鬼酪穿。 笑死,一個(gè)胖子當(dāng)著我的面吹牛晴裹,可吹牛的內(nèi)容都是我干的被济。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼涧团,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼只磷!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起泌绣,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤钮追,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后赞别,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體畏陕,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年仿滔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了惠毁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡崎页,死狀恐怖鞠绰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情飒焦,我是刑警寧澤蜈膨,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站牺荠,受9級(jí)特大地震影響翁巍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜休雌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一灶壶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧杈曲,春花似錦驰凛、人聲如沸胸懈。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)趣钱。三九已至,卻和暖如春胚宦,著一層夾襖步出監(jiān)牢的瞬間首有,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工枢劝, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留绞灼,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓呈野,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親印叁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子被冒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容