Go context goroutine termination, timeouts, and passing values

使用 Go Context 处理 goroutine 终止、超时、传递请求相关的值

前言

Context 是 Go 1.7 添加于标准函数库的功能。常在存取数据库或其他服务时会遇到,初步看起来是用于「传递取消信号」用途的语法,用于处理 goroutine:

  • 背负期限(deadline)
  • 取消信号(cancellation signal)
  • 传递请求相关的值(request-scoped values)

Context 是啥?

举例当一个请求会衍生出多个 goroutine、或会调用多层函数时,常会遇到几个问题:

  • 上层请求已经结束了,但底下 goroutine 还在跑
  • 某个操作卡住,希望超时后能全部中断
  • 想在整条调用链中携带 request id 之类的信息

Go context 包了 channel 在 goroutine 间传递,定义其存在的理由(某条件或期限达成),而这个理由应该与 context 相关联。

// Context 背负期限(deadline)、取消信号(cancellation signal)、与请求相关的值(request-scoped values)
// 它的方法可以被多个 goroutine 同时安全地使用。
type Context interface {
// Done 会返回一个 channel,
// 当此 Context 被取消或逾时(timeout)时,该 channel 会被关闭。
Done() <-chan struct{}
// Err 会在 Done channel 关闭之后,
// 返回此 Context 被取消的原因。
Err() error
// Deadline 会返回此 Context 预计被取消的时间(如果有设置的话)。
// ok 表示是否存在 deadline。
Deadline() (deadline time.Time, ok bool)
// Value 会返回与指定 key 关联的值;
// 如果没有对应的值,则返回 nil。
Value(key interface{}) interface{}
}

Done() <-chan struct{} 用例

结束时 Done() channel 才会被关闭。未关闭前会返回 nil

  1. Context 被取消(cancel)
  2. 超时(timeout)
  3. parent Context 结束
select {
case <-ctx.Done():
// 收到取消或超时信号
return ctx.Err()
case result := <-workChan:
return result
}

Err() error 用例

当 Context 结束后得知原因。未关闭前会返回 nil

<-ctx.Done()
log.Println(ctx.Err())

Deadline() (time.Time, bool) 用例

查 Context 是否有设置截止时间。

  • ok == false:没有 deadline
  • ok == true:deadline 是什么时候

Value(key interface{}) interface{} 用例

Value 允许你在 Context 里存放「请求范围内」的数据,例如:

// 上游创建 context
ctx = context.WithValue(ctx, "request_id", "abc-123")
// 下游读取 context
reqID := ctx.Value("request_id")

与 Context 搭配的方法

Background

所有 Context 的根节点,通常用于主函数、初始化或测试中

返回一个非 nil 的空 Context,它:

  • 永远不会被取消
  • 没有截止时间
  • 没有携带任何值
// ctx1 是根节点(Background)
ctx1 := context.Background()
// ctx2 是 ctx1 的子节点
ctx2 := context.WithValue(ctx1, "key", "value") // 仍然不可取消(只是带了值)
// ctx3 是 ctx2 的子节点
ctx3, cancel := context.WithTimeout(ctx2, 5*time.Second) // 可以取消(有超时)
defer cancel()
context.Background() (永远不会被取消)
└─> WithValue(...) (继承父节点,仍不可取消,但带了值)
└─> WithTimeout(...) (可以被取消!5秒后自动取消)
└─> WithCancel(...) (可以被取消!调用 cancel() 时取消)

TODO

当你不确定该用哪个 Context 时的占位符
func someFunction() {
// 暂时不确定要用什么 Context,先用 TODO
ctx := context.TODO()
anotherFunction(ctx)
}

WithCancel

当你希望「某个事件发生时,主动中止所有相关 goroutine」
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 很重要,避免资源泄漏
go func() {
time.Sleep(2 * time.Second)
cancel()
}()
<-ctx.Done()
fmt.Println(ctx.Err()) // context.Canceled

WithDeadline

当你希望「在指定的时间点之前完成,主动中止所有相关 goroutine」
// API 必须在特定时间前响应
deadline := time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC)
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
resp, err := client.DoWithContext(ctx, req)

WithTimeout

当你希望「某个期限过后,主动中止所有相关 goroutine」
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
select {
case <-time.After(5 * time.Second):
fmt.Println("finished work")
case <-ctx.Done():
fmt.Println(ctx.Err()) // context.DeadlineExceeded
}

WithValue

Context 数值应基于 request 范围内的数据传输过程和 API 数据
// 上游创建 context
ctx := context.Background()
ctx = context.WithValue(ctx, "request_id", "abc-123")
// 下游读取 context
reqID := ctx.Value("request_id")
// 创建私有类型避免 key 撞名
type ctxKeyRequestID struct{}
ctx = context.WithValue(ctx, ctxKeyRequestID{}, "abc-123")

总结

Go Context 是一种官方方式去管理 Go Routine 的生命周期基于 Channel

目前我只在与 DB 互动上使用到这项功能,或许之后会遇到更多情境帮得上忙。就是一种官方的方式管理 Go Routine 的生命周期。

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var todos []models.Todo
cursor, err := db.GetCollection().Find(ctx, bson.M{})

延伸阅读