1. 什么是 Context?
在 Go 1.7 版本之前,context 还是非编制的,它存在于 golang.org/x/net/context 包中。
后来,Golang 团队发现 context 还挺好用的,就把 context 收编了,在 Go 1.7 版本正式纳入了标准库。
Context,也叫上下文,它的接口定义如下
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
可以看到 Context 接口共有 4 个方法
Deadline
:返回的第一个值是 截止时间,到了这个时间点,Context 会自动触发 Cancel 动作。返回的第二个值是 一个布尔值,true 表示设置了截止时间,false 表示没有设置截止时间,如果没有设置截止时间,就要手动调用 cancel 函数取消 Context。Done
:返回一个只读的通道(只有在被cancel后才会返回),类型为struct{}
。当这个通道可读时,意味着parent context已经发起了取消请求,根据这个信号,开发者就可以做一些清理动作,退出goroutine。Err
:返回 context 被 cancel 的原因。Value
:返回被绑定到 Context 的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。
2. 为何需要 Context?
当一个协程(goroutine)开启后,我们是无法强制关闭它的。
常见的关闭协程的原因有如下几种:
- goroutine 自己跑完结束退出
- 主进程crash退出,goroutine 被迫退出
- 通过通道发送信号,引导协程的关闭。
第一种,属于正常关闭,不在今天讨论范围之内。
第二种,属于异常关闭,应当优化代码。
第三种,才是开发者可以手动控制协程的方法,代码示例如下:
func main() { stop := make(chan bool) go func() { for { select { case <-stop: fmt.Println("监控退出,停止了...") return default: fmt.Println("goroutine监控中...") time.Sleep(2 * time.Second) } } }() time.Sleep(10 * time.Second) fmt.Println("可以了,通知监控停止") stop<- true //为了检测监控过是否停止,如果没有监控输出,就表示停止了 time.Sleep(5 * time.Second) }
例子中我们定义一个stop
的chan,通知他结束后台goroutine。实现也非常简单,在后台goroutine中,使用select判断stop
是否可以接收到值,如果可以接收到,就表示可以退出停止了;如果没有接收到,就会执行default
里的监控逻辑,继续监控,只到收到stop
的通知。
以上是一个 goroutine 的场景,如果是多个 goroutine ,每个goroutine 底下又开启了多个 goroutine 的场景呢?在 飞雪无情的博客 里关于为何要使用 Context,他是这么说的
chan+select的方式,是比较优雅的结束一个goroutine的方式,不过这种方式也有局限性,如果有很多goroutine都需要控制结束怎么办呢?如果这些goroutine又衍生了其他