context.WithTimeout通过返回带截止时间的ctx和cancel函数实现超时控制,需在goroutine启动前创建、defer调用cancel,并在阻塞操作中用select监听ctx.Done()主动退出。

用 context.WithTimeout 控制任务超时最直接
Go 中没有全局“中断线程”机制,所有取消和超时都依赖 context 传递信号。最常用的是 context.WithTimeout,它返回一个带截止时间的 ctx 和 cancel 函数。
关键点:超时不是“杀掉 goroutine”,而是让接收方主动检查 ctx.Done() 并退出。若 goroutine 内部不响应,超时无效。
- 必须在启动 goroutine 前创建带超时的
ctx,不能在内部临时创建 -
cancel()应该被调用(哪怕只在 defer 中),否则可能泄漏 context - 超时时间建议用
time.Second * 5这类显式单位,避免5000这种无单位数字
示例:
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) defer cancel() // 必须 defer,防止 panic 跳过go func(ctx context.Context) { select { case <-time.After(time.Second * 5): fmt.Println("work done") case <-ctx.Done(): fmt.Println("canceled:", ctx.Err()) // context deadline exceeded } }(ctx)
用 select + ctx.Done() 响应取消信号
任何阻塞操作(如 channel 接收、HTTP 请求、数据库查询)都要配合 select 监听 ctx.Done(),否则无法及时退出。
立即学习“go语言免费学习笔记(深入)”;
常见错误是只在循环开头检查一次 ctx.Err(),但实际阻塞在 上就卡死了。
- HTTP 客户端必须传入带 cancel 的
ctx:http.NewRequestWithContext(ctx, ...) - 数据库查询(如
db.QueryRowContext)也需对应 Context 方法,原生QueryRow不响应取消 - 自定义阻塞操作(如轮询)应在每次迭代前加
select { case
错误写法(无法响应取消):
for i := 0; i < 10; i++ {
if ctx.Err() != nil { return } // 只检查一次,下面 sleep 仍会执行完
time.Sleep(time.Second)
}
正确写法:
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
return
default:
}
time.Sleep(time.Second)
}
组合多个取消源:context.WithCancel + context.WithValue 不推荐
当需要手动触发取消(比如用户点击“停止”按钮),用 context.WithCancel;但不要用 context.WithValue 传取消函数——context 设计上只传只读值,且取消函数是行为,不是数据。
真正需要组合多个条件时(如“超时或用户取消”),直接用 context.WithCancel 创建父 ctx,再用 context.WithTimeout 或另一个 WithCancel 派生子 ctx,然后用 context.WithCancel 的 cancel 函数统一控制。
- 避免把
cancel函数塞进 struct 或全局变量,容易误调或漏调 - 如果要跨 goroutine 通知取消,直接传
ctx,接收方调用cancel()是反模式 - HTTP handler 中常用
r.Context(),它已自带请求生命周期绑定的 cancel,无需额外包装
goroutine 泄漏常因忘记监听 ctx.Done() 或未关闭 channel
并发任务中,goroutine 没有退出路径是最常见的泄漏原因。典型场景:向无缓冲 channel 发送数据,但接收方已退出且未关闭 channel,发送方永久阻塞。
- 所有 channel 发送操作都应配
select+ctx.Done(),尤其是无缓冲 channel - 使用
for range ch时,确保发送方会在适当时候close(ch),否则接收方永不退出 - 第三方库(如
github.com/redis/go-redis)的 Pub/Sub 操作也需传入ctx,否则 subscribe goroutine 会长期存活
记住:Go 的并发模型里,“取消”永远是协作式的,没有强制终止。你写的每一条阻塞语句,都得自己负责让它可中断。










