context.WithCancel 是最常用且可控的 goroutine 退出方式,它通过协作式取消机制让协程主动监听 ctx.Done() 并安全退出,需配合 select 持续响应、避免轮询、正确处理 err 且 cancel 仅调用一次。

context.WithCancel 是最常用且可控的 goroutine 退出方式
Go 中没有直接“杀掉” goroutine 的机制,context.WithCancel 提供了一种协作式取消信号传递机制。它不强制终止协程,而是让协程主动检查 ctx.Done() 并自行退出,这是唯一被 Go 官方推荐、安全且可预测的方式。
常见错误是只调用 cancel() 却没在 goroutine 内监听 ctx.Done(),结果协程继续运行甚至泄漏;或者监听了但没处理 ctx.Err() 导致逻辑卡死。
- 必须在 goroutine 启动时传入
ctx,不能事后注入 - 监听需用
select配合,不能用ctx.Done()if ctx.Err() != nil轮询(浪费 CPU) -
cancel()只能调用一次,重复调用会 panic;建议用defer cancel()配合作用域管理
goroutine 中正确监听 context.Done() 的写法
监听不是“检查一次就完事”,而是要在可能阻塞或长期运行的路径中持续响应取消信号。典型场景包括:HTTP 请求、channel 接收、定时器等待、数据库查询等。
错误示例是把 select 放在循环外,导致只检查一次;或漏掉对 case 分支的清理逻辑(如关闭 channel、释放资源)。
func worker(ctx context.Context, jobs <-chan int) {
for {
select {
case job, ok := <-jobs:
if !ok {
return
}
process(job)
case <-ctx.Done():
// 必须在此处做清理:关闭下游 channel、释放锁、记录日志等
log.Println("worker exit due to:", ctx.Err())
return
}
}
}不要用 time.After 或 time.Sleep 替代 context 超时控制
time.After(5 * time.Second) 看似简单,但它会创建一个不可取消的 timer,即使父 context 已取消,timer 仍会触发,造成 goroutine 意外唤醒或资源滞留。真正需要的是与 context 生命周期绑定的超时行为。
正确做法是用 context.WithTimeout 或 context.WithDeadline,它们返回的 ctx 在超时后自动关闭 Done() channel,且可被上层统一取消。
-
context.WithTimeout(parent, 5*time.Second)→ 基于相对时间,适合大多数场景 -
context.WithDeadline(parent, time.Now().Add(5*time.Second))→ 基于绝对时间,适合跨系统协调 - 永远不要在
select中混用time.After和ctx.Done(),除非你明确知道 timer 不会泄露
子 goroutine 必须继承并传播 context,不能用 background 或 todo 替代
启动子协程时若传入 context.Background() 或 context.TODO(),等于切断了取消链路。上级调用 cancel() 后,子 goroutine 完全收不到信号。
正确做法是将上游传入的 ctx 显式传给每个子 goroutine,并在必要时用 context.WithValue 附加请求级数据(注意:仅限只读元信息,不要传业务对象)。
func handleRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) {
// 正确:子 goroutine 继承并传播 ctx
go processAsync(ctx, r.Body)
// 错误:断开 context 链路
// go processAsync(context.Background(), r.Body)
}
func processAsync(ctx context.Context, body io.ReadCloser) {
defer body.Close()
select {
case
最容易被忽略的是:context 取消后,goroutine 退出前的清理工作是否完整。比如未关闭的 channel、未释放的 mutex、未关闭的文件句柄,这些不会因为 ctx.Done() 触发而自动回收。每次写 case ,都要问一句——这里该关什么、该记什么、该通知谁。










