在 Go 的世界里,写并发程序少不了一些控制工具。提到控制,许多人马上会想到锁、通道这些常见方法。但今天聊的 context,虽说简单,水却挺深。如果把并发控制比作江湖,锁和通道像是功夫里的外家拳,而 context 更像是内功心法,潜移默化之间就把复杂的局面打理得井井有条。
先聊聊 context 的出身。context 的诞生是为了解决并发程序中数据传递和取消操作等问题。开发服务时,尤其像处理 HTTP 请求这样的场景,线程或者 goroutine 一多,手动管理这些上下文成了麻烦事。context 正式在 Go 1.7 的时候登堂入室,入了标准库。
context 的主要结构
context 的核心接口有四个方法:
- Deadline
- Done
- Err
- Value
结构不复杂,代码看起来清晰易懂。顺着思路走下去,源码中 context
包分为几大类:emptyCtx,cancelCtx,timerCtx,和 valueCtx。
emptyCtx 是最简单的上下文,表示“什么都没做的上下文”。你在初始化、或者不关心上下文信息时,得到的往往就是这种。BackGround 和 TODO 这两个预定义的实例,只起到占位作用。大部分人经常直接用,甚少去纠结。
稍复杂一点的是 cancelCtx。它是用来干啥的呢?实际应用时,如果一个请求进来,很可能后台会启动一堆 goroutine 来处理各个子任务。但假若主任务突然被取消了,这些子任务还在忙活岂不是浪费资源?于是 cancelCtx 有用武之地了。当你调用它的取消函数时,所有的子任务也会收到“打烊下班”的通知。
看看它的代码结构。一个 cancelCtx 除了基础的 context 接口实现外,还有一个done 通道,专门用来发取消信号。调用取消函数时,其实就是往这个通道塞了个信号,使相关任务得以及时退出。光说没感觉,你看这个逻辑是不是简约而不简单?
再来看 timerCtx。它其实是 cancelCtx 的亲戚。有时候,你不但想要主动取消任务,还想根据时间控制。遇到这种情况,timerCtx 则挑起大梁。它不仅能做取消任务的事,还能根据设定的时间点“定时”取消。Deadline 这个方法说白了,就是提供获取这个设定时间的功能。timerCtx 里面的内核数据结构不算难,底层依赖 timer 定时器跟通道结合,调度恰到好处。
valueCtx 又有所不同。它的作用更多是在上下文之间传递数据。可以看到,它在层层的 goroutine 之间穿针引线,传递一些“小纸条”。明白了前面几种的原理,这最后一个就很简单了,是一个数据挂载功能。注意,值在 context 链路上传递时并不会复制,也就是说这些数据是在 goroutine 之间共享的,因此你不能啥都往里面塞。准确使用 valueCtx 可以避免过多显式参数传递,显得整洁有序。
回过头想,每次写代码时通过巧妙地使用 context,能规避掉大半并发下的管理难题,风格也是行云流水一般。甚至很多 Go 项目中大量逻辑代码看似独立,却又相互影响时,都是 context 在背后疏通调节。
这工具设计出来显然不仅仅是为了管理 cancelling 和 deadlines。关键在于通过它能把相关逻辑巧妙的变为更合适的关联,有机的搭配使得程序健壮又高效。几个方法虽然用起来大多不过一两行,实际上内容分布精致,整体不可小觑。了解 context,做事一步到位,少走不少冤枉路。岂不快哉?
整体体会,context 的精巧之处就像那条古话——“小隐隐于野,大隐隐于市”。想要时能拿来迅速救急,隐藏在那里的原理又能扛住长期、复杂任务打磨。如果能善加利用,程序健壮些许不成问题。