第27课:Context 与取消传播
学习目标
- 理解 Go 语言中
context包的核心作用:管理 goroutine 的生命周期、传递取消信号和截止时间。 - 掌握创建和使用
Context的几种基本方式(Background,WithCancel,WithTimeout,WithDeadline)。 - 学会在一组相关的 goroutine 中传播取消信号,实现优雅退出。
- 了解如何在
Context中存储和传递请求范围的元数据。
核心概念
想象你正在指挥一场音乐会,你给每位乐手(goroutine)发了一份乐谱。现在,你突然需要立即停止演奏。一种方式是跑去一个个告诉乐手“停下!”,这很慢且不优雅。更好的方式是拥有一个中央广播系统(Context)。你只需按下一个按钮(调用 cancel()),所有正在听广播的乐手都会同时收到停止信号。
在 Go 的并发编程中,context 包就是这个“中央广播系统”。它有两个核心任务:
- 控制生命周期:通过
Done()通道发送关闭信号,通知所有正在运行的 goroutine 停止工作并清理资源。 - 传递请求范围的值:比如请求 ID、用户认证信息等,这些值可以沿着调用链在不同的 goroutine 间传递。
Context 是一个只读的接口(context.Context),通常作为函数的第一个参数显式传递,其命名约定为 ctx。
代码示例
示例1:使用 WithCancel 进行手动取消
package main
import (
"context"
"fmt"
"time"
)
// worker 是一个执行具体任务的函数,它会监听 ctx 的取消信号
func worker(ctx context.Context, name string) {
for {
select {
case <-ctx.Done(): // 监听取消信号
fmt.Printf("%s 收到取消信号,原因: %v\n", name, ctx.Err())
return
default:
fmt.Printf("%s 正在工作中...\n", name)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
// 创建一个带有取消功能的 context
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx, "Worker-A")
go worker(ctx, "Worker-B")
time.Sleep(2 * time.Second) // 让 worker 工作2秒
fmt.Println("主程序发出取消信号")
cancel() // 发送取消信号,所有使用此 ctx 的 goroutine 都会收到
time.Sleep(1 * time.Second) // 等待 goroutine 打印退出信息
fmt.Println("程序结束")
}
输出示例:
Worker-A 正在工作中...
Worker-B 正在工作中...
... (中间会有多次输出) ...
主程序发出取消信号
Worker-A 收到取消信号,原因: context canceled
Worker-B 收到取消信号,原因: context canceled
程序结束
示例2:使用 WithTimeout 设置超时
package main
import (
"context"
"fmt"
"time"
)
func slowOperation(ctx context.Context) error {
// 模拟一个需要长时间完成的任务
time.Sleep(3 * time.Second)
return nil
}
func main() {
// 创建一个1秒后自动取消的 context
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() // 在函数退出前调用 cancel 是一个好习惯,即使超时也会触发
err := slowOperation(ctx)
if err != nil {
fmt.Println("操作出错:", err)
}
// 检查 context 是否因超时而取消
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("操作因超时而被取消")
} else {
fmt.Println("操作完成")
}
}
输出:
操作因超时而被取消
示例3:在 HTTP 服务器中使用 Context
这是 context 最常见的应用场景之一。
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func handler(w http.ResponseWriter, r *http.Request) {
// 从请求中获取 context,它会在客户端断开时自动取消
ctx := r.Context()
fmt.Println("handler 开始")
// 模拟一个耗时的操作,比如数据库查询
select {
case <-time.After(3 * time.Second):
// 操作完成
fmt.Fprintln(w, "请求处理成功")
case <-ctx.Done():
// 客户端提前断开连接
err := ctx.Err()
fmt.Println("handler 因客户端取消而中断:", err)
http.Error(w, "请求被取消", http.StatusRequestTimeout)
}
fmt.Println("handler 结束")
}
func main() {
http.HandleFunc("/", handler)
fmt.Println("服务器启动于 :8080")
http.ListenAndServe(":8080", nil)
}
测试方法:启动服务器后,使用 curl --max-time 2 http://localhost:8080 命令,2秒后会因客户端超时而中断。
实践练习
练习1:基本的 Context 取消
要求:创建一个 WithCancel context,并启动3个 goroutine。每个 goroutine 模拟不同的工作(打印不同的计数)。在主函数中,等待1秒后调用 cancel(),确保所有 goroutine 都能优雅退出并打印退出信息。
预期输出:三个 goroutine 打印若干次计数后,都打印出 “收到取消信号,退出”。
练习2:实现一个带超时的缓存查询
要求:编写一个函数 getFromCache(ctx context.Context, key string) (string, error)。该函数模拟从缓存中查询数据,正常需要2秒。使用 WithTimeout 为该操作设置1秒的超时。如果超时,应返回 context.DeadlineExceeded 错误;如果手动取消,则返回 context.Canceled 错误。
预期输出:调用函数后,1秒内应收到超时错误。
练习3:组合 Context
要求:基于练习2的 getFromCache 函数。现在创建一个“父” context(带3秒超时),然后基于这个父 context 再创建一个“子” context(带1秒超时)。调用 getFromCache 时使用这个子 context。观察并解释当子 context 超时后,父 context 是否也会被取消?为什么?
提示:子 context 的取消会导致父 context 的 Done() 通道关闭吗?
常见错误
- 忽略
ctx.Done()的检查:在 for 循环或 select 语句中,不监听ctx.Done(),导致 goroutine 无法响应取消信号,造成资源泄漏。 - 在 goroutine 内部调用
cancel():cancel函数应由创建 context 的那个函数(通常是调用链的上层)负责调用,并通过defer cancel()确保执行。在子 goroutine 中调用会过早取消整个任务。 - 将 Context 存储在结构体中:
Context应作为函数参数显式传递(通常是第一个参数),而不是作为结构体字段。这使它的生命周期和范围更清晰。 - 传递
nilContext:如果不确定用什么 Context,应传递context.TODO(),而不是nil,以避免运行时 panic。
小结
- Context 的核心作用:协调多个 goroutine 的取消和超时,是 Go 并发控制的“遥控器”。
- 关键函数:
Background():创建根 Context。WithCancel(parent):创建可手动取消的子 Context。WithTimeout(parent, timeout):创建指定超时后自动取消的子 Context。WithDeadline(parent, time):创建指定截止时间后自动取消的子 Context。
- 使用模式:
- 创建 Context(通常是
Background或从*http.Request获取)。 - 将
ctx作为函数的第一个参数传递。 - 在函数内部,通过
select监听ctx.Done()通道来响应取消信号。 - 使用
ctx.Err()来获取取消的原因(Canceled或DeadlineExceeded)。
- 创建 Context(通常是
- 最佳实践:尽早检查
ctx.Done(),使用defer cancel(),并始终处理ctx.Err()返回的错误。