Skip to main content

Golang context 的使用和应用

· 4 min read

1. 背景

context 是 Go 标准库中的一个核心包,用于在多个 Goroutine 之间传递请求相关的上下文信息(如超时、取消信号、键值数据等)。它常用于高并发场景(如 HTTP 服务、RPC 调用、数据库操作等),帮助优雅地控制 Goroutine 的生命周期。

2. 核心功能

  • 超时控制:通过 context.WithTimeout 设置截止时间,避免请求长时间阻塞。
  • 取消传播:通过 context.WithCancel 发送取消信号,通知所有相关 Goroutine 退出。
  • 数据传递:通过 context.WithValue 在调用链中安全传递键值对(但需谨慎使用)。

3. 典型应用场景

  • HTTP 服务:为每个请求创建 ctx,传递请求 ID、超时设置等。
  • 数据库/API 调用:将 ctx 传递给底层调用,确保资源及时释放。
  • 分布式追踪:通过 ctx 传递链路跟踪信息(如 OpenTelemetry)。

4. 代码示例

案例 1: 简单

package main

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

func main() {
// 创建一个带有超时的 context(1秒后自动取消)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() // 确保释放资源

// 模拟一个耗时操作
go func() {
select {
case <-time.After(2 * time.Second):
fmt.Println("任务完成")
case <-ctx.Done(): // 监听取消信号
fmt.Println("任务取消:", ctx.Err())
}
}()

// 等待结果
time.Sleep(2 * time.Second)
}

案例 2: 分布式服务调用链,数据库写作,可取消管道

package main

import (
"context"
"fmt"
"log"
"math/rand"
"sync"
"time"
)

// 模拟服务返回结构
type Result struct {
ServiceName string
Data string
Err error
}

// 生成追踪ID
func generateTraceID() string {
return fmt.Sprintf("trace-%d-%d", time.Now().UnixNano(), rand.Intn(1000))
}

// 模拟认证服务
func callAuthService(ctx context.Context, wg *sync.WaitGroup, results chan<- Result) {
defer wg.Done()

// 从上下文获取追踪ID
traceID := ctx.Value("trace_id").(string)
log.Printf("[%s] 开始调用认证服务", traceID)

select {
case <-time.After(time.Duration(rand.Intn(2000)) * time.Millisecond): // 随机耗时
results <- Result{
ServiceName: "auth",
Data: "user_authenticated",
}
case <-ctx.Done():
results <- Result{
ServiceName: "auth",
Err: fmt.Errorf("认证服务取消: %v", ctx.Err()),
}
}
}

// 模拟数据库调用
func callDB(ctx context.Context, wg *sync.WaitGroup, results chan<- Result) {
defer wg.Done()

traceID := ctx.Value("trace_id").(string)
log.Printf("[%s] 开始调用数据库", traceID)

select {
case <-time.After(time.Duration(rand.Intn(3000)) * time.Millisecond):
results <- Result{
ServiceName: "database",
Data: "query_result",
}
case <-ctx.Done():
results <- Result{
ServiceName: "database",
Err: fmt.Errorf("数据库调用取消: %v", ctx.Err()),
}
}
}

func HandleRequest(ctx context.Context) {
// 注入追踪ID(实际项目可用OpenTelemetry等)
traceID := generateTraceID()
ctx = context.WithValue(ctx, "trace_id", traceID)
log.Printf("[%s] 请求开始", traceID)

// 设置总超时(实际根据业务需求调整)
ctx, cancel := context.WithTimeout(ctx, 1500*time.Millisecond)
defer cancel()

// 并发调用服务
var wg sync.WaitGroup
results := make(chan Result, 2) // 缓冲通道防止goroutine泄漏

wg.Add(2)
go callAuthService(ctx, &wg, results)
go callDB(ctx, &wg, results)

// 异步关闭结果通道
go func() {
wg.Wait()
close(results)
}()

// 处理结果
successCount := 0
for r := range results {
if r.Err != nil {
log.Printf("[%s] %s 失败: %v", traceID, r.ServiceName, r.Err)
cancel() // 触发快速失败
continue
}
successCount++
log.Printf("[%s] %s 成功: %s", traceID, r.ServiceName, r.Data)
}

if successCount == 2 {
log.Printf("[%s] 所有服务调用成功", traceID)
} else {
log.Printf("[%s] 部分服务调用失败", traceID)
}
}

func main() {
rand.Seed(time.Now().UnixNano())

// 模拟10次请求
for i := 0; i < 10; i++ {
HandleRequest(context.Background())
time.Sleep(500 * time.Millisecond) // 间隔避免日志混杂
}
}

关键设计说明

  1. 完整执行流

    • 生成唯一追踪 ID
    • 设置全局超时(1500ms)
    • 并发调用两个服务(随机耗时 0-2000ms/0-3000ms)
    • 处理成功/失败结果
  2. 容错机制

    case <-ctx.Done():
    results <- Result{Err: fmt.Errorf("服务取消: %v", ctx.Err())}

任何服务失败时通过cancel()触发其他服务的快速终止

  1. 实战验证: 运行后会看到三种典型场景:

    • 两个服务都在超时前完成(成功)
    • 一个服务超时导致另一个被取消(部分失败)
    • 两个服务都超时(完全失败)
  2. 扩展建议

    • 添加pprof监控查看 goroutine 泄漏
    • 集成opentelemetry替换简单 traceID
    • 增加重试逻辑(需使用context.Deadline()剩余时间)

5. 注意事项

  • 避免滥用 WithValue:数据传递应仅用于请求范围的元数据(如认证信息),而非函数参数。
  • 及时调用 cancel():防止上下文泄漏(如未调用 cancel 可能导致 Goroutine 堆积)。

6. 扩展学习

  • 官方文档:pkg.go.dev/context
  • 最佳实践:在中间件、gRPC 等框架中观察 ctx 的实际使用。