什么是 Context
上下文 context.Context在Go 语言中用来设置截止日期、同步信号,传递请求相关值的结构体。
上下文与 Goroutine 有比较密切的关系,是 Go 语言中独特的设计,在其他编程语言中我们很少见到类似的概念。
主要用于超时控制和多Goroutine间的数据传递。
注:这里的数据传递主要指全局数据,如 链路追踪里的 traceId 之类的数据,并不是普通的参数传递(也非常不推荐用来传递参数)。
为什么需要 Context
WaitGroup 和信道(channel)是常见的 2 种并发控制的方式。
如果并发启动了多个子协程,需要等待所有的子协程完成任务,WaitGroup 非常适合于这类场景,例如下面的例子:
1. 利用全变量完成上下文控制
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
// 1. 利用全变量完成
var stop bool
func cpuInfo() {
defer wg.Done()
for {
if stop {
fmt.Println("退出cpu监控")
break
}
time.Sleep(time.Second*2)
fmt.Println("cpu信息读取完成")
}
}
func main() {
wg.Add(1)
go cpuInfo()
time.Sleep(time.Second*5)
// 5秒后不再监控
stop = true
wg.Wait()
fmt.Println("信息监控完成")
}
2. 利用 chan
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
// 2. 利用 chan
var stop2 chan bool = make(chan bool)
func cpuInfo2() {
defer wg.Done()
for {
select {
case <- stop2:
fmt.Println("退出cpu监控")
return
default:
// 上面没有就执行下面
time.Sleep(time.Second*2)
fmt.Println("cpu信息读取完成")
}
}
}
func main() {
wg.Add(1)
go cpuInfo2()
time.Sleep(time.Second*6)
// 5秒后不再监控
stop2 <- true
wg.Wait()
fmt.Println("信息监控完成")
}
更复杂的场景如何做并发控制呢?比如子协程中开启了新的子协程,或者需要同时控制多个子协程。
这种场景下,select+chan的方式就显得力不从心了。
Go 语言提供了 Context 标准库可以解决这类场景的问题,Context 的作用和它的名字很像,上下文,即子协程的下上文。Context 有两个主要的功能:
- 通知子协程退出(正常退出,超时退出等);
- 传递必要的参数。
context整体概览
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 |
使用
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wgp3 sync.WaitGroup
/**
为了解决 Context1 和 Context2中的问题 go 1.7 提供Context.WithCancel
使用上变得更加优雅
*/
func cpuInfo3(ctx context.Context) {
defer wgp3.Done()
// go memoryInfo3(ctx)
// ctx2, _ := context.WithCancel(context.Background()) // 一个新的ctx
ctx2, _ := context.WithCancel(ctx) // 父子关系 父退出,子也会退出 链式取消 web开发中常用
// 深层嵌套
go memoryInfo3(ctx2)
for {
select {
case <- ctx.Done():
fmt.Println("退出cpu监控")
return
default:
// 上面没有就执行下面
time.Sleep(time.Second*2)
fmt.Println("cpu信息读取完成")
}
}
}
func memoryInfo3(ctx context.Context) {
defer wgp3.Done()
for {
select {
case <- ctx.Done():
fmt.Println("退出内存监控")
return
default:
// 上面没有就执行下面
time.Sleep(time.Second*2)
fmt.Println("内存信息读取完成")
}
}
}
func main() {
wgp3.Add(2)
// 使用context.WithCancel(parent)函数,创建一个可取消的子Context
// 函数返回值有两个:子Context Cancel 取消函数
// context.Background() 返回一个空的Context
ctx, cancel := context.WithCancel(context.Background())
go cpuInfo3(ctx)
// go memoryInfo3(ctx)
time.Sleep(time.Second*6)
// 6秒后不再监控
cancel()
wgp3.Wait()
fmt.Println("信息监控完成")
}
使用准则
context 包一开始就告诉了我们应该怎么用,不应该怎么用,这是应该被共同遵守的约定。
- 不要把Context放在结构体中,要以参数的方式传递
- 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
- 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
- Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
- Context是线程安全的,可以放心的在多个goroutine中传递
使用场景
参考