Golang Context 上下文
1. Context 基本介绍
Go 1.7 标准库引入context,译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。
随着 context 包的引入,标准库中很多接口因此加上了 context 参数,例如database/sql
包。context 几乎成为了并发控制和超时控制的标准做法
作用:
在一组 goroutine 之间传递共享的值、取消信号、deadline
2. Context 核心结构
context.Context
是 Go 语言在 1.7 版本中引入标准库的接口,该接口定义了四个需要实现的方法:
type Context interface {
// 返回被取消的时间
Deadline() (deadline time.Time, ok bool)
// 返回用于通知Context完结的channel
// 当这个 channel 被关闭时,说明 context 被取消了
// 在子协程里读这个 channel,除非被关闭,否则读不出来任何东西
Done() <-chan struct{}
// 返回Context取消的错误
Err() error
// 返回key对应的value
Value(key any) any
}
Done() : 会关闭传递信号的 channel , 当 channel 被关闭了 , 就可以接收到数据了 ( context 被取消了 )
value() : 通过key:value 来传递context中需要共享的值
- 除了 Context 接口,还存在一个 canceler 接口,用于实现 Context 可以被取消
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
可以取消的上下文都应该实现 canceler 接口
- 除了以上两个接口,还有4个预定义的Context类型:
// 空Context
type emptyCtx int
// 取消Context
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
// 定时取消Context
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
// KV值Context
type valueCtx struct {
Context
key, val any
}
3. 默认 Context 使用
context 包中最常用的方法是context.Background
、context.TODO
,这两个方法都会返回预先初始化好的私有变量 background 和 todo,它们会在同一个 Go 程序中被复用:
-
context.Background
, 是上下文的默认值,所有其他的上下文都应该从它衍生出来,在多数情况下,如果当前函数没有上下文作为入参,我们都会使用context.Background
作为起始的上下文向下传递。 -
context.TODO
,是一个备用,一个 context 占位,通常用在并不知道传递什么 context的情形。
底层都是返回一个空context
// 创建方法
func Background() Context {
return background
}
func TODO() Context {
return todo
}
// 预定义变量
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// emptyCtx 定义
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 any) any {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
作用:
有些函数或者方法需要 context , 但是自己没有 context , 这时候就可以定义空 context
database/sql
包中的某些函数
func (db *DB) PingContext(ctx context.Context) error
func (db *DB) ExecContext(ctx context.Context, query string, args ...any) (Result, error)
func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
func (db *DB) QueryRowContext(ctx context.Context, query string, args ...any) *Row
- 具体使用
db, _ := sql.Open("", "")
query := "DELETE FROM `table_name` WHERE `id` = ?"
db.ExecContext(context.Background(), query, 42)
4. Contex 传递取消信号
4.1 主动取消
需要的操作为:
-
创建带有cancel函数的 Context,
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
-
接收 cancel 的
Channel,ctx.Done()
-
主动 Cancel 的函数,
cancel CancelFunc
具有同一个 context 对象的 goroutine , 调用主动 Cancel 取消函数后 ,
ctx.Done()
就可以取出数据
每一个 goroutine 都会监听c.Done()
这个 channel , 当监听到之后就会执行用户自己定义的操作 , 比如return 退出所有goroutine
取消这个操作不是由 context 执行的 , context 只负责去传递这个信号 , 有了这个信号 , 就可以基于这个信号做后续操作
func ContextCancel() {
// 一:创建带有cancel函数的context
ctx, cancel := context.WithCancel(context.Background())
// 二: 启动goroutine,携带cancelCtx
wg := sync.WaitGroup{}
wg.Add(4)
for i := 0; i < 4; i++ {
go func(c context.Context, n int) {
defer wg.Done()
// 监听context的取消完成channel,来确定是否执行主动cancel操作
fmt.Println("第", n, "个Goroutine")
for {
select {
// 等待接收c.Done()这个channel
case <-c.Done():
fmt.Println("第", n, "个 Goroutine ", "context cancel")
return
default:
}
time.Sleep(300 * time.Millisecond)
}
}(ctx, i)
}
// 三: 定时取消cancel()
// 定时器,三秒后取消所有goroutine的执行
select {
case <-time.NewTimer(3 * time.Second).C:
fmt.Println("3秒时间到")
cancel()
}
// 也可以使用select解决goroutine结束无法打印contex cancel问题
select {
case <-ctx.Done():
fmt.Println("main context cancel")
}
wg.Wait()
}
4.2 Deadline 和 Timeout 定时取消
context.WithTimeout()
某个时间段 , 比如5秒后
context.WithDeadline()
某个时间点 . 比如每天20:30
// 10s后cancel
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
// 20:30 cancel
// time.Data()的方式
curr := time.Now()
t := time.Date(curr.Year(), curr.Month(), curr.Day(), 20, 30, 0, 0, time.Local)
ctx, cancel := context.WithDeadline(context.Background(), t)
// 以当前的时间加10分钟
context.WithDeadline(context.Background(),time.Now().Add( 10 * time.Minute))
带有时间的自动取消也可以自行调用 Cancel() 来实现主动取消
select {
// 3秒后主动取消
case <-time.NewTimer(3 * time.Second).C:
cancel()
// 通过withTimeout自动取消
// context.WithTimeout(context.Background(), 10 * time.Second)
case ctx.Done()
}
使用场景 : 不能确定主动调用是否能够调用成功 , 就可以使用但是取消
- 从底层开始看出 , 定时取消是在主动取消的基础上增加的功能
type timerCtx struct {
cancelCtx // 主动取消
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
- 而
WithTimeout()
也是利用WithDeadline()
实现的
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
4.3 Cancel 操作的向下传递
当父上下文被取消时 , 子上下文也会被取消
func ContextExtends() {
// 定义符合上图的context结构
ContextOne, _ := context.WithCancel(context.Background())
ContextTwo, cancel := context.WithCancel(ContextOne)
ContextThree, _ := context.WithCancel(ContextOne)
ContextFour, _ := context.WithCancel(ContextTwo)
wg := sync.WaitGroup{}
wg.Add(4)
// 开启四个goroutine分别监控四个context的Done()方法
go func(c context.Context) {
defer wg.Done()
select {
case <-ContextOne.Done():
fmt.Println("contextOne cancel")
}
}(ContextOne)
go func(c context.Context) {
defer wg.Done()
select {
case <-ContextTwo.Done():
fmt.Println("contextTwo cancel")
}
}(ContextTwo)
go func(c context.Context) {
defer wg.Done()
select {
case <-ContextThree.Done():
fmt.Println("contextThree cancel")
}
}(ContextThree)
go func(c context.Context) {
defer wg.Done()
select {
case <-ContextFour.Done():
fmt.Println("contextFour cancel")
}
}(ContextFour)
// 手动取消信号
cancel()
wg.Wait()
}
通过给对应 Context 执行 cancel , 输出遵循: 当父上下文被取消时 , 子上下文也会被取消
5. 取消操作流程源码
5.1 创建 cancelCtx 的流程
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
// 构建cancelCtx对象
c := newCancelCtx(parent)
// 传播Cancel操作
propagateCancel(parent, &c)
// 返回值,注意第二个cancel函数的实现
return &c, func() { c.cancel(true, Canceled) }
}
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
核心操作:
newCancelCtx
, 使用 parent 构建 cancelCtxpropagateCancel
, 传播 Cancel 操作,用来构建父子 Context 的关联,用于保证在父级 Context 取消时可以同步取消子级 Context
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
// parent不会触发cancel操作
done := parent.Done()
if done == nil {
return // parent is never canceled
}
// parent已经触发了cancel操作
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
// parent还没有触发cancel操作
if p, ok := parentCancelCtx(parent); ok {
// 内置cancelCtx类型
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
// 将当前context放入parent.children中
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
// 非内置cancelCtx类型
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
以上代码在建立 child 和 parent 的 cancelCtx 联系时,处理了下面情况:
- parent 不会触发 cancel 操作,不做任何操作,直接返回
- parent 已经触发了 cancel 操作,执行 child 的 cancel 操作,返回
- parent 还没有触发 cancel 操作,
child
会被加入parent
的children
列表中,等待parent
释放取消信号 - 如果是自定义 Context 实现了可用的 Done(),那么开启 goroutine 来监听
parent.Done()
和child.Done()
,同样在parent.Done()
时取消 child
如果是 WithDeadline 构建的 timerCtx,构建的过程多了两步:
- 对截至时间的判定,判定是否已经截至
- 设置定时器
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
// 已过时
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
// 设置定时器
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
5.2 ctx.Done() 初始信号 channel 流程
func (c *cancelCtx) Done() <-chan struct{} {
// 加载已经存在的
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
c.mu.Lock()
defer c.mu.Unlock()
// 初始化新的
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
其中两个步骤:
- 先尝试加载已经存在的
- 后初始化新的
核心要点是,当调用
Done()
时,初始化chan struct{}
, 而不是在上下文 cancelCtx 创建时,就初始化完成了。称为懒惰初始化。
5.3 cancel() 操作流程
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
// 设置 err
c.err = err
// 关闭channel
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
// 遍历全部可取消的子context
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
// 从parent的children删除自己
if removeFromParent {
removeChild(c.Context, c)
}
}
以上流程的核心操作:
- 关闭 channel,用来通知全部使用该 ctx 的 goroutine
- 遍历全部可取消的子 context,执行 child 的取消操作
- 从 parent 的 children 删除自己
6. Context 传值
如果在 ContextB 中设置了一个值 , 那么这个值只会在 contextA 和基于A产生的 contextC 和 contextD 中使用
Web 开发中 , 每一个新请求创建一个 context 中 , 将数据存储到 context 中 , 当前 context 中的后续调用都可以用到该值
context 数据类型 : key - value 数据
func WithValue(parent Context, key, val any) Context
type Context interface {
Value(key any) any
}
需要三个参数:
-
上级 Context
-
key 要求是 comparable 的(可比较的),实操时,推荐使用特定的 Key 类型,避免直接使用 string 或其他内置类型而带来 package 之间的冲突。
// 避免冲突
// 例如其他包中有相同的字段"name"
type MyContextKey String
- val any
6.1 单个 context 传值
type MyContext string
func ContextValue() {
wg := sync.WaitGroup{}
// 1.创建带有value的context
ctx := context.WithValue(context.Background(), MyContextKey("name"), "Sakura")
// 2.将ctx传入goroutine中
wg.Add(1)
key := "name"
go func(c context.Context, key any) {
defer wg.Done()
// 通过key拿到value
if value := c.Value(key); value != nil {
fmt.Println("value:", value)
return
}
fmt.Println("找不到key为\"", key, "\"的数据")
}(ctx, key)
wg.Wait()
}
要注意获取到的 value 为 any 空接口类型 , 如果想要使用 value 需要进行断言或者类型判断
- 查看WithValue()可以返回返回值为valueCTX
func WithValue(parent Context, key, val any) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
type valueCtx struct {
Context
key, val any
}
// valueCtx实现了这个value这个方法
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
也就是除了 value 功能,其他 Contenxt 功能都由 parent Context 实现。
如果 context.valueCtx.Value 方法查询的 key 不存在于当前 valueCtx 中,就会从父上下文中查找该键对应的值直到某个父上下文中返回
6.2 多个context传值
type MyContext string
func ContextValue() {
wg := sync.WaitGroup{}
// 1.创建带有value的context
ctxOne := context.WithValue(context.Background(), MyContext("name"), "Sakura1")
ctxTwo := context.WithValue(ctxOne, MyContext("name"), "Sakura2")
ctxThree := context.WithValue(ctxTwo, MyContext("name"), "Sakura3")
// 2.将ctx传入goroutine中
wg.Add(1)
key := "name"
go func(c context.Context, key any) {
defer wg.Done()
// 通过key拿到value
if value := c.Value(key); value != nil {
fmt.Println("value:", value)
return
}
fmt.Println("找不到key为\"", key, "\"的数据")
}(ctxThree, key)
wg.Wait()
}
不存在会返回该 context 中上一级的 value
7. 使用 context 的注意事项
-
推荐以参数的方式显示传递 Context
-
以 Context 作为参数的函数方法,应该把 Context 作为第一个参数。
-
给一个函数方法传递 Context 的时候,不要传递 nil,如果不知道传递什么,就使用context.TODO()
-
Context 的 Value 相关方法应该传递请求域的必要数据,不应该用于传递可选参数
-
Context 是线程安全的,可以放心的在多个 goroutine 中传递