Go 语言的 context 包短小精悍,很是适合新手学习。不管是它的源码仍是实际使用,都值得投入时间去学习。html
这篇文章依然想尝试全面、深刻地去研究。文章相比往期而言,总体不长,但愿你看完能够有所收获!git
Go 1.7 标准库引入 context,中文译做“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。github
context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。golang
随着 context 包的引入,标准库中不少接口所以加上了 context 参数,例如 database/sql 包。context 几乎成为了并发控制和超时控制的标准作法。web
context.Context 类型的值能够协调多个 groutine 中的代码执行“取消”操做,而且能够存储键值对。最重要的是它是并发安全的。与它协做的 API 均可以由外部控制执行“取消”操做,例如:取消一个 HTTP 请求的执行。sql
没看懂?不要紧,先日后看。shell
Go 经常使用来写后台服务,一般只须要几行代码,就能够搭建一个 http server。数据库
在 Go 的 server 里,一般每来一个请求都会启动若干个 goroutine 同时工做:有些去数据库拿数据,有些调用下游接口获取相关数据……segmentfault
这些 goroutine 须要共享这个请求的基本数据,例如登录的 token,处理请求的最大超时时间(若是超过此值再返回数据,请求方由于超时接收不到)等等。当请求被取消或是处理时间太长,这有多是使用者关闭了浏览器或是已经超过了请求方规定的超时时间,请求方直接放弃了此次请求结果。这时,全部正在为这个请求工做的 goroutine 须要快速退出,由于它们的“工做成果”再也不被须要了。在相关联的 goroutine 都退出后,系统就能够回收相关的资源。后端
再多说一点,Go 语言中的 server 其实是一个“协程模型”,也就是说一个协程处理一个请求。例如在业务的高峰期,某个下游服务的响应变慢,而当前系统的请求又没有超时控制,或者超时时间设置地过大,那么等待下游服务返回数据的协程就会愈来愈多。而咱们知道,协程是要消耗系统资源的,后果就是协程数激增,内存占用飙涨,甚至致使服务不可用。更严重的会致使雪崩效应,整个服务对外表现为不可用,这确定是 P0 级别的事故。这时,确定有人要背锅了。
其实前面描述的 P0 级别事故,经过设置“容许下游最长处理时间”就能够避免。例如,给下游设置的 timeout 是 50 ms,若是超过这个值尚未接收到返回数据,就直接向客户端返回一个默认值或者错误。例如,返回商品的一个默认库存数量。注意,这里设置的超时时间和建立一个 http client 设置的读写超时时间不同,这里不详细展开。能够去看看参考资料【Go 在今日头条的实践】
一文,有很精彩的论述。
context 包就是为了解决上面所说的这些问题而开发的:在 一组 goroutine 之间传递共享的值、取消信号、deadline……
用简练一些的话来讲,在Go 里,咱们不能直接杀死协程,协程的关闭通常会用 channel+select
方式来控制。可是在某些场景下,例如处理一个请求衍生了不少协程,这些协程之间是相互关联的:须要共享一些全局变量、有共同的 deadline 等,并且能够同时被关闭。再用 channel+select
就会比较麻烦,这时就能够经过 context 来实现。
一句话:context 用来解决 goroutine 之间退出通知
、元数据传递
的功能。
咱们分析的 Go 版本依然是 1.9.2
。
context 包的代码并不长,context.go
文件总共不到 500 行,其中还有不少大段的注释,代码可能也就 200 行左右的样子,是一个很是值得研究的代码库。
先给你们看一张总体的图:
类型 | 名称 | 做用 |
---|---|---|
Context | 接口 | 定义了 Context 接口的四个方法 |
emptyCtx | 结构体 | 实现了 Context 接口,它实际上是个空的 context |
CancelFunc | 函数 | 取消函数 |
canceler | 接口 | context 取消接口,定义了两个方法 |
cancelCtx | 结构体 | 能够被取消 |
timerCtx | 结构体 | 超时会被取消 |
valueCtx | 结构体 | 能够存储 k-v 对 |
Background | 函数 | 返回一个空的 context,常做为根 context |
TODO | 函数 | 返回一个空的 context,经常使用于重构时期,没有合适的 context 可用 |
WithCancel | 函数 | 基于父 context,生成一个能够取消的 context |
newCancelCtx | 函数 | 建立一个可取消的 context |
propagateCancel | 函数 | 向下传递 context 节点间的取消关系 |
parentCancelCtx | 函数 | 找到第一个可取消的父节点 |
removeChild | 函数 | 去掉父节点的孩子节点 |
init | 函数 | 包初始化 |
WithDeadline | 函数 | 建立一个有 deadline 的 context |
WithTimeout | 函数 | 建立一个有 timeout 的 context |
WithValue | 函数 | 建立一个存储 k-v 对的 context |
上面这张表展现了 context 的全部函数、接口、结构体,能够纵览全局,能够在读完文章后,再回头细看。
总体类图以下:
如今能够直接看源码:
type Context interface { // 当 context 被取消或者到了 deadline,返回一个被关闭的 channel Done() <-chan struct{} // 在 channel Done 关闭后,返回 context 取消缘由 Err() error // 返回 context 是否会被取消以及自动取消时间(即 deadline) Deadline() (deadline time.Time, ok bool) // 获取 key 对应的 value Value(key interface{}) interface{} }
Context
是一个接口,定义了 4 个方法,它们都是幂等
的。也就是说连续屡次调用同一个方法,获得的结果都是相同的。
Done()
返回一个 channel,能够表示 context 被取消的信号:当这个 channel 被关闭时,说明 context 被取消了。注意,这是一个只读的channel。 咱们又知道,读一个关闭的 channel 会读出相应类型的零值。而且源码里没有地方会向这个 channel 里面塞入值。换句话说,这是一个 receive-only
的 channel。所以在子协程里读这个 channel,除非被关闭,不然读不出来任何东西。也正是利用了这一点,子协程从 channel 里读出了值(零值)后,就能够作一些收尾工做,尽快退出。
Err()
返回一个错误,表示 channel 被关闭的缘由。例如是被取消,仍是超时。
Deadline()
返回 context 的截止时间,经过此时间,函数就能够决定是否进行接下来的操做,若是时间过短,就能够不往下作了,不然浪费系统资源。固然,也能够用这个 deadline 来设置一个 I/O 操做的超时时间。
Value()
获取以前设置的 key 对应的 value。
再来看另一个接口:
type canceler interface { cancel(removeFromParent bool, err error) Done() <-chan struct{} }
实现了上面定义的两个方法的 Context,就代表该 Context 是可取消的。源码中有两个类型实现了 canceler 接口:*cancelCtx
和 *timerCtx
。注意是加了 *
号的,是这两个结构体的指针实现了 canceler 接口。
Context 接口设计成这个样子的缘由:
caller 不该该去关心、干涉 callee 的状况,决定如何以及什么时候 return 是 callee 的责任。caller 只需发送“取消”信息,callee 根据收到的信息来作进一步的决策,所以接口并无定义 cancel 方法。
“取消”某个函数时,和它相关联的其余函数也应该“取消”。所以,Done()
方法返回一个只读的 channel,全部相关函数监听此 channel。一旦 channel 关闭,经过 channel 的“广播机制”,全部监听者都能收到。
源码中定义了 Context
接口后,而且给出了一个实现:
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 }
看这段源码,很是 happy。由于每一个函数都实现的异常简单,要么是直接返回,要么是返回 nil。
因此,这其实是一个空的 context,永远不会被 cancel,没有存储值,也没有 deadline。
它被包装成:
var ( background = new(emptyCtx) todo = new(emptyCtx) )
经过下面两个导出的函数(首字母大写)对外公开:
func Background() Context { return background } func TODO() Context { return todo }
background 一般用在 main 函数中,做为全部 context 的根节点。
todo 一般用在并不知道传递什么 context的情形。例如,调用一个须要传递 context 参数的函数,你手头并无其余 context 能够传递,这时就能够传递 todo。这经常发生在重构进行中,给一些函数添加了一个 Context 参数,但不知道要传什么,就用 todo “占个位子”,最终要换成其余 context。
再来看一个重要的 context:
type cancelCtx struct { Context // 保护以后的字段 mu sync.Mutex done chan struct{} children map[canceler]struct{} err error }
这是一个能够取消的 Context,实现了 canceler 接口。它直接将接口 Context 做为它的一个匿名字段,这样,它就能够被当作一个 Context。
先来看 Done()
方法的实现:
func (c *cancelCtx) Done() <-chan struct{} { c.mu.Lock() if c.done == nil { c.done = make(chan struct{}) } d := c.done c.mu.Unlock() return d }
c.done 是“懒汉式”建立,只有调用了 Done() 方法的时候才会被建立。再次说明,函数返回的是一个只读的 channel,并且没有地方向这个 channel 里面写数据。因此,直接调用读这个 channel,协程会被 block 住。通常经过搭配 select 来使用。一旦关闭,就会当即读出零值。
Err()
和 String()
方法比较简单,很少说。推荐看源码,很是简单。
接下来,咱们重点关注 cancel()
方法的实现:
func (c *cancelCtx) cancel(removeFromParent bool, err error) { // 必需要传 err if err == nil { panic("context: internal error: missing cancel error") } c.mu.Lock() if c.err != nil { c.mu.Unlock() return // 已经被其余协程取消 } // 给 err 字段赋值 c.err = err // 关闭 channel,通知其余协程 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) } }
整体来看,cancel()
方法的功能就是关闭 channel:c.done;递归地取消它的全部子节点;从父节点从删除本身。达到的效果是经过关闭 channel,将取消信号传递给了它的全部子节点。goroutine 接收到取消信号的方式就是 select 语句中的读 c.done
被选中。
咱们再来看建立一个可取消的 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} }
这是一个暴露给用户的方法,传入一个父 Context(这一般是一个 background
,做为根节点),返回新建的 context,新 context 的 done channel 是新建的(前文讲过)。
当 WithCancel 函数返回的 CancelFunc 被调用或者是父节点的 done channel 被关闭(父节点的 CancelFunc 被调用),此 context(子节点) 的 done channel 也会被关闭。
注意传给 WithCancel 方法的参数,前者是 true,也就是说取消的时候,须要将本身从父节点里删除。第二个参数则是一个固定的取消错误类型:
var Canceled = errors.New("context canceled")
还注意到一点,调用子节点 cancel 方法的时候,传入的第一个参数 removeFromParent
是 false。
两个问题须要回答:1. 何时会传 true?2. 为何有时传 true,有时传 false?
当 removeFromParent
为 true 时,会将当前节点的 context 从父节点 context 中删除:
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() }
最关键的一行:
delete(p.children, child)
何时会传 true 呢?答案是调用 WithCancel()
方法的时候,也就是新建立一个可取消的 context 节点时,返回的 cancelFunc 函数会传入 true。这样作的结果是:当调用返回的 cancelFunc 时,会将这个 context 从它的父节点里“除名”,由于父节点可能有不少子节点,你本身取消了,因此我要和你断绝关系,对其余人没影响。
在取消函数内部,我知道,我全部的子节点都会由于个人一:c.children = nil
而化为灰烬。我天然就没有必要再多作这一步,最后我全部的子节点都会和我断绝关系,不必一个个作。另外,若是遍历子节点的时候,调用 child.cancel 函数传了 true,还会形成同时遍历和删除一个 map 的境地,会有问题的。
如上左图,表明一棵 context 树。当调用左图中标红 context 的 cancel 方法后,该 context 从它的父 context 中去除掉了:实线箭头变成了虚线。且虚线圈框出来的 context 都被取消了,圈内的 context 间的父子关系都荡然无存了。
重点看 propagateCancel()
:
func propagateCancel(parent Context, child canceler) { // 父节点是个空节点 if parent.Done() == nil { return // parent is never canceled } // 找到能够取消的父 context if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() if p.err != nil { // 父节点已经被取消了,本节点(子节点)也要取消 child.cancel(false, p.err) } else { // 父节点未取消 if p.children == nil { p.children = make(map[canceler]struct{}) } // "挂到"父节点上 p.children[child] = struct{}{} } p.mu.Unlock() } else { // 若是没有找到可取消的父 context。新启动一个协程监控父节点或子节点取消信号 go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): } }() } }
这个方法的做用就是向上寻找能够“挂靠”的“可取消”的 context,而且“挂靠”上去。这样,调用上层 cancel 方法的时候,就能够层层传递,将那些挂靠的子 context 同时“取消”。
这里着重解释下为何会有 else 描述的状况发生。else
是指当前节点 context 没有向上找到能够取消的父节点,那么就要再启动一个协程监控父节点或者子节点的取消动做。
这里就有疑问了,既然没找到能够取消的父节点,那 case <-parent.Done()
这个 case 就永远不会发生,因此能够忽略这个 case;而 case <-child.Done()
这个 case 又啥事不干。那这个 else
不就多余了吗?
其实否则。咱们来看 parentCancelCtx
的代码:
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 } } }
这里只会识别三种 Context 类型:cancelCtx,timerCtx,*valueCtx。如果把 Context 内嵌到一个类型里,就识别不出来了。
因为 context 包的代码并很少,因此我直接把它 copy 出来了,而后在 else 语句里加上了几条打印语句,来验证上面的说法:
type MyContext struct { // 这里的 Context 是我 copy 出来的,因此前面不用加 context. Context } func main() { childCancel := true parentCtx, parentFunc := WithCancel(Background()) mctx := MyContext{parentCtx} childCtx, childFun := WithCancel(mctx) if childCancel { childFun() } else { parentFunc() } fmt.Println(parentCtx) fmt.Println(mctx) fmt.Println(childCtx) // 防止主协程退出太快,子协程来不及打印 time.Sleep(10 * time.Second) }
我自已在 else 里添加的打印语句我就不贴出来了,感兴趣的能够本身动手实验下。咱们看下三个 context 的打印结果:
context.Background.WithCancel {context.Background.WithCancel} {context.Background.WithCancel}.WithCancel
果真,mctx,childCtx 和正常的 parentCtx 不同,由于它是一个自定义的结构体类型。
else
这段代码说明,若是把 ctx 强行塞进一个结构体,并用它做为父节点,调用 WithCancel 函数构建子节点 context 的时候,Go 会新启动一个协程来监控取消信号,明显有点浪费嘛。
再来讲一下,select 语句里的两个 case 其实都不能删。
select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): }
第一个 case 说明当父节点取消,则取消子节点。若是去掉这个 case,那么父节点取消的信号就不能传递到子节点。
第二个 case 是说若是子节点本身取消了,那就退出这个 select,父节点的取消信号就不用管了。若是去掉这个 case,那么极可能父节点一直不取消,这个 goroutine 就泄漏了。固然,若是父节点取消了,就会重复让子节点取消,不过,这也没什么影响嘛。
timerCtx 基于 cancelCtx,只是多了一个 time.Timer 和一个 deadline。Timer 会在 deadline 到来时,自动取消 context。
type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time }
timerCtx 首先是一个 cancelCtx,因此它能取消。看下 cancel() 方法:
func (c *timerCtx) cancel(removeFromParent bool, err error) { // 直接调用 cancelCtx 的取消方法 c.cancelCtx.cancel(false, err) if removeFromParent { // 从父节点中删除子节点 removeChild(c.cancelCtx.Context, c) } c.mu.Lock() if c.timer != nil { // 关掉定时器,这样,在deadline 到来时,不会再次取消 c.timer.Stop() c.timer = nil } c.mu.Unlock() }
建立 timerCtx 的方法:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) }
WithTimeout
函数直接调用了 WithDeadline
,传入的 deadline 是当前时间加上 timeout 的时间,也就是从如今开始再通过 timeout 时间就算超时。也就是说,WithDeadline
须要用的是绝对时间。重点来看它:
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) { if cur, ok := parent.Deadline(); ok && cur.Before(deadline) { // 若是父节点 context 的 deadline 早于指定时间。直接构建一个可取消的 context。 // 缘由是一旦父节点超时,自动调用 cancel 函数,子节点也会随之取消。 // 因此不用单独处理子节点的计时器时间到了以后,自动调用 cancel 函数 return WithCancel(parent) } // 构建 timerCtx c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: deadline, } // 挂靠到父节点上 propagateCancel(parent, c) // 计算当前距离 deadline 的时间 d := time.Until(deadline) 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 { // d 时间后,timer 会自动调用 cancel 函数。自动取消 c.timer = time.AfterFunc(d, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) } }
也就是说仍然要把子节点挂靠到父节点,一旦父节点取消了,会把取消信号向下传递到子节点,子节点随之取消。
有一个特殊状况是,若是要建立的这个子节点的 deadline 比父节点要晚,也就是说若是父节点是时间到自动取消,那么必定会取消这个子节点,致使子节点的 deadline 根本不起做用,由于子节点在 deadline 到来以前就已经被父节点取消了。
这个函数的最核心的一句是:
c.timer = time.AfterFunc(d, func() { c.cancel(true, DeadlineExceeded) })
c.timer 会在 d 时间间隔后,自动调用 cancel 函数,而且传入的错误就是 DeadlineExceeded
:
var DeadlineExceeded error = deadlineExceededError{} type deadlineExceededError struct{} func (deadlineExceededError) Error() string { return "context deadline exceeded" }
也就是超时错误。
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 做为匿名字段,所以仅管它只实现了 2 个方法,其余方法继承自父 context。但它仍然是一个 Context,这是 Go 语言的一个特色。
建立 valueCtx 的函数:
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} }
对 key 的要求是可比较,由于以后须要经过 key 取出 context 中的值,可比较是必须的。
经过层层传递 context,最终造成这样一棵树:
和链表有点像,只是它的方向相反:Context 指向它的父节点,链表则指向下一个节点。经过 WithValue 函数,能够建立层层的 valueCtx,存储 goroutine 间能够共享的变量。
取值的过程,其实是一个递归查找的过程:
func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key) }
它会顺着链路一直往上找,比较当前节点的 key
是不是要找的 key,若是是,则直接返回 value。不然,一直顺着 context 往前,最终找到根节点(通常是 emptyCtx),直接返回一个 nil。因此用 Value 方法的时候要判断结果是否为 nil。
由于查找方向是往上走的,因此,父节点无法获取子节点存储的值,子节点却能够获取父节点的值。
WithValue
建立 context 节点的过程实际上就是建立链表节点的过程。两个节点的 key 值是能够相等的,但它们是两个不一样的 context 节点。查找的时候,会向上查找到最后一个挂载的 context 节点,也就是离得比较近的一个父节点 context。因此,总体上而言,用 WithValue
构造的实际上是一个低效率的链表。
若是你接手过项目,确定经历过这样的窘境:在一个处理过程当中,有若干子函数、子协程。各类不一样的地方会向 context 里塞入各类不一样的 k-v 对,最后在某个地方使用。
你根本就不知道何时什么地方传了什么值?这些值会不会被“覆盖”(底层是两个不一样的 context 节点,查找的时候,只会返回一个结果)?你确定会崩溃的。
而这也是 context.Value
最受争议的地方。不少人建议尽可能不要经过 context 传值。
context 使用起来很是方便。源码里对外提供了一个建立根节点 context 的函数:
func Background() Context
background 是一个空的 context, 它不能被取消,没有值,也没有超时时间。
有了根节点 context,又提供了四个函数建立子节点 context:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithValue(parent Context, key, val interface{}) Context
context 会在函数传递间传递。只须要在适当的时间调用 cancel 函数向 goroutines 发出取消信号或者调用 Value 函数取出 context 中的值。
在官方博客里,对于使用 context 提出了几点建议:
我翻译一下:
对于 Web 服务端开发,每每但愿将一个请求处理的整个过程串起来,这就很是依赖于 Thread Local(对于 Go 可理解为单个协程所独有) 的变量,而在 Go 语言中并无这个概念,所以须要在函数调用的时候传递 context。
package main import ( "context" "fmt" ) func main() { ctx := context.Background() process(ctx) ctx = context.WithValue(ctx, "traceId", "qcrao-2019") process(ctx) } func process(ctx context.Context) { traceId, ok := ctx.Value("traceId").(string) if ok { fmt.Printf("process over. trace_id=%s\n", traceId) } else { fmt.Printf("process over. no trace_id\n") } }
运行结果:
process over. no trace_id process over. trace_id=qcrao-2019
第一次调用 process 函数时,ctx 是一个空的 context,天然取不出来 traceId。第二次,经过 WithValue
函数建立了一个 context,并赋上了 traceId
这个 key,天然就能取出来传入的 value 值。
固然,现实场景中多是从一个 HTTP 请求中获取到的 Request-ID。因此,下面这个样例可能更适合:
const requestIDKey int = 0 func WithRequestID(next http.Handler) http.Handler { return http.HandlerFunc( func(rw http.ResponseWriter, req *http.Request) { // 从 header 中提取 request-id reqID := req.Header.Get("X-Request-ID") // 建立 valueCtx。使用自定义的类型,不容易冲突 ctx := context.WithValue( req.Context(), requestIDKey, reqID) // 建立新的请求 req = req.WithContext(ctx) // 调用 HTTP 处理函数 next.ServeHTTP(rw, req) } ) } // 获取 request-id func GetRequestID(ctx context.Context) string { ctx.Value(requestIDKey).(string) } func Handle(rw http.ResponseWriter, req *http.Request) { // 拿到 reqId,后面能够记录日志等等 reqID := GetRequestID(req.Context()) ... } func main() { handler := WithRequestID(http.HandlerFunc(Handle)) http.ListenAndServe("/", handler) }
咱们先来设想一个场景:打开外卖的订单页,地图上显示外卖小哥的位置,并且是每秒更新 1 次。app 端向后台发起 websocket 链接(现实中多是轮询)请求后,后台启动一个协程,每隔 1 秒计算 1 次小哥的位置,并发送给端。若是用户退出此页面,则后台须要“取消”此过程,退出 goroutine,系统回收资源。
后端可能的实现以下:
func Perform() { for { calculatePos() sendResult() time.Sleep(time.Second) } }
若是须要实现“取消”功能,而且在不了解 context 功能的前提下,可能会这样作:给函数增长一个指针型的 bool 变量,在 for 语句的开始处判断 bool 变量是发由 true 变为 false,若是改变,则退出循环。
上面给出的简单作法,能够实现想要的效果,没有问题,可是并不优雅,而且一旦协程数量多了以后,而且各类嵌套,就会很麻烦。优雅的作法,天然就要用到 context。
func Perform(ctx context.Context) { for { calculatePos() sendResult() select { case <-ctx.Done(): // 被取消,直接返回 return case <-time.After(time.Second): // block 1 秒钟 } } }
主流程多是这样的:
ctx, cancel := context.WithTimeout(context.Background(), time.Hour) go Perform(ctx) // …… // app 端返回页面,调用cancel 函数 cancel()
注意一个细节,WithTimeOut 函数返回的 context 和 cancelFun 是分开的。context 自己并无取消函数,这样作的缘由是取消函数只能由外层函数调用,防止子节点 context 调用取消函数,从而严格控制信息的流向:由父节点 context 流向子节点 context。
前面那个例子里,goroutine 仍是会本身执行完,最后返回,只不过会多浪费一些系统资源。这里改编一个“若是不用 context 取消,goroutine 就会泄漏的例子”,来自参考资料:【避免协程泄漏】
。
func gen() <-chan int { ch := make(chan int) go func() { var n int for { ch <- n n++ time.Sleep(time.Second) } }() return ch }
这是一个能够生成无限整数的协程,但若是我只须要它产生的前 5 个数,那么就会发生 goroutine 泄漏:
func main() { for n := range gen() { fmt.Println(n) if n == 5 { break } } // …… }
当 n == 5 的时候,直接 break 掉。那么 gen 函数的协程就会执行无限循环,永远不会停下来。发生了 goroutine 泄漏。
用 context 改进这个例子:
func gen(ctx context.Context) <-chan int { ch := make(chan int) go func() { var n int for { select { case <-ctx.Done(): return case ch <- n: n++ time.Sleep(time.Second) } } }() return ch } func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // 避免其余地方忘记 cancel,且重复调用不影响 for n := range gen(ctx) { fmt.Println(n) if n == 5 { cancel() break } } // …… }
增长一个 context,在 break 前调用 cancel 函数,取消 goroutine。gen 函数在接收到取消信号后,直接退出,系统回收资源。
读彻底文,你必定有这种感受:context 就是为 server 而设计的。说什么处理一个请求,须要启动多个 goroutine 并行地去处理,而且在这些 goroutine 之间还要传递一些共享的数据等等,这些都是写一个 server 要作的事。
没错,Go 很适合写 server,但它终归是一门通用的语言。你在用 Go 作 Leetcode 上面的题目的时候,确定不会认为它和通常的语言有什么差异。因此,不少特性好很差,应该从 Go 只是一门普通的语言,很擅长写 server
的角度来看。
从这个角度来看,context 并无那么美好。Go 官方建议咱们把 Context 做为函数的第一个参数,甚至连名字都准备好了。这形成一个后果:由于咱们想控制全部的协程的取消动做,因此须要在几乎全部的函数里加上一个 Context 参数。很快,咱们的代码里,context 将像病毒同样扩散的处处都是。
在参考资料【Go2 应该去掉 context】
这篇英文博客里,做者甚至调侃说:若是要把 Go 标准库的大部分函数都加上 context 参数的话,例以下面这样:
n, err := r.Read(context.TODO(), p)
就给我来一枪吧!
原文是这样说的:put a bullet in my head, please.
我当时看到这句话的时候,会心一笑。这可能就是陶渊明说的:每有会意,便欣然忘食。固然,我是在晚饭会看到这句话的。
为了表达本身对 context 并无什么好感,做者接着又说了一句:If you use ctx.Value in my (non-existent) company, you’re fired. 简直太幽默了,哈哈。
另外,像 WithCancel
、WithDeadline
、WithTimeout
、WithValue
这些建立函数,其实是建立了一个个的链表结点而已。咱们知道,对链表的操做,一般都是 O(n)
复杂度的,效率不高。
那么,context 包到底解决了什么问题呢?答案是:cancelation
。仅管它并不完美,但它确实很简洁地解决了问题。
到这里,整个 context 包的内容就所有讲完了。源码很是短,很适合学习,必定要去读一下。
context 包是 Go 1.7 引入的标准库,主要用于在 goroutine 之间传递取消信号、超时时间、截止时间以及一些共享的值等。它并非太完美,但几乎成了并发控制和超时控制的标准作法。
使用上,先建立一个根节点的 context,以后根据库提供的四个函数建立相应功能的子节点 context。因为它是并发安全的,因此能够放心地传递。
当使用 context 做为函数参数时,直接把它放在第一个参数的位置,而且命名为 ctx。另外,不要把 context 嵌套在自定义的类型里。
最后,你们下次在看到代码里有用到 context 的,观察下是怎么使用的,确定逃不出咱们讲的几种类型。熟悉以后会发现:context 可能并不完美,但它确实简洁高效地解决了问题。
【context 官方博客】https://blog.golang.org/context
【今日头条构建Go的实践】https://zhuanlan.zhihu.com/p/...
【飞雪无情的博客】https://www.flysnow.org/2017/...
【context 源码】https://juejin.im/post/5a6873...
【腾讯云源码阅读】https://cloud.tencent.com/dev...
【更宏观地一些思考,english】https://siadat.github.io/post...
【避免协程泄漏】https://rakyll.org/leakingctx/
【应用分类】https://dreamerjonson.com/201...
【官方文档示例翻译版】https://brantou.github.io/201...
【例子,english】http://p.agnihotry.com/post/u...
【Go2 应该去掉 context】https://faiface.github.io/pos...
【源码,比较详细】https://juejin.im/post/5c1514...
【Golang Context 是好的设计吗?】https://segmentfault.com/a/11...
【今日头条的 Go 实践】https://36kr.com/p/5073181
【实例】https://zhuanlan.zhihu.com/p/...