context标准库 | 青训营笔记

69 阅读5分钟

这是我参与「第五届青训营」伴学笔记创作活动的第30天

Context 是Go 语言独有功能之一,用于上下文控制,可以在goroutine中进行传递

image.png

context 与select-case 联合,还可以实现上下文的截止时间、信号控制、信息传递等跨

goroutine 的操作

context 基本特性

image.png

  1. Background:创建一个空的 context,一般常用于作为根的父级 context
  2. WithCancel:基于父级 context,创建一个可以取消的新 context。
  3. WithDeadline:基于父级 context,创建一个具有截止时间(Deadline)的新 context。
  4. WithTimeout:基于父级 context,创建一个具**有超时时间(Timeout)**的新 context。
  5. TODO:创建一个空的 context,一般用于未确定时的声明使用。
  6. WithValue:基于某个 context 创建并存储对应的上下文信息。

一般用法

  1. 不要将context存储在结构类型中
    1. 在 Go 语言中,所有的第三方开源库,业务代码。几乎清一色的都会将context 放在方法的一个入参参数,作为首位形参
  2. 每个方法的第一个参数都将 context 作为第一个参数
    1. 本质目的是为了传播 context,自行完整调用链路上的各类控制
  3. 使用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 执行语句的时间问题
  1. 不要传递 nil context
    1. 很多时候我们在创建 context 时,还不知道其具体的作用和下一步用途是什么
    2. 这种时候大家可能会直接使用 context.Background 或是context.TODO方法
    3. 建议使用 context.TODO 方法来创建顶级的 context,直到弄清楚实际 Context 的下一步用途,再进行变更
var ( 
    background = new(emptyCtx) 
    todo = new(emptyCtx) 
) 
func Background() Context {
    return background 
} 
func TODO() Context {
    return todo 
}
  1. context 仅传递必要的值,不要让可选参数揉在一起
    1. context 传值适用于传必要的业务核心属性

context 本质

接口

Context 接口:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{} 
    Err() error 
    Value(key interface{}) interface{} }
  1. Deadline:获取当前 context 的截止时间
  2. Done:获取一个只读的 channel,类型为结构体。可用于识别当前channel 是否已经被关闭,其原因可能是到期,也可能是被取消了。
  3. Err:获取当前 context 被关闭的原因
  4. Value:获取当前 context 对应所存储的上下文信息

Canceler 接口:

type canceler interface { 
    cancel(removeFromParent bool, err error) 
    Done() <-chan struct{} 
}
  1. cancel:调用当前 context 的取消方法。
  2. Done:与前面一致,可用于识别当前 channel 是否已经被关闭

基础结构

在标准库 context 的设计上,一共提供了四类 context 类型来实现上述接口。分别是 emptyCtx、cancelCtx、timerCtx 、valueCtx

image.png

emptyCtx

  1. 空 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

  1. 其中的 newCancelCtx 方法将会生成出一个可以取消的新 context
  2. 如果该 context 执行取消,与其相关联的子 context 以及对应的 goroutine 也会收到取消信息

image.png

  1. 首先 main goroutine 创建并传递了一个新的 context 给 goroutine a,此时 goroutine a 的 context 是 main goroutine context 的子集
  2. 传递过程中,goroutine a 再将其 context 一个个传递给了 goroutine c、d、e
  3. 最后在运行时 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

  1. 在调用 context.WithTimeout 方法时,我们会涉及到timerCtx 类型,其主要特性是 Timeout 和 Deadline 事件
  2. timerCtx 类型是基于 cancelCtx 类型
type timerCtx struct {
    cancelCtx 
    timer *time.Timer // Under cancelCtx.mu. 
    deadline time.Time 
}
  1. cancel 方法
    1. 先会调用cancelCtx 类型的取消事件
    2. 若存在父级节点,则移除当前context 子节点
    3. 最后停止定时器并进行定时器重置

valueCtx

image.png

  1. 本质上valueCtx 类型是一个单向链表,会在调用Value 方法时:
    1. 先查询自己的节点是否有该值
    2. 若无,则会通过自身存储的上层父级节点的信息一层层向上寻找对应的值,直到找到为止。

context 取消事件

  • context 是如何实现跨 goroutine 的取消事件并传播开来的,是如何实现的?
    • propagateCancel 方法,其作用是构建父子级的上下文的关联关系
  1. 当父级上下文(parent)的Done 结果为 nil时,将会直接返回,因为其不会具备取消事件的基本条件,可能该 context 是 Background、TODO 等方法产生的空白 context
  2. 当父级上下文(parent)的 Done 结果不为 nil 时,则发现父级上下文已经被取消,作为其子级,该 context 将会触发取消事件并返回父级上下文的取消原因

当前父级和子级 context 均正常(未取消),将会执行以下流程:

  1. 调用 parentCancelCtx 方法找到具备取消功能的父级 context。并将当前 context,也就是 child 加入到 父级 context 的 children 列表中,等待后续父级 context 的取消事件通知和响应。
  2. 调用 parentCancelCtx 方法没有找到,将会启动一个新的 goroutine 去监听父子 context 的取消事件通知。

image.png