Context使用

130 阅读3分钟

在并发编程中,goroutine 是 Go 语言的核心特性之一。为了管理多个 goroutine 的生命周期,以及实现高效的协作和退出机制,可以结合 contextchanneldefer 等工具来组织代码。这篇讲解如何通过 context 管理 goroutine,以及在日志中追踪请求的 traceID

请求流程中的 traceID 记录与追踪

在处理客户端请求时,每个请求通常会分配一个唯一的标识符 traceID,用于追踪整个请求的生命周期。这个 traceID 可通过 context 传递到各个 goroutine,确保每个协程都能标识出属于自己的请求。

实现方法:

  1. 创建一个 context,通过 context.WithValue 传入 traceID
  2. goroutine 中从 context 中提取 traceID,记录日志。
  3. 分析日志时,可以通过搜索相同的 traceID,追踪请求的完整流程。
func handleRequest(ctx context.Context) {
	traceID := ctx.Value("traceID").(string)
	fmt.Println("Handling request with traceID:", traceID)
}

func main() {
	ctx := context.WithValue(context.Background(), "traceID", "123456")
	go handleRequest(ctx)

	time.Sleep(time.Second) // 模拟主线程阻塞
}

确保操作在函数结束时执行:defer

defer 是 Go 的关键字,用于确保某些操作在函数退出时执行,常用于清理资源或关闭连接。例如,当我们使用 context.WithCancel 创建上下文时,通常会通过 defer cancel() 确保在函数结束时调用 cancel,避免资源泄露。

func process(ctx context.Context) {
	defer fmt.Println("Process ended.")
	// 其他业务逻辑
}

func main() {
	process(context.Background())
}

输出中会始终看到 "Process ended.",即使函数中途返回。


优雅退出 goroutine

在并发场景下,如何让一个长时间运行的 goroutine 在需要时优雅退出,是一个常见问题。以下是三种常见实现方式:

1. 使用全局变量

通过修改全局变量的值来通知 goroutine 退出。

var exit bool

func worker() {
	defer fmt.Println("Worker exited.")
	for {
		if exit {
			break
		}
		fmt.Println("Working...")
		time.Sleep(time.Second)
	}
}

func main() {
	go worker()
	time.Sleep(5 * time.Second)
	exit = true
	time.Sleep(1 * time.Second)
}

缺点:全局变量缺乏灵活性,不适用于复杂场景。

2. 使用 channel

channel 是 Go 的核心并发机制,可以用于协程之间的通信和退出信号的传递。使用一个 channel 通知 goroutine 停止工作。

go
复制代码
func worker(stopCh <-chan struct{}) {
	for {
		select {
		case <-stopCh:
			fmt.Println("Worker stopped.")
			return
		default:
			fmt.Println("Working...")
			time.Sleep(time.Second)
		}
	}
}

func main() {
	stopCh := make(chan struct{})
	go worker(stopCh)

	time.Sleep(5 * time.Second)
	close(stopCh) // 通知协程退出
	time.Sleep(1 * time.Second)
}

channel 通信更安全,避免了全局变量的竞争问题。

3. 使用 context.Context

context 是 Go 标准库中设计用于跨协程的上下文管理工具,特别适合用来控制多个 goroutine 的生命周期。

  • 核心接口:

    • Done():返回一个通道,表示上下文被取消。
    • WithCancel:创建可取消的上下文。
    • WithDeadlineWithTimeout:设置超时时间。
  • 使用 context.WithCancel

    func worker(ctx context.Context) {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("Worker stopped:", ctx.Err())
                return
            default:
                fmt.Println("Working...")
                time.Sleep(time.Second)
            }
        }
    }
    
    func main() {
        ctx, cancel := context.WithCancel(context.Background())
        go worker(ctx)
    
        time.Sleep(5 * time.Second)
        cancel() // 通知协程退出
        time.Sleep(1 * time.Second)
    }
    

4. 使用 context.WithDeadline

```
func main() {
    deadline := time.Now().Add(3 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()

    select {
    case <-time.After(5 * time.Second):
        fmt.Println("Operation completed.")
    case <-ctx.Done():
        fmt.Println("Timeout:", ctx.Err())
    }
}
```

context 提供了一个高效的协程管理方法,避免了全局变量的复杂性,并能统一管理多个 goroutine

总结

  1. traceID 管理:

    • 利用 context 传递 traceID,在日志中实现请求的全程追踪。
  2. 协程退出管理:

    • 简单场景可用全局变量。
    • 中小型系统中推荐使用 channel
    • 更复杂的场景建议使用 context,结合 WithCancelWithDeadline,统一管理生命周期,避免资源泄露。
  3. defer

    • 确保资源释放,适用于关闭文件、释放锁、取消上下文等场景。

通过这些工具的结合,Go 语言能够以极高的效率和优雅的方式管理并发请求,既提高了性能,也增强了代码的可维护性。