「笔记007」Go语言Context初识 下
5. 高级应用场景
5.1 多协程共享 context
context
的父子关系可以很好地控制多个协程的生命周期,尤其是在复杂任务中。
示例:任务分发与统一取消
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d stopped: %v\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d is working...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
// 启动 3 个协程
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(ctx, i, &wg)
}
// 等待 2 秒后取消所有任务
time.Sleep(2 * time.Second)
cancel()
// 等待所有协程结束
wg.Wait()
fmt.Println("All workers stopped.")
}
运行结果:
Worker 1 is working...
Worker 2 is working...
Worker 3 is working...
Worker 1 is working...
Worker 2 is working...
Worker 3 is working...
Worker 1 stopped: context canceled
Worker 2 stopped: context canceled
Worker 3 stopped: context canceled
All workers stopped.
解析:
- 父
context
的取消信号会级联到所有子协程。 - 通过
sync.WaitGroup
确保协程在主程序退出前完成清理。
5.2 结合 http.Request
的上下文管理
在 Web 开发中,每个请求可以用一个 context
来管理。Go 的 net/http
包已经为每个请求默认附带了一个 context
,开发者可以使用它来设置超时或传递数据。
示例:请求超时处理
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // 获取请求的 context
fmt.Println("Handler started")
defer fmt.Println("Handler ended")
select {
case <-time.After(2 * time.Second): // 模拟任务执行
fmt.Fprintln(w, "Task completed")
case <-ctx.Done(): // 处理取消信号
fmt.Fprintln(w, "Request canceled:", ctx.Err())
}
}
func main() {
http.HandleFunc("/", handler)
server := &http.Server{
Addr: ":8080",
Handler: http.TimeoutHandler(http.DefaultServeMux, 1*time.Second, "Timeout!"),
}
fmt.Println("Server is running on port 8080")
server.ListenAndServe()
}
解析:
- 使用
http.Request
的Context
方法可以获取请求的上下文。 - 当客户端取消请求(如断开连接)或超时时,
ctx.Done()
会关闭,触发取消逻辑。
5.3 使用 context
实现超时重试
当任务失败时,可能需要重试一定次数。在这种情况下,可以通过 context
实现重试和超时控制。
示例:带超时的任务重试
package main
import (
"context"
"fmt"
"time"
)
func retryTask(ctx context.Context, attempts int, task func() error) error {
for i := 0; i < attempts; i++ {
select {
case <-ctx.Done():
return ctx.Err() // 如果超时或取消,直接返回
default:
if err := task(); err != nil {
fmt.Printf("Attempt %d failed: %v\n", i+1, err)
} else {
return nil // 成功时退出
}
}
time.Sleep(500 * time.Millisecond) // 重试间隔
}
return fmt.Errorf("all attempts failed")
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err := retryTask(ctx, 5, func() error {
fmt.Println("Performing task...")
return fmt.Errorf("task failed")
})
if err != nil {
fmt.Println("Task result:", err)
}
}
解析:
- 通过
ctx.Done()
检查任务是否被取消。 - 在重试逻辑中合理设置间隔时间,避免频繁重试。
6. 常见问题与注意事项
6.1 滥用 context
context
的设计初衷是用于传递控制信号(如取消、超时)和少量元数据,而非数据存储或函数通信工具。
反例:过度依赖 WithValue
ctx := context.WithValue(context.Background(), "userID", 12345)
// 在深层函数中读取
userID := ctx.Value("userID").(int)
问题:
- 滥用
WithValue
会导致代码耦合度高,难以维护。 - 数据传递更应使用显式参数。
建议:
- 只使用
WithValue
传递关键元数据(如请求 ID)。 - 对于大数据或复杂结构,使用专门的数据传递方式。
6.2 资源释放
当使用 context.WithCancel
或 context.WithTimeout
时,务必调用 cancel()
释放资源。即使任务提前结束,未调用 cancel()
也可能导致内存泄露。
正确做法:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
6.3 避免跨边界传递 context
context
的生命周期应与任务保持一致,不应跨越函数或模块边界。例如,不要将 context
存储为全局变量或长期依赖。
问题:
- 传递过期的
context
会导致意外的取消或不可预测行为。 context
应始终是短生命周期的工具。
7. 最佳实践
7.1 确定 context
的范围
- 短生命周期:
context
的生命周期应尽量与任务一致。 - 适度嵌套:避免过深的
context
嵌套,确保逻辑清晰。
7.2 使用自定义类型作为键
避免键冲突,推荐使用自定义类型:
type ctxKey string
const UserIDKey ctxKey = "userID"
ctx := context.WithValue(context.Background(), UserIDKey, 12345)
7.3 明确取消信号
- 在协程中始终监听
ctx.Done()
:
select {
case <-ctx.Done():
return ctx.Err()
default:
// 执行任务
}
7.4 context
不应作为函数返回值
context
的设计目的是在调用链中向下传递,而非作为返回值向上传递。
8. 结
context
是 Go 并发模型的核心工具,用于任务取消、超时控制和上下文数据传递。- 基本用法(
WithCancel
、WithTimeout
等)并设计哲学(不可变性、父子结构)。 - 遵循最佳实践,避免滥用
WithValue
和全局context
,确保资源释放和逻辑清晰。