context 有什么作用
Go 常用来写后台服务, 通常只需要几行代码, 就可以搭建一个 HTTP server。 在 Go 的 server 里,对每个 Request(请求)都会启动若干个 goroutine 同时工作:有些去内存查一些数据, 有些去数据库拿数据,有些调用第三方接口获取相关数据等,如图 7-1 所示。
这些 goroutine 需要共享请求的基本信息:例如登录的 token,处理请求的最大超时时间(如果超过此值再返回数据,请求方会因为超时接收不到)等。当请求被取消或是处理时间太长,这有可能是使用者关闭了浏览器或是已经超过了请求方规定的超时时间,请求方直接放弃了这次请求结果。这时,所有正在为这个请求工作的 goroutine 需要快速退出,因为它们的 “工作成果” 不再被需要了。在相关联的 goroutine 都退出后,系统就可以回收相关的资源。
Go 语言中的 server 实际上是一个 “协程模型”,处理一个请求需要多个协程。例如在业务的高峰期,某个下游服务的响应速度变慢,而当前系统的请求又没有超时控制,或者超时时间设置过大,那么等待下游服务返回数据的协程就会越来越多。而协程是要消耗系统资源的,后果就是协程数激增,内存占用飙涨,Go 调度器和 GC 不堪其重,甚至导致服务不可用。更严重的会导致雪崩效应,整个服务对外表现为不可用,这肯定是 P0 级别的事故。
其实前面描述的 P0 级别事故,通过设置 “允许下游最长处理时间” 就可以避免。例如,给下游设置的 timeout 是 50ms,如果超过这个值还没有接收到返回数据,就直接向客户端返回一个默认值或者错误。例如返回商品的一个默认库存数量。注意,这里设置的超时时间和创建一个 HTTP client 设置的读写超时时间不一样,后者表示一次 TCP 传输的时间, 而一次请求可能包含多次 TCP 传输,前者则表示所有传输的总时间。
而 context 包就是为了解决上面所说的这些问题而开发的:在一组 goroutine 之间传递共享的值、取消信号、deadline 等,如图 7-2 所示。
在 Go 里,不能直接杀死协程,协程的关闭一般采用 channel 和 select 的方式来控制。但是在某些场景下,例如处理一个请求衍生了很多协程,这些协程之间是相互关联的:需要共享一些全局变量、有共同的 deadline 等,而且可以同时被关闭。用 channel 和 select 就会比较麻烦,这时可以通过 context 来实现。
一句话:context 用来解决 goroutine 之间退出通知、元数据传递的功能的问题。 context 使用起来非常方便。
源码里对外提供了一个创建根节点 context 的函数:
// src/context/context.go
func Background() Context
2
3
Background 是一个空的 context,它不能被取消,没有值,也没有超时时间。
有了根节点 context,又提供了四个函数创建子节点 context:
// src/context/context.go
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
2
3
4
5
6
context 会在函数中间传递,只需要在适当的时间调用 Cancel 函数向 goroutines 发出取消信号或者调用 Value 函数取出 context 中的值。
在官方博客里,对于使用 context 提出了几点建议:
1)不要将 context 塞到结构体里。直接将 context 类型作为函数的第一参数,而且一般都命名为 ctx。
2)不要向函数传入一个含有 nil 属性的 context,如果实在不知道传什么,标准库准备好了一个 context:todo。
3)不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登录的 session、cookie 等。
4)同一个 context 可能会被传递到多个 goroutine,但别担心,context 是并发安全的。