Golang之context
当我们使用一些golang框架的时候,总能在框架中发现有个叫做context的东西。如果你之前了解过java的spring,那么你肯定也听说过其中有个牛逼的ApplicationContext。Context这个东西好像随时随地都在出现,在golang中也是非常重要的存在。今天我们就来看看这个神奇的Context。
定义
- 首先我们要知道什么是context?
很多人把它翻译成上下文,其实这个是一个很难描述很定义的东西,对于这种东西,我习惯用功能去定义它。
我的定义是:context是用于在多个goroutines之间传递信息的媒介。
官方定义:At Google, we developed a context package that makes it easy to pass request-scoped values, cancelation signals, and deadlines across API boundaries to all the goroutines involved in handling a request.
用法
同样的我们先来看看它的一些基本用法,大致了解它的使用。
传递信息
1 | func main() { |
其实传递消息很简单,只需要通过context.WithValue
方法设置,key-value然后通过ctx.Value
方法取值就可以了。
暂时不用关心context.Background()只要知道context有传递值的功能就可以了。
关闭goroutine
在我们写golang的时候goroutine是一个非常常用的东西,我们经常会开一个goroutine去处理对应的任务,特别是一些循环一直处理的情况,这些goroutine需要知道自己什么时候要停止。
我们常见的解决方案是使用一个channel去接收一个关闭的信号,收到信号之后关闭,或者说,需要一个标识符,每个goroutine去判断这个标识符的变更从而得知什么时候关闭。
那么用context如何实现呢?
1 | func main() { |
通过context.WithTimeout我们创建了一个3秒后自动取消的context;
所有工作goroutine监听ctx.Done()的信号;
收到信号就证明需要取消任务;
其实使用起来比较简单,让我们来看看内部的原理。
源码解析
创建
context.TODO()
这个就是创建一个占位用的context,可能在写程序的过程中还不能确定后期这个context的作用,所以暂时用这个占位
context.Background()
这个是最大的context,也就是根context,这里就有必要说一下context的整个构成了,context其实构成的是一棵树,Background为根节点,每次创建一个新的context就是创建了一个新的节点加入这棵树。
context.WithTimeout()
比如这个方法,创建一个自动过期的context
1 | // WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)). |
可以看到需要传入一个parent,和过期时间,新创建的context就是parent的子节点。
1 | func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { |
注意其中cancelCtx: newCancelCtx(parent),
其实是创建了一个可以取消的ctx,然后利用time.AfterFunc
来实现定时自动过期。
还有一个细节c.mu.Lock()
defer c.mu.Unlock()
这个mu来自:
1 | type cancelCtx struct { |
这个context因为有了锁,所以是并发安全的。
取消
1 | // cancel closes c.done, cancels each of c's children, and, if |
当达到过期时间或者调用cancelFunc的时候就会触发context的取消,然后看到上面的源码你就明白了,取消的时候有一个三个操作:
c.mu.Lock()
加锁保证安全close(c.done)
将done信道关闭,从而所有在观察done信道的goroutine都知道要关闭了for child := range c.children
循环每个子节点,关闭每个子节点。我们知道context的结构是树状的,所以同时我们要注意父节点如果关闭会关闭子节点的context。
WithValue和Value
1 | type valueCtx struct { |
首先valueCtx的结构如上所示,包含一个Context和key-val
1 | func WithValue(parent Context, key, val interface{}) Context { |
其实这个方法很简单,就是创建了一个parent的拷贝,并且将对应的key和val放进去。
1 | func (c *valueCtx) Value(key interface{}) interface{} { |
Value方法就更简单了,就是判断当前key是否匹配,如果不匹配就去子节点寻找。
案例
最后我们来看看在实际的使用过程中,我们在哪里使用到了context,我举两个实际中常用的框架gin和etcd
gin
gin是一个web框架,在web开发的时候非常实用。
1 | func main() { |
其实很多web框架都有Context,他们都自己封装了一个Context,利用这个Context可以做到一个request-scope中的参数传递和返回,还有很多操作通通都可以用Context来完成。
etcd
如果你没有了解过etcd你就可以把它想象成redis,它其实是一个分布式的k-v数据存储
我们在使用etcd进行操作(put或del等)的时候,需要传入context参数
1 | timeoutCtx, cancel := context.WithTimeout(context.Background(), 2 * time.Second) |
这里传入的context是一个超时自动取消的context,也就是说,当put操作超过两秒后还没有执行成功的话,context就会自动done,同时这个操作也将被取消。
因为我们在使用etcd的时候,如果当前网络出现异常,无法连接到节点,或者是节点数量不足的时候,都会出现操作被hang住,如果没有定时取消的机制,或者手动取消,那么当前goroutine会被一直占用。所以就利用context来完成这个操作。
总结
- context在web开发中,你可以类比java中的ThreadLocal,利用它来完成一个request-scope中参数的传递
- context可以用于多个goroutine之间的参数传递
- context还可以作为完成信号的通知
- context并发安全
其实,我们不仅要学到context的使用,还可以学到这样设计一个系统的优点,如果以后自己在设计一些框架和系统的时候可以有更多的想法。