Appearance
33|context 与取消
context.Context 在并发程序里负责传递取消信号和超时。它不是强行终止 goroutine 的机制,而是通过 ctx.Done() 这个 channel 发出"已经取消"或"已经超时"的信号。函数内部监听 ctx.Done(),收到信号后返回,取消才会实际生效。HTTP 请求、数据库查询、Kubernetes Client 调用都支持 context,它是 Go 并发控制的核心工具。
一、超时控制
go
package main
import (
"context"
"fmt"
"time"
)
func check(ctx context.Context, host string) error {
select {
case <-time.After(2 * time.Second):
fmt.Println(host, "ok")
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
if err := check(ctx, "web01"); err != nil {
fmt.Println("check failed:", err) // context deadline exceeded
}
}context.WithTimeout 创建一个带超时限制的上下文。1 秒后 ctx.Done() 关闭,check 函数收到信号后返回 ctx.Err(),错误通常是 context deadline exceeded。
defer cancel() 不能省。WithTimeout/WithCancel 内部会启动一个 goroutine 等待超时或取消——不调 cancel(),这个 goroutine 和它持有的计时器会一直存在,直到超时到期。请求提前返回时如果不 cancel,等于白占资源。习惯上,创建 context 的下一行就写 defer cancel()。
二、取消传播
context.WithCancel 创建可手动取消的上下文,取消信号会沿着上下文树传播:
go ctx,
defer cancel()
go func() {
// 子 goroutine 也收到取消信号
<-ctx.Done()
fmt.Println("child cancelled")
}()
cancel() // 手动取消,所有子上下文都收到信号批量请求接口时,如果整体任务已经超时,继续等待每个请求意义不大。context 能把"整批任务取消"传给下游函数,避免后台请求继续占连接和 goroutine。
三、WithDeadline
WithDeadline 在指定时间点取消,和 WithTimeout 的区别是参数类型:
go
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
defer cancel()WithTimeout(duration) 等于 WithDeadline(time.Now().Add(duration)),两者底层一样。
四、传递元数据
context.WithValue 在 context 里存 key-value,用于传递请求级元数据(如 requestID、用户ID):
go
func withRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, "request_id", id)
}
func getRequestID(ctx context.Context) string {
id, _ := ctx.Value("request_id").(string)
return id
}使用:
go
ctx := withRequestID(context.Background(), "abc123")
logger.Info("processing", slog.String("request_id", getRequestID(ctx)))context.Value 不是通用数据容器,只用于传递请求作用域的元数据。不要把业务对象塞进 context,那会让代码难以理解和测试。
五、context 树
context 可以嵌套,形成树形结构:
go
root := context.Background()
// 第一层:超时 10 秒
ctx1, cancel1 := context.WithTimeout(root, 10*time.Second)
// 第二层:在 ctx1 基础上再超时 5 秒(实际以 5 秒为准)
ctx2, cancel2 := context.WithTimeout(ctx1, 5*time.Second)
// 第三层:手动取消
ctx3, cancel3 := context.WithCancel(ctx2)任何一个祖先 context 被取消,所有后代都会收到信号。但子 context 的取消不会影响父 context。
六、常见错误
context 不传递
函数签名里不接受 context.Context 参数,内部启动的 goroutine 就无法被取消。写库函数时,第一个参数留 ctx context.Context 是 Go 社区的约定。
把可选参数放进 context
go
ctx := context.WithValue(ctx, "page_size", 10)
ctx := context.WithValue(ctx, "sort_by", "name")函数参数应该显式声明,不要用 context 传递可选参数。context value 只用于跨越 API 边界的元数据(如 requestID、traceID)。
忘记调用 cancel
go
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// 忘了 defer cancel()导致 goroutine 泄漏。养成习惯:创建 context 的下一行就写 defer cancel()。
向已取消的 context 派生新 context
go
ctx, cancel := context.WithCancel(context.Background())
cancel()
ctx2, _ := context.WithTimeout(ctx, 5*time.Second) // ctx2 已经取消了父 context 已取消,子 context 创建时就已经是取消状态。