不能直接用 time.Sleep 模拟耗时计算来支持取消,因为它是阻塞调用、无法响应中断;真实耗时操作必须主动轮询 ctx.Err(),在循环或递归中插入检查点,且 context 必须显式传递至所有子函数。

为什么不能直接用 time.Sleep 模拟耗时计算来支持取消?
因为 time.Sleep 是阻塞调用,无法响应外部中断;一旦开始就只能等完。真实耗时计算(比如遍历大 slice、解密、网络请求)必须主动检查是否被取消,否则 context.Context 的 Done() 信号根本进不来。
常见错误是只在函数开头检查 ctx.Err(),但没在循环或长耗时步骤中反复轮询——结果还是卡死。
- 耗时操作必须拆成可中断的片段(如每次处理一批数据后检查
ctx.Err()) - 避免在单次
for循环里做不可分割的百万次运算 - IO 类操作(如
http.Client.Do)天然支持context,但纯 CPU 计算必须手动插入检查点
如何正确传入并使用 context.Context 参数?
函数签名必须显式接收 ctx context.Context,且所有下游调用(包括自定义子函数)都要传递它。不能只在顶层检查,子函数也要自己判断。
示例:一个模拟“计算斐波那契第 n 项”的耗时函数,每递归一层或每迭代一次都检查 ctx.Err():
func fib(ctx context.Context, n int) (int, error) {
if n <= 1 {
return n, nil
}
a, b := 0, 1
for i := 2; i <= n; i++ {
select {
case <-ctx.Done():
return 0, ctx.Err()
default:
}
a, b = b, a+b
}
return b, nil
}
- 用
select+case 是标准写法,比 <code>if ctx.Err() != nil更安全(避免竞态) - 不要在
default分支里放耗时逻辑,否则可能跳过检查 - 如果函数内部启动 goroutine,必须把
ctx传进去,且 goroutine 退出前要清理资源
取消后如何保证资源不泄漏?
取消不是终点,而是清理起点。比如打开文件、持有锁、启动 goroutine 后,必须确保无论是否被取消,都能释放。
典型陷阱:在 defer 里关闭文件,但函数因取消提前返回,defer 还没触发——此时要用 context.AfterFunc 或显式 cleanup。
- 推荐模式:在函数开头创建 cleanup 函数,用
defer注册,再在取消分支里立即调用 - 对 channel 操作,取消时记得 close 它(如果设计为可关闭),否则接收方可能永久阻塞
- goroutine 中使用
ctx,但别忘了用sync.WaitGroup等待其退出,否则 cancel 后 goroutine 还在跑
测试取消逻辑时最容易漏掉什么?
本地跑一次 Cancel 看是否返回 context.Canceled 不够——得验证它真正在耗时中途退出,而不是等完才响应。
关键点:必须让取消发生在耗时操作进行中,比如用 time.AfterFunc 在循环中间触发 cancel()。
- 测试时用
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)不可靠,因为实际耗时可能远小于 10ms - 更稳妥:用
ctx, cancel := context.WithCancel(context.Background()),然后在 goroutine 里 sleep 后调用cancel() - 断言错误类型必须用
errors.Is(err, context.Canceled),而不是err == context.Canceled(因为可能包装过)
真正难的不是加 ctx,是在每个可能卡住的地方埋检查点,并让它们协同响应同一个取消信号。漏掉任意一处,整个链路就失效。


















