Gorilla/context库分析

简要说明

gorilla/context 用于在请求期间保存状态,是一个很好的解决多goroutine下通知传递和元数据的Go标准库。由于Go中的goroutine之间没有父子关系,因此也不存在子进程退出后的通知机制。多个goroutine协调工作涉及通信,同步,通知,退出四个方面,但由于goroutine之间地位平等,因此当遇到复杂的并发结构时处理退出机制则会显得力不从心。因此Go1.7版本开始提供了context标准库来解决这个问题。他提供两个功能:退出通知和元数据传递。

源码分析

context接口如下,其中Deadline会返回一个超时时间,Goroutine获得了超时时间后,例如可以对某些 io 操作设定超时时间;Done 方法返回一个信道,当 Context 被撤销或过期时,该信道是关闭的,即它是一个表示 Context 是否关闭的信号;当 Done 信道关闭后,Err方法表明 Context 被撤销的原因;Value 可以让 Goroutine 共享一些数据,当然获得数据是协程安全的。但是使用这些数据的时候,需要注意同步,比如返回了一个 map,而这个 map 的读写则要加锁:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
context中的数据结构:
  1. cancelCtx的数据结构如下,其中Context接口保存父类的context,children map[canceler]struct{}保存的是所有直属与这个context的子类context。done chan struct{}用于发送退出信号:
type cancelCtx struct {
    Context
    mu       sync.Mutex            // protects following fields
    done     chan struct{}         // created lazily, closed by first cancel call
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error                 // set to non-nil by the first cancel call
}
  1. timerCtx的数据结构如下,timerCtx继承于cancelCtx,并为定时退出功能新增自己的数据结构。timerCtx可以用timerCtx.cancelCtx.Context的方法查看parent context:
type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}
  1. valueCtx的数据结构如下,它没有继承自cancelCtx struct,而是直接继承自Context接口:
type valueCtx struct {
    Context
    key, val interface{}
}
调用这些数据结构的API如下:
  1. WithCancel(parent Context) (Context, CancelFunc):创建cancelCtx实例。它是将父节点复制到子节点,并且还返回一个额外的 CancelFunc 函数类型变量。其中propagateCancel函数的作用是将自己注册至parent context。
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}
}
  1. WithDeadline(parent Context, d time.Time) (Context, CancelFunc) :创建timerCtx实例,并定义了定时退出机制相关内容。它返回 Context 的类型是 parent 的副本,但其过期时间由 deadline 和 parent 的过期时间同时决定。当 parent 的过期时间早于传入的 deadline 时间时,返回的过期时间应与 parent 相同。父节点过期时,其所有的子孙节点必须同时关闭;反之,返回的父节点的过期时间则为 deadline:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    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(true, 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) }
}
func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{Context: parent}
}
  1. WithValue(parent Context, key, val interface{}) Context:创建valueCtx实例。它返回 parent 的一个函数副本,调用该副本的 Value(key) 方法将得到 val。这样,我们不光将根节点的原有的值保留了,还在孙节点中加入了新的值,注意若存在Key相同,则会被覆盖:
func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflect.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}
一些辅助函数
  1. propagateCancel()负责注册信息,代码如下,该函数接收parent context 和 child canceler方法,若parent为emptyCtx,则不注册;否则通过funcparentCancelCtx寻找最近的一个*cancelCtx;若该cancelCtx已经结束,则调用child的cancel方法,否则向该cancelCtx注册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]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}
  1. parentCancelCtx()函数代码如下,它从parentCtx中向上迭代寻找第一个cancelCtx并返回。从函数逻辑中可以看到,只有当parent.(type)为valueCtx的时候,parent才会向上迭代而不是立即返回,否则该函数都是直接返回或返回经过包装的*cancelCtx:
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
        }
    }
}
  1. func (c *cancelCtx) cancel函数中,若外部err为空,则代表这是一个非法的cancel操作,抛出panic;若cancelCtx内部err不为空,说明该Ctx已经执行过cancel操作,直接返回;关闭done channel,关联该Ctx的goroutine收到退出通知;遍历children,若有的话,执行child.cancel操作;调用removeChild将自己从parent context中移除:
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
    if c.done == nil {
        c.done = closedchan
    } else {
        close(c.done)
    }
    for child := range c.children {
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
        removeChild(c.Context, c)
    }
}

从上述代码可见Context如何保存父类和子类上下文,以及cancel方法实现了,对于存在父子关系的ctx,一旦cancel父ctx,所有子ctx一起cancel的退出功能。

你可能感兴趣的:(学习)