一个接口,四种实现,六个函数。


01



一个接口


golang的context包定义了Context类型,根据官方文档的说法,该类型被设计用来在API边界之间以及过程之间传递截止时间、取消信号及其他与请求相关的数据。Context实际上是一个接口,提供了4个方法:   
 type Context interface {    Deadline() (deadline time.Time, ok bool)    Done() struct{}    Err() error    Value(key interface{}) interface{}}

02



四种实现



golang对Context接口的具体实现基于如下几种类型:
type emptyCtx inttype cancelCtx struct {    Context    mu       sync.Mutex            // 保护其他字段    done     chan struct{}         // 延迟创建,被第一个cancel close    children map[canceler]struct{} // 被第一个cancel设为nil    err      error                 // 被第一个cancel赋值(非nil)}type timerCtx struct {    cancelCtx    timer *time.Timer // 被cancelCtx.mu保护(并发保护)    deadline time.Time}type valueCtx struct {    Context    key, val interface{}}

03



六个函数



为了方便我们使用Context,context包还实现了一组函数:
func Background() Contextfunc TODO() Contextfunc 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 interface{}) Context

01 Background




var (  background = new(emptyCtx)  todo       = new(emptyCtx))func Background() Context {  return background}
ctx := context.Background()

7174bcdf4a5095fbb75d4217c0f9060f.png



02 TODO



func TODO() Context {  return todo}

03 WithCancel



func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func main() {    var wg sync.WaitGroup    ctx := context.Background()    ctx1, cancel = context.WithCancel(ctx)    wg.Add(1)    go func() {        defer wg.Done()        tick := time.NewTicker(300 * time.Millisecond)        for {            select {            case                 fmt.Println(ctx1.Err())                return            case t :=                 fmt.Println(t.Nanosecond())            }        }    }()    time.Sleep(time.Second)    cancel()    wg.Wait()}

4be4c55e1e87cf29664598c5cf1cfe0d.png



通过`ctx1.Done()`获取一个channel,监听这个channel可以获得`ctx1`的Cancel通知,还可以通过
`ctx.Err()`
获取
`ctx1`
被取消时写入的错误信息。

04 WithDeadline



func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func main() {    var wg sync.WaitGroup    ctx := context.Background()    ctx2, cancel := context.WithDeadline(ctx,time.Now().Add(time.Second))    defer cancel()    wg.Add(1)    go func() {        defer wg.Done()        tick := time.NewTicker(300 * time.Millisecond)        for {            select {            case                 fmt.Println(ctx2.Err())                return            case t :=                 fmt.Println(t.Nanosecond())            }        }    }()    wg.Wait()}

968f775ca6bb8c3628446e86ea09c9b2.png



如果WithDeadline函数接收到的是一个有截止时间的Context,那就要比较一下这两个截止时间了。例如下面再基于
ctx2
创建一个Context:
ctx3, cancel := context.WithDeadline(ctx2, time.Now().Add(time.Second*2))

3d88567c203e4798371ad783a646de16.png


如果
ctx3
要设定的截止时间比
ctx2
还晚,那么就没必要给它加定时器和截止时间了。只会用WithCancel函数基于
ctx2
构造一个cancelCtx,所以
ctx3
的动态类型是
*cancelCtx

50608d46703f65a262e3a435c592edef.png



上面的
ctx3
是基于
ctx2
创建的,而
ctx2
又是基于
ctx
创建的。这样层层包装的结果就是,这些Context会形成树状结构,子节点是基于父节点的包装,每个节点都可能有零个或多个子节点。

66099d5019891b38f456e90801ff82ab.png



对于可取消的Context而言,也就是实现了
context.canceler
接口的类型,还有一个关键点,就是它们会被注册到距离当前Context节点最近的、可取消的祖先节点中。注册位置就在cancelCtx结构体的
children
map中。而
*cancelCtx和
*timerCtx都实现了
canceler
接口,所以
ctx3
会被注册到
ctx2
中。

655ef27dced30a254fd99608bc9cc6f0.png



这样如果
ctx2
先取消,就可以根据
children
map这里的记录,取消自己子节点中所有可Cancel的Context。

d27f0125689b21d3a57288119005154c.png



05 WithTimeout



func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {    return WithDeadline(parent, time.Now().Add(timeout))}

06 WithValue


    
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}}

`WithValue`将传入的`parent` Context和`key`、`val`打包成一个新的`ctx`,再借助`valueCtx`的`Value`方法就可以通过Context传递数据了。使用Context传递数据要特别注意:


- Context是本着不可改变(immutable)的模式设计的,所以
不要试图修改Context里保存的数据

- 为了避免后续包装的键值对覆盖先前的值,最好不要直接使用
string

int
这类基础类型,而是用自定义类型包装一下。

下面我们来测试一下上述第二点:
package mainimport (    "context"    "fmt")var keyA string = "keyA"var keyC string = "keyA"func main() {    ctx := context.Background()      ctx1 := context.WithValue(ctx, keyA, "valueA")    fmt.Println("In ctx:")    fmt.Println("keyA => ", ctx1.Value(keyA))    ctx2 := context.WithValue(ctx1, keyC, "valueC")    fmt.Println("In ctx2:")    fmt.Println("keyA => ", ctx2.Value(keyA))    fmt.Println("keyC => ", ctx2.Value(keyC))        return}
In ctx:keyA =>  valueAIn ctx2:keyA =>  valueCkeyC =>  valueC

a28cd7e008934f9a602a05e1dc8a0193.png



再结合valueCtx.Value方法看一下键值对查找过程:
func (c *valueCtx) Value(key interface{}) interface{} {    if c.key == key {        return c.val    }    return c.Context.Value(key)}
type akeytype stringtype ckeytype stringvar keyA akeytype = "keyA"var keyC ckeytype = "keyA"......
In ctx:keyA =>  valueAIn ctx2:keyA =>  valueAkeyC =>  valueC

6f49581e5b8304a9d9759a49ddd4dfb3.png


WithCancel

WithDeadline

WithValue
函数,内部就是分别构造了
cancelCtx

timerCtx

valueCtx
这3种结构,最终所有的
ctx
将构成类似一棵树的结构,在根节点执行cancel就可以Cancel整个棵树,所以非常适合用来控制请求处理。
Go语言的http和sql包中都有Context的应用,这些待到“应用篇”再接着聊~


戳这里了解接口~

8ead62e34c75d92d101d97e21e6f205c.png


点个赞呗