这是我参与「第五届青训营」伴学笔记创作活动的第30天
Context 是Go 语言独有功能之一,用于上下文控制,可以在goroutine中进行传递
context 与select-case 联合,还可以实现上下文的截止时间、信号控制、信息传递等跨
goroutine 的操作
context 基本特性
Background:创建一个空的 context,一般常用于作为根的父级 contextWithCancel:基于父级 context,创建一个可以取消的新 context。WithDeadline:基于父级 context,创建一个具有截止时间(Deadline)的新 context。WithTimeout:基于父级 context,创建一个具**有超时时间(Timeout)**的新 context。TODO:创建一个空的 context,一般用于未确定时的声明使用。WithValue:基于某个 context 创建并存储对应的上下文信息。
一般用法
- 不要将context存储在结构类型中
- 在 Go 语言中,所有的第三方开源库,业务代码。几乎清一色的都会将context 放在方法的一个入参参数,作为
首位形参
- 在 Go 语言中,所有的第三方开源库,业务代码。几乎清一色的都会将context 放在方法的一个入参参数,作为
- 每个方法的第一个参数都将 context 作为第一个参数
- 本质目的是为了传播 context,自行完整调用链路上的各类控制
- 使用
ctx变量名惯用语
func List(ctx context.Context, db *sqlx.DB) ([]User, error) {
ctx, span := trace.StartSpan(ctx, "internal.user.List")
defer span.End()
users := []User{}
const q = `SELECT * FROM users`
if err := db.SelectContext(ctx, &users, q); err != nil {
return nil, errors.Wrap(err, "selecting users")
}
return users, nil
}
- 将外部的 context 传入 List 方法,再传入 SQL 执行的方法,解决了 SQL 执行语句的时间问题
- 不要传递 nil context
- 很多时候我们在创建 context 时,还不知道其具体的作用和下一步用途是什么
- 这种时候大家可能会直接使用
context.Background 或是context.TODO方法 - 建议使用 context.TODO 方法来创建顶级的 context,直到弄清楚实际 Context 的下一步用途,再进行变更
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
- context 仅传递必要的值,不要让可选参数揉在一起。
- context 传值适用于传必要的业务核心属性
context 本质
接口
Context 接口:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{} }
Deadline:获取当前 context 的截止时间。Done:获取一个只读的 channel,类型为结构体。可用于识别当前channel 是否已经被关闭,其原因可能是到期,也可能是被取消了。Err:获取当前 context 被关闭的原因。Value:获取当前 context 对应所存储的上下文信息。
Canceler 接口:
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
cancel:调用当前 context 的取消方法。Done:与前面一致,可用于识别当前 channel 是否已经被关闭。
基础结构
在标准库 context 的设计上,一共提供了四类 context 类型来实现上述接口。分别是 emptyCtx、cancelCtx、timerCtx 、valueCtx
emptyCtx
空 context 的定义,因此没有 deadline,更没有 timeout,可以认为就是一个基础空白 context 模板
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 interface{}) interface{} {
return nil
}
cancelCtx
- 其中的 newCancelCtx 方法将会生成出一个
可以取消的新 context - 如果该 context 执行取消,
与其相关联的子 context 以及对应的 goroutine 也会收到取消信息
- 首先 main goroutine 创建并传递了一个新的 context 给 goroutine a,此时 goroutine a 的 context 是 main goroutine context 的子集
- 传递过程中,goroutine a 再将其 context 一个个传递给了 goroutine c、d、e
- 最后在运行时 goroutine a
调用了 cancel 方法。使得该 context 以及其对应的子集均接受到取消信号,对应的 goroutine 也进行了响应
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done 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
}
timerCtx
- 在调用 context.WithTimeout 方法时,我们会涉及到timerCtx 类型,其主要特性是
Timeout 和 Deadline 事件 - timerCtx 类型是基于 cancelCtx 类型的
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
- cancel 方法
- 先会调用cancelCtx 类型的取消事件
- 若存在父级节点,则移除当前context 子节点
- 最后停止定时器并进行定时器重置
valueCtx
- 本质上valueCtx 类型是一个
单向链表,会在调用Value 方法时:- 先查询自己的节点是否有该值
- 若无,则会通过自身存储的上层父级节点的信息一层层向上寻找对应的值,直到找到为止。
context 取消事件
- context 是如何实现跨 goroutine 的取消事件并传播开来的,是如何实现的?
- propagateCancel 方法,其作用是构建父子级的上下文的关联关系
- 当父级上下文(parent)的
Done 结果为 nil时,将会直接返回,因为其不会具备取消事件的基本条件,可能该 context 是 Background、TODO 等方法产生的空白 context - 当父级上下文(parent)的
Done 结果不为 nil时,则发现父级上下文已经被取消,作为其子级,该 context 将会触发取消事件并返回父级上下文的取消原因
当前父级和子级 context 均正常(未取消),将会执行以下流程:
- 调用 parentCancelCtx 方法
找到具备取消功能的父级 context。并将当前 context,也就是 child 加入到 父级 context 的 children 列表中,等待后续父级 context 的取消事件通知和响应。 - 调用 parentCancelCtx 方法没有找到,将会
启动一个新的 goroutine 去监听父子 context 的取消事件通知。