GoLang Context包实解


Context包 应用场景由请求衍生出来的各个goroutine 需要满足一定的约束条件
以实现诸如有效期 终止routline树 传递请求全局变量之类的功能
Context实现上下文功能 约定 在方法中 参数第一个传入context.Context类型的变量

核心接口

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

默认错误

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"
}
源码包 提供了两个空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 结构体继承Context 实现 canceler 方法
//*cancelCtx 和 *timerCtx都实现了canceler接口 该接口的类型都可以直接被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)
}
//核心是关闭c.done
//同时会设置c.err = err, c.children = nil
//依次遍历c.children 每个child分别cancel
//如果设置了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)
    }
}

cancel相关方法

type CancelFunc func()
// WithCancel方法返回一个继承自parent的Context对象 同时返回的cancel方法可以用来关闭返回的Context当中的Done channel
// 其将新建立的节点挂载在最近的可以被cancel的父节点下(向下方向) 如果传入的parent是不可被cancel的节点 则直接只保留向上关系
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 从当前传入的parent开始(包括该parent) 向上查找最近的一个可以被cancel的parent
// 如果找到的parent已经被cancel 则将方才传入的child树给cancel掉
// 否则 将child节点直接连接为找到的parent的children中(Context字段不变 即向上的父亲指针不变 但是向下的孩子指针变直接了)
// 如果没有找到最近可被cancel的parent 即其上都不可被cancel 则启动个goroutine等待传入的parent终止 则cancel传入的child树 或等待传入的child终结
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对象开始 依次往上找到一个最近的可以被cancel的对象 即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开始往上找到最近的一个可以cancel的父对象 从父对象的children map中删除这个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

继承自cancelCtx的结构体
type timerCtx struct {
    cancelCtx //此处的封装为了继承来自于cancelCtx的方法 cancelCtx.Context才是父亲节点的指针
    timer *time.Timer // Under cancelCtx.mu. 是一个计时器
    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 还回对c.timer进行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()
}
由此结构体衍生出的两个方法
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
    // 如果parent的deadline比新传入的deadline已经要早 则直接WithCancel 因为新传入的deadline没有效 父亲的deadline会先到期。
    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,
    }  // 接入树
    propagateCancel(parent, c)
    // 检查如果已经过期 则cancel新的子树
    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 {// 还没有被cancel的话 就设置deadline之后cancel的计时器
        c.timer = time.AfterFunc(d, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}
// timeout和deadline本质一样
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

Value

valueCtx主要传递元数据 通过WithValue()传入继承 通过Value()读取
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)
}

Context使用原则

Programs that use Contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation: 用Context的程序包需要遵循如下的原则来满足接口的一致性以及便于静态分析
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存在结构体中 显式地传入函数 Context变量需要作为第一个参数使用 一般命名为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.
即使方法允许 也不要传入一个nil的Context 如果你不确定你要用什么Context的时候传一个context.TODO
Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
使用context的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据 不要用它来传递一些可选的参数
The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.
同样的Context可以用来传递到不同的goroutine中 Context在多个goroutine中是安全的

Context使用要点

导入包后 初始化Context对象 在资源访问方法中都调用 然后使用时检查Context对象是否已经被Cancel
如果是 就释放绑定的资源  初始化并设置最终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.

在中间过程传递 并在资源相关操作时判断是否ctx.Done()传出了值  意外终结的程序返回值应该为对应的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
    }
}