“ 一个接口,四种实现,六个函数。”
01
—
一个接口
golang的context包定义了Context类型,根据官方文档的说法,该类型被设计用来在API边界之间以及过程之间传递截止时间、取消信号及其他与请求相关的数据。Context实际上是一个接口,提供了4个方法: type Context interface { Deadline() (deadline time.Time, ok bool) Done() struct{} Err() error Value(key interface{}) interface{}}
- Deadline 用来返回ctx的截止时间,ok为false表示没有设置。达到截止时间的ctx会被自动Cancel掉;- 如果当前ctx是可取消的, Done 返回一个chan用来监听,否则返回nil。当ctx被Cancel时,返回的chan会同时被close掉,也就变成“有信号”状态;- 如果ctx已经被canceled, Err 会返回执行Cancel时指定的error,否则返回nil;- Value 用来从ctx中根据指定的key提取对应的value;
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{}}
-
emptyCtx
类型只是实现了Context接口,几个方法都只是简单的返回nil、false等,实际上什么也不做。Background和TODO返回的Context,实际上都是emptyCtx;-
cancelCtx
类型可以说是最核心的一个类型,它实现了Cancel操作和信号机制,以及Context父子关系关联,从而支持在父Context Cancel时同步Cancel所有子Context;-
timerCtx
类型用来支持Deadline、Timeout,Cancel操作依赖于内置的cancelCtx;-
valueCtx
类型用来支持key、val打包,结合内置的Context字段,实际上构造了一个单链表节点;
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}
Background
函数会创建一个没有Deadline、没有Value,也不能被Cancel的emptyCtx。通常在一个请求的初始化阶段用Background()创建最顶层的根Context。
ctx := context.Background()
`ctx`是Context类型,也就是一个非空接口,它的结构如下图所示:
02 TODO
func TODO() Context { return todo}
TODO
函数和Background一样,也会创建一个emptyCtx,官方文档建议在本来应该使用外层传递的ctx,而外层却没有传递的地方使用,就像函数名的含义一样,留下一个TODO。
03 WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel
函数会基于传入的
parent
创建一个可以Cancel的
ctx
,与
cancel
函数一起返回。调用
cancel
函数就会将这个新的`ctx` Cancel掉,所有基于此
ctx
创建的子孙Context也会一并被Cancel掉。
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()}
上面的示例把`Background()`获取的`ctx`包装为可Cancel的`ctx1`,它的结构如下图所示:(示例中拆分成ctx和ctx1单纯为了图好画-_-!)
通过`ctx1.Done()`获取一个channel,监听这个channel可以获得`ctx1`的Cancel通知,还可以通过 `ctx.Err()` 获取 `ctx1` 被取消时写入的错误信息。
04 WithDeadline
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
WithDeadline
内部会创建一个可以Cancel的
ctx
,并且为它设置一个超时时间,然后与一个
cancel
函数一并返回。如果用户代码没有主动调用
cancel
函数,则
ctx
会在超时时间到达后自动Cancel。如果需要给任务设定一个截止时间,就可以用WithDeadline创建一个timerCtx。用wg来等待goroutine返回,若没有达到截止时间就完成了任务,`defer cancel()`也会把`ctx2`取消掉。
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()}
上面的示例中传递给WithDeadline函数的,是一个没有截止时间且不可取消的Context。WithDeadline内部会基于`ctx`构建一个`cancelCtx`,所以`ctx2`的动态类型是`*timerCtx`结构如下图所示:
如果WithDeadline函数接收到的是一个有截止时间的Context,那就要比较一下这两个截止时间了。例如下面再基于 ctx2 创建一个Context:
ctx3, cancel := context.WithDeadline(ctx2, time.Now().Add(time.Second*2))
如果
ctx3
要设定的截止时间早于
ctx2
,那么就会构造一个timerCtx,并且
timerCtx.cancelCtx
是基于
ctx2
构建的。
如果
ctx3
要设定的截止时间比
ctx2
还晚,那么就没必要给它加定时器和截止时间了。只会用WithCancel函数基于
ctx2
构造一个cancelCtx,所以
ctx3
的动态类型是
*cancelCtx
。
上面的 ctx3 是基于 ctx2 创建的,而 ctx2 又是基于 ctx 创建的。这样层层包装的结果就是,这些Context会形成树状结构,子节点是基于父节点的包装,每个节点都可能有零个或多个子节点。
对于可取消的Context而言,也就是实现了 context.canceler 接口的类型,还有一个关键点,就是它们会被注册到距离当前Context节点最近的、可取消的祖先节点中。注册位置就在cancelCtx结构体的 children map中。而 *cancelCtx和 *timerCtx都实现了 canceler 接口,所以 ctx3 会被注册到 ctx2 中。
这样如果 ctx2 先取消,就可以根据 children map这里的记录,取消自己子节点中所有可Cancel的Context。
05 WithTimeout
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout))}
`WithTimeout`只是`WithDeadline`的一个wrapper,接受一个`time.Duration`类型的参数,使用当前时间加上这个时间段来设置Deadline。
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}
运行结果如下,
ctx2
中用相等的key覆盖了
ctx1
中的值。
In ctx:keyA => valueAIn ctx2:keyA => valueCkeyC => valueC
我们先来看一下 ctx1 和 ctx2 的结构,valueCtx中 key 和 val 都是interface{}类型,空接口类型的结构是一个动态类型元数据指针,外加一个指向动态值的unsafe.Pointer。 再结合valueCtx.Value方法看一下键值对查找过程:
func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key)}
可以看到,通过`ctx2.Value("keyA")`查找val时,通过比较`key`,会直接锁定`ctx2`中保存的`val`,所以发生了Context间Value的覆盖。可以通过自定义key类型来避免这个问题。
type akeytype stringtype ckeytype stringvar keyA akeytype = "keyA"var keyC ckeytype = "keyA"......
执行结果如下:
In ctx:keyA => valueAIn ctx2:keyA => valueAkeyC => valueC
这一次通过
ctx2.Value(keyA)
查找val时,因为
ctx2
中存储的
key
与
`keyA`
类型不相符,所以会把查找请求委托给
ctx2
的parent,也就是
ctx1
。通过这样一级一级的向上查找,实际上形成了一个链表结构。
WithCancel
、
WithDeadline
和
WithValue
函数,内部就是分别构造了
cancelCtx
、
timerCtx
和
valueCtx
这3种结构,最终所有的
ctx
将构成类似一棵树的结构,在根节点执行cancel就可以Cancel整个棵树,所以非常适合用来控制请求处理。
Go语言的http和sql包中都有Context的应用,这些待到“应用篇”再接着聊~
戳这里了解接口~
点个赞呗