Skip to content

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 创建时就已经是取消状态。