引入

服务调用了下游 API,等了 30 秒还没返回,用户早跑了。goroutine 启动后怎么优雅取消?多个请求共享的用户信息怎么传递?context.Context 统一管理超时、取消和请求级数据,是 Go 并发编程的基础设施。

正文

定义

定义

context.Context 是 Go 标准库中用于传递截止时间、取消信号和请求级数据的机制。它通过 WithCancelWithTimeoutWithValue 等函数创建派生 context,沿调用链传播,控制 goroutine 的生命周期。

语法

import "context"
 
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
 
select {
case <-ctx.Done():
    fmt.Println(ctx.Err())
default:
    // 正常工作
}

例子

WithCancel

func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("收到取消信号:", ctx.Err())
            return
        default:
            fmt.Println("工作中...")
            time.Sleep(500 * time.Millisecond)
        }
    }
}
 
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go worker(ctx)
    time.Sleep(2 * time.Second)
    cancel()
    time.Sleep(100 * time.Millisecond)
}

WithCancel 返回一个可取消的 context 和 cancel 函数,调用 cancel()ctx.Done() 通道关闭,所有监听方收到信号。

WithTimeout

func fetchData(ctx context.Context) (string, error) {
    ch := make(chan string, 1)
    go func() {
        time.Sleep(3 * time.Second)
        ch <- "数据"
    }()
 
    select {
    case result := <-ch:
        return result, nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}
 
func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()
 
    result, err := fetchData(ctx)
    if err != nil {
        fmt.Println("超时:", err) // context deadline exceeded
    } else {
        fmt.Println(result)
    }
}

WithTimeout 设置截止时间,到期后自动取消。比手动算时间再用 WithCancel 方便得多。

WithValue

type key string
 
const userIDKey key = "userID"
 
func handleRequest(ctx context.Context) {
    uid := ctx.Value(userIDKey)
    fmt.Println("用户ID:", uid)
}
 
func main() {
    ctx := context.WithValue(context.Background(), userIDKey, "12345")
    handleRequest(ctx) // 用户ID: 12345
}

WithValue 在 context 中存请求级数据,沿调用链向下传递。key 要用自定义类型避免冲突。

WithDeadline

deadline := time.Now().Add(2 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
 
select {
case <-ctx.Done():
    fmt.Println(ctx.Err())
case <-time.After(3 * time.Second):
    fmt.Println("完成")
}

WithDeadline 指定具体截止时间点WithTimeout 是它的简化版(基于当前时间加偏移)。

context 传递

func doWork(ctx context.Context) {
    subCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
    defer cancel()
 
    select {
    case <-subCtx.Done():
        fmt.Println("子任务超时:", subCtx.Err())
    case <-time.After(1 * time.Second):
        fmt.Println("子任务完成")
    }
}

子 context 继承父 context 的取消信号和截止时间,父取消则子也取消,形成树状传播

常见写法

写法说明
context.Background()根 context,用于 main 和初始化
context.TODO()占位用,表示还没想好用什么 context
context.WithCancel(parent)创建可取消的 context
context.WithTimeout(parent, d)创建带超时的 context
context.WithDeadline(parent, t)创建带截止时间的 context
context.WithValue(parent, k, v)存储请求级数据
ctx.Done()返回取消信号通道
ctx.Err()返回取消原因(CanceledDeadlineExceeded
ctx.Value(key)获取存储的值
defer cancel()确保 cancel 被调用,防止泄漏

特点

  • context不可变的,每个 WithXxx 返回新的子 context
  • cancel() 必须调用,否则 goroutine 和资源会泄漏,用 defer cancel() 保底
  • context 形成树状结构,父取消则所有子节点取消
  • WithValue 只存请求级数据(如 trace ID、用户 ID),别拿来存配置
  • WithValue 的 key 要用自定义类型,避免和其他包冲突
  • context.Background() 用于顶层context.TODO() 用于还没确定的地方
  • 函数签名把 context.Context 作为第一个参数是 Go 的惯例

理解

context.Context 就是 Go 并发世界的”信号灯系统”。WithCancel 手动关,WithTimeout 自动关,WithValue 带行李。父 context 一取消,所有子 context 跟着取消,像树一样一层层传下去。记住 defer cancel() 别忘写,context 当第一个参数传,这两条是铁律。

引出

到这里,Go 的核心知识点基本过了一遍。从基础类型、函数、结构体、接口到并发、测试、工具链、泛型、反射、正则和 context,这些知识组合起来就能应对大部分实际项目了。下一步就是动手写项目——一个小工具、一个 API 服务、一个 CLI 程序,把学到的东西用起来,才是真正学会 Go 的开始。