context

context 用于在跨 goroutine、跨函数调用的请求链路中传递取消信号、截止时间与请求范围数据。它是网络服务中控制请求生命周期的标准设施,几乎所有涉及 I/O 的标准库与第三方库(net/http、database/sql 等)都接受 context 参数。

基本用法

context.Context 是一个接口,通过根 context 派生子 context 形成树状结构,父节点取消时所有子节点随之取消:

构造函数用途
context.Background()根 context,通常在 main 或请求入口使用
context.TODO()尚不确定用哪个时的占位
context.WithCancel(parent)手动取消
context.WithTimeout(parent, d)超时自动取消
context.WithDeadline(parent, t)指定绝对时间取消
context.WithValue(parent, k, v)携带请求范围数据

超时控制

package main

import (
    "context"
    "fmt"
    "time"
)

func slowTask(ctx context.Context) error {
    select {
    case <-time.After(3 * time.Second): // 模拟耗时操作
        return nil
    case <-ctx.Done(): // 响应取消信号
        return ctx.Err() // context.DeadlineExceeded
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel() // 惯例:无论是否超时都要释放资源

    if err := slowTask(ctx); err != nil {
        fmt.Println("任务失败:", err) // 任务失败: context deadline exceeded
        return
    }
    fmt.Println("任务完成")
}
Tip

两条铁律:

  1. 所有耗时函数的第一个参数应是 ctx context.Context(标准库惯例)。
  2. 派生 context 后必须 defer cancel(),避免资源泄漏,即便超时会自动触发取消。

context 的取消是协作式的:只有函数主动检查 ctx.Done() 才会提前退出,长循环中应定期检查。

手动取消

package main

import (
    "context"
    "fmt"
    "sync"
)

func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("worker %d 收到取消信号\n", id)
            return
        default:
            // 执行一个工作单元(实际项目应更细粒度检查 ctx)
            fmt.Printf("worker %d 工作中\n", id)
            time.Sleep(100 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    var wg sync.WaitGroup

    for i := 1; i <= 2; i++ {
        wg.Add(1)
        go worker(ctx, i, &wg)
    }

    time.Sleep(300 * time.Millisecond)
    cancel() // 广播取消信号给所有派生 goroutine
    wg.Wait()
}

需要 import "time" 配合上例运行。

传递请求范围数据

WithValue 用于携带跨层请求元数据(如请求 ID、认证信息):

package main

import (
    "context"
    "fmt"
)

type ctxKey string // 自定义键类型,避免与其它包的键冲突

const requestIDKey ctxKey = "requestID"

func handler(ctx context.Context) {
    if id, ok := ctx.Value(requestIDKey).(string); ok {
        fmt.Println("请求 ID:", id)
    }
}

func main() {
    ctx := context.WithValue(context.Background(), requestIDKey, "req-1001")
    handler(ctx)
}
Warning

WithValue 不应替代函数参数传递业务数据。仅用于横切关注点(日志、链路追踪、认证);键必须使用自定义类型,取值时做类型断言。

在标准库中的集成

HTTP 服务端会自动为每个请求生成带超时的 context,客户端与服务端均可利用它控制 I/O 生命周期:

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://httpbin.org/delay/1", nil)

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        fmt.Println("请求失败:", err)
        return
    }
    defer resp.Body.Close()
    fmt.Println("状态码:", resp.Status)
}

详见 REST 客户端章节。

小结

  • context 树状传播取消信号,是请求生命周期的统一载体。
  • WithTimeout + defer cancel() 是超时控制的标准组合。
  • 取消是协作式的,耗时逻辑需主动监听 ctx.Done()