context.afterfunc 是当 context 被取消时在新 goroutine 中触发回调的机制;它弥补 defer 无法响应中途取消的缺陷,如 http 连接断开时 defer 仍不执行。

context.AfterFunc 是什么,为什么不能直接用 defer
context.AfterFunc 不是“延迟执行”,而是“当 Context 被取消时,在新 goroutine 中触发回调”。它解决的是:你没法在 defer 里响应 ctx.Done(),因为 defer 只在函数返回时跑,而取消可能发生在函数中间任意时刻——比如 HTTP 连接断开、上游 timeout 已触发,但你的业务逻辑还没走到末尾。
常见错误是写成这样:
func handle(ctx context.Context) {
conn := openDBConn()
defer conn.Close() // ❌ 取消时不会触发
<p>这会导致连接泄漏。真正需要的是:一旦 <code>ctx</code> 取消,立刻关连接,不管当前执行到哪。</p><h3>AfterFunc 的 stop() 返回值到底代表什么</h3><p><code>stop()</code> 返回 <code>true</code> 表示“成功阻止回调执行”;返回 <code>false</code> 并不意味着“回调已执行完”,只说明“它可能已启动、也可能刚被调度、也可能已经结束”。它不是同步等待,也不提供完成通知。</p>
- 回调本身在独立 goroutine 执行,和主流程无同步关系
- 多次调用
stop()是安全的,但只有第一次可能返回true - 如果清理逻辑会修改共享状态(如关闭文件、释放锁),必须自己加互斥或判断是否已清理过
典型误用:
<pre class="brush:php;toolbar:false;">stop := context.AfterFunc(ctx, func() { os.Remove(tmpFile) })
if stop() { /* 认为文件已删 */ } // ❌ 错!stop() == true 只表示没触发,不代表你该删如何让 AfterFunc 真正“收敛”到业务语义
要等清理完成、或确保清理只执行一次,得手动补信号机制。核心是:用 channel + defer close()
- 定义
done := make(chan struct{}),在回调里defer close(done) - 正常路径下,如果
stop()返回true,也要close(done),否则会永远阻塞 - 最后统一
等待清理实际结束
示例:
done := make(chan struct{})
stop := context.AfterFunc(ctx, func() {
defer close(done)
cleanupResources()
})
// 正常退出时尝试取消回调,并确保 done 关闭
if stop() {
close(done)
}
<h3>哪些资源适合用 AfterFunc 清理,哪些不适合</h3><p>适合:生命周期与 <code>ctx</code> 强绑定、且清理动作幂等或可并发安全的资源,比如:</p>
- 临时文件(
os.CreateTemp后注册删除) - 内存缓存项(需从 map 删除并标记失效)
- 后台心跳 goroutine(向其通知通道发 stop 信号)
不适合:
- 数据库事务(需配合
tx.Rollback()显式控制,不能靠异步回调) - HTTP 连接池中的连接(由 net/http 自行管理)
- 持有 mutex 的清理逻辑(容易死锁,因回调 goroutine 和主 goroutine 可能同时抢锁)
关键点:AfterFunc 不是万能钩子,它只解决“取消时机不可控 + 清理动作轻量”的场景。重逻辑、有状态依赖、需精确顺序的,仍得走显式 cancel 链或封装成可 await 的结构体。











