Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Go 语言标准库 —— context 包(上下文)


🔹 概述

context 包定义了 Context 类型,用于在 goroutine 之间传递截止时间、取消信号和其他请求范围的值。

主要功能:

  • 取消操作(Cancellation)
  • 超时控制(Timeout)
  • 截止时间(Deadline)
  • 传递请求范围的值(Values)

重要说明:

  • context 是并发安全的
  • 用于管理 goroutine 的生命周期
  • 避免 goroutine 泄漏
  • 控制请求的生命周期
  • 是 Go 并发编程的核心组件

使用场景:

  • HTTP 服务器处理请求
  • RPC 调用
  • 数据库查询
  • 并发任务管理
  • 资源清理

🔹 核心接口

Context 接口

context.Context interface

  • 说明:

    • 上下文接口,所有 context 实现都实现此接口
    • 并发安全
    • 可以在多个 goroutine 之间共享
  • 接口定义:

    type Context interface {
        Deadline() (deadline time.Time, ok bool)
        Done() <-chan struct{}
        Err() error
        Value(key interface{}) interface{}
    }
    
  • 方法详解:

    • Deadline()

      • 说明:返回截止时间
      • 返回值:
        • deadline time.Time - 截止时间
        • ok bool - 是否设置了截止时间
      • 示例:
        deadline, ok := ctx.Deadline()
        if ok {
            fmt.Println("截止时间:", deadline)
        }
        
    • Done()

      • 说明:返回一个只读通道
      • 返回值:
        • <-chan struct{} - 当 context 被取消或超时时关闭
      • 注意:
        • 通道关闭表示 context 完成
        • 不应直接读写该通道
      • 示例:
        select {
        case <-ctx.Done():
            fmt.Println("context 已取消")
        default:
            // 继续工作
        }
        
    • Err()

      • 说明:返回 context 完成的原因
      • 返回值:
        • error - 如果 Done 未关闭返回 nil
        • 如果已关闭,返回取消原因
      • 错误类型:
        • Canceled - context 被取消
        • DeadlineExceeded - 超过截止时间
      • 示例:
        err := ctx.Err()
        if err == context.Canceled {
            fmt.Println("已取消")
        } else if err == context.DeadlineExceeded {
            fmt.Println("已超时")
        }
        
    • Value()

      • 说明:获取与 key 关联的值
      • 参数:
        • key interface{} - 键
      • 返回值:
        • interface{} - 与 key 关联的值
      • 注意:
        • 仅用于传递请求范围的数据
        • 不应用于传递配置或选项
      • 示例:
        type contextKey string
        const userIDKey contextKey = "userID"
        
        value := ctx.Value(userIDKey)
        if value != nil {
            userID := value.(string)
        }
        

🔹 核心函数

创建可取消的 Context

context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)

  • 说明:

    • 从父 context 创建一个新的可取消的 context
    • 返回 context 和取消函数
    • 调用 cancel 函数会关闭 ctx.Done() 通道
  • 参数:

    • parent Context - 父 context
  • 返回值:

    • ctx Context - 新的 context
    • cancel CancelFunc - 取消函数
  • 重要说明:

    • 必须调用 cancel 函数释放资源
    • 通常使用 defer 调用
    • 多次调用 cancel 是安全的
  • 示例:

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    func main() {
    	ctx, cancel := context.WithCancel(context.Background())
    	defer cancel() // 确保释放资源
    
    	// 启动 goroutine
    	go func() {
    		for i := 0; i < 5; i++ {
    			select {
    			case <-ctx.Done():
    				fmt.Println("收到取消信号")
    				return
    			default:
    				fmt.Printf("工作 %d\n", i)
    				time.Sleep(1 * time.Second)
    			}
    		}
    	}()
    
    	// 2 秒后取消
    	time.Sleep(2 * time.Second)
    	cancel()
    
    	time.Sleep(1 * time.Second)
    }
    

创建带截止时间的 Context

context.WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

  • 说明:

    • 创建在指定时间自动取消的 context
    • 到达时间 d 时自动调用 cancel
  • 参数:

    • parent Context - 父 context
    • d time.Time - 截止时间
  • 返回值:

    • Context - 新的 context
    • CancelFunc - 取消函数
  • 注意:

    • 到达截止时间自动取消
    • 也应调用 cancel 释放资源
  • 示例:

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    func main() {
    	// 10 秒后自动取消
    	ctx, cancel := context.WithDeadline(
    		context.Background(),
    		time.Now().Add(10*time.Second),
    	)
    	defer cancel()
    
    	select {
    	case <-time.After(5 * time.Second):
    		fmt.Println("完成工作")
    	case <-ctx.Done():
    		fmt.Println("context 取消:", ctx.Err())
    	}
    }
    

创建带超时的 Context

context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

  • 说明:

    • 创建在指定超时时间后自动取消的 context
    • 等价于 WithDeadline(parent, time.Now().Add(timeout))
  • 参数:

    • parent Context - 父 context
    • timeout time.Duration - 超时时长
  • 返回值:

    • Context - 新的 context
    • CancelFunc - 取消函数
  • 注意:

    • 超时后自动取消
    • 必须调用 cancel 释放资源
  • 示例:

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    func main() {
    	// 5 秒超时
    	ctx, cancel := context.WithTimeout(
    		context.Background(),
    		5*time.Second,
    	)
    	defer cancel()
    
    	select {
    	case <-time.After(3 * time.Second):
    		fmt.Println("完成工作")
    	case <-ctx.Done():
    		fmt.Println("超时:", ctx.Err())
    	}
    }
    

创建带值的 Context

context.WithValue(parent Context, key, val interface{}) Context

  • 说明:

    • 创建携带键值对的 context
    • 用于传递请求范围的数据
  • 参数:

    • parent Context - 父 context
    • key interface{} - 键(建议使用自定义类型)
    • val interface{} - 值
  • 返回值:

    • Context - 新的 context
  • 重要说明:

    • 键应该使用自定义类型(避免冲突)
    • 不应用于传递配置
    • 不应用于传递大量数据
  • 示例:

    package main
    
    import (
    	"context"
    	"fmt"
    )
    
    // 使用自定义类型作为 key
    type contextKey string
    
    const (
    	userIDKey  contextKey = "userID"
    	usernameKey contextKey = "username"
    )
    
    func main() {
    	ctx := context.Background()
    	
    	// 添加值
    	ctx = context.WithValue(ctx, userIDKey, "123")
    	ctx = context.WithValue(ctx, usernameKey, "alice")
    
    	// 获取值
    	userID := ctx.Value(userIDKey).(string)
    	username := ctx.Value(usernameKey).(string)
    
    	fmt.Printf("User: %s (%s)\n", username, userID)
    }
    

🔹 预定义 Context

背景 Context

context.Background()

  • 说明:

    • 返回一个空的 context
    • 永不取消,没有值,没有截止时间
    • 通常作为根 context 使用
  • 返回值:

    • Context - 背景 context
  • 使用场景:

    • main 函数
    • 测试
    • 作为其他 context 的父 context
  • 示例:

    // 作为根 context
    ctx := context.Background()
    
    // 创建子 context
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()
    

待办 Context

context.TODO()

  • 说明:

    • 返回一个空的 context
    • 当不确定使用哪个 context 时使用
    • 代码审查时会被标记
  • 返回值:

    • Context - 待办 context
  • 使用场景:

    • 重构时临时使用
    • 不确定使用哪个 context 时
  • 示例:

    // 临时使用,稍后替换
    ctx := context.TODO()
    
    // 代码审查时会提醒替换为合适的 context
    

🔹 使用场景

1. HTTP 服务器中的取消

package main

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

func handler(w http.ResponseWriter, r *http.Request) {
	// 从请求中获取 context
	ctx := r.Context()

	// 模拟长时间运行的任务
	for i := 0; i < 10; i++ {
		select {
		case <-ctx.Done():
			fmt.Println("请求已取消")
			return
		default:
			fmt.Printf("处理请求 %d\n", i)
			time.Sleep(500 * time.Millisecond)
		}
	}

	w.Write([]byte("请求完成"))
}

func main() {
	http.HandleFunc("/", handler)
	fmt.Println("Server starting on :8080")
	http.ListenAndServe(":8080", nil)
}

2. 带超时的数据库查询

package main

import (
	"context"
	"database/sql"
	"fmt"
	"time"

	_ "github.com/lib/pq"
)

func queryWithTimeout(db *sql.DB, query string, timeout time.Duration) error {
	// 创建带超时的 context
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	// 使用 context 执行查询
	rows, err := db.QueryContext(ctx, query)
	if err != nil {
		return fmt.Errorf("查询失败:%w", err)
	}
	defer rows.Close()

	// 处理结果
	for rows.Next() {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
			// 处理行数据
		}
	}

	return rows.Err()
}

func main() {
	db, _ := sql.Open("postgres", "conn_string")
	
	err := queryWithTimeout(db, "SELECT * FROM users", 5*time.Second)
	if err != nil {
		fmt.Println("查询错误:", err)
	}
}

3. 并发任务管理

package main

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

// Worker 执行任务
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:
			fmt.Printf("Worker %d 工作中...\n", id)
			time.Sleep(1 * time.Second)
		}
	}
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	var wg sync.WaitGroup

	// 启动 5 个 worker
	for i := 1; i <= 5; i++ {
		wg.Add(1)
		go Worker(ctx, i, &wg)
	}

	// 运行 3 秒后取消
	time.Sleep(3 * time.Second)
	fmt.Println("取消所有任务")
	cancel()

	// 等待所有 worker 完成
	wg.Wait()
	fmt.Println("所有任务完成")
}

4. 多级取消(父子 Context)

package main

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

func main() {
	// 创建根 context
	rootCtx, rootCancel := context.WithCancel(context.Background())
	defer rootCancel()

	// 创建子 context
	childCtx, childCancel := context.WithCancel(rootCtx)
	defer childCancel()

	// 启动 goroutine 监听根 context
	go func() {
		<-rootCtx.Done()
		fmt.Println("根 context 取消")
	}()

	// 启动 goroutine 监听子 context
	go func() {
		<-childCtx.Done()
		fmt.Println("子 context 取消")
	}()

	// 取消子 context
	time.Sleep(1 * time.Second)
	childCancel()

	time.Sleep(1 * time.Second)

	// 取消根 context(会传播到所有子 context)
	rootCancel()

	time.Sleep(1 * time.Second)
}

5. 管道和 context 组合

package main

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

// 生成器
func generator(ctx context.Context, out chan<- int) {
	defer close(out)

	i := 0
	for {
		select {
		case <-ctx.Done():
			fmt.Println("生成器收到取消信号")
			return
		case out <- i:
			i++
			time.Sleep(100 * time.Millisecond)
		}
	}
}

// 处理器
func processor(ctx context.Context, in <-chan int) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("处理器收到取消信号")
			return
		case v, ok := <-in:
			if !ok {
				return
			}
			fmt.Printf("处理:%d\n", v)
		}
	}
}

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

	ch := make(chan int)

	// 启动生成器
	go generator(ctx, ch)

	// 启动处理器
	go processor(ctx, ch)

	// 等待完成
	<-ctx.Done()
	time.Sleep(500 * time.Millisecond)
}

6. 重试机制

package main

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

// 带重试的操作
func doWithRetry(ctx context.Context, operation func(context.Context) error, maxRetries int) error {
	var lastErr error

	for i := 0; i < maxRetries; i++ {
		// 创建带超时的子 context
		retryCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
		
		// 执行操作
		lastErr = operation(retryCtx)
		cancel()

		if lastErr == nil {
			return nil // 成功
		}

		// 检查是否应该放弃
		if ctx.Err() != nil {
			return ctx.Err()
		}

		fmt.Printf("重试 %d/%d: %v\n", i+1, maxRetries, lastErr)
		time.Sleep(time.Duration(i+1) * time.Second)
	}

	return lastErr
}

func main() {
	ctx := context.Background()

	// 模拟可能失败的操作
	operation := func(ctx context.Context) error {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
			// 模拟随机失败
			if time.Now().Unix()%2 == 0 {
				return fmt.Errorf("操作失败")
			}
			fmt.Println("操作成功")
			return nil
		}
	}

	err := doWithRetry(ctx, operation, 3)
	if err != nil {
		fmt.Println("最终失败:", err)
	}
}

🔹 注意事项和最佳实践

1. 必须调用 Cancel 函数

  • ⚠️ 重要:忘记调用 cancel 会导致资源泄漏
  • ✅ 始终使用 defer 调用 cancel
// 正确
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// 错误 - 会导致资源泄漏
ctx, cancel := context.WithCancel(context.Background())
// 忘记调用 cancel()

2. 不要存储 Context 到结构体

  • ⚠️ Context 应该作为函数的第一个参数传递
  • ❌ 不要将 Context 存储在结构体字段中
// 错误
type Service struct {
	ctx context.Context // 不应该这样
}

// 正确
type Service struct{}

func (s *Service) Run(ctx context.Context) {
	// 使用 ctx
}

3. 使用自定义类型作为 Value 的 Key

  • ✅ 使用自定义类型避免冲突
  • ⚠️ 不要使用 string 作为 key
// 错误 - 可能冲突
ctx = context.WithValue(ctx, "userID", "123")

// 正确 - 使用自定义类型
type contextKey string
const userIDKey contextKey = "userID"
ctx = context.WithValue(ctx, userIDKey, "123")

4. Value 仅用于请求范围的数据

  • ✅ 传递认证信息、请求 ID 等
  • ❌ 不要传递配置或选项
  • ❌ 不要传递大量数据
// 正确 - 传递请求范围的数据
ctx = context.WithValue(ctx, requestIDKey, reqID)
ctx = context.WithValue(ctx, authTokenKey, token)

// 错误 - 不应该传递配置
ctx = context.WithValue(ctx, "config", config)

5. 传播取消信号

  • ✅ 子 context 会继承父 context 的取消信号
  • ✅ 取消父 context 会取消所有子 context
// 创建父子 context
parent, cancel := context.WithCancel(context.Background())
child, childCancel := context.WithCancel(parent)

// 取消 parent 会同时取消 child
cancel()

6. 并发安全

  • ✅ Context 是并发安全的
  • ✅ 可以在多个 goroutine 之间共享
ctx, cancel := context.WithCancel(context.Background())

// 多个 goroutine 可以安全使用
go worker(ctx)
go worker(ctx)
go worker(ctx)

🔹 Context 树结构

父子关系

context.Background() (根)
    ├── WithCancel
    │       ├── WithTimeout
    │       └── WithValue
    ├── WithTimeout
    └── WithValue

取消传播

取消父 context
    ↓
所有子 context 都会被取消
    ↓
所有监听 Done 通道的 goroutine 都会收到信号

🔥 总结

核心接口

接口说明
Context上下文接口(4 个方法)

Context 方法

方法说明
Deadline()返回截止时间
Done()返回取消信号通道
Err()返回取消原因
Value()获取关联的值

核心函数

函数说明用途
WithCancel(parent)创建可取消的 context手动取消
WithDeadline(parent, d)创建带截止时间的 context定时取消
WithTimeout(parent, d)创建带超时的 context超时控制
WithValue(parent, k, v)创建带值的 context传递数据

预定义 Context

Context说明使用场景
Background()空 context,永不取消根 context
TODO()临时 context重构时使用

主要特点

  • 并发安全 👉 可在多个 goroutine 间共享
  • 取消传播 👉 父 context 取消会传播到子 context
  • 资源管理 👉 必须调用 cancel 释放资源
  • 轻量级 👉 开销很小

使用场景

  • HTTP 服务器 👉 请求处理取消
  • 数据库查询 👉 超时控制
  • 并发任务 👉 任务管理
  • RPC 调用 👉 超时和取消
  • 管道处理 👉 流式数据处理

最佳实践

  • ✅ 将 Context 作为函数的第一个参数
  • ✅ 始终调用 cancel 函数(使用 defer)
  • ✅ 使用自定义类型作为 Value 的 key
  • ✅ 仅传递请求范围的数据
  • ✅ 不要存储 Context 到结构体
  • ⚠️ 注意:Value 不应传递配置

错误处理

// 检查取消原因
select {
case <-ctx.Done():
    if ctx.Err() == context.Canceled {
        fmt.Println("已取消")
    } else if ctx.Err() == context.DeadlineExceeded {
        fmt.Println("已超时")
    }
}

常见错误

  • ❌ 忘记调用 cancel 函数
  • ❌ 将 Context 存储到结构体
  • ❌ 使用 string 作为 Value 的 key
  • ❌ 滥用 Value 传递配置
  • ❌ 不传播 Context

context 包是 Go 并发编程的核心组件,用于管理 goroutine 的生命周期和传递请求范围的数据!