守护协程必须使用独立创建的可取消context,而非复用http请求context,以避免意外终止或goroutine泄漏;应通过context.withcancel(context.background())显式管理生命周期,启动时传入协程,关闭时调用cancel,并避免在协程内defer cancel或长期持有context引用。

守护协程为什么不能直接用父 Context
守护协程(如定期健康检查、指标上报、日志 flush)通常长期运行,不绑定单次请求生命周期。如果直接复用 HTTP handler 的 r.Context(),会导致:父请求结束时 ctx.Done() 关闭,守护协程被意外终止;或更糟——忘记 cancel,造成 goroutine 泄漏和内存堆积。
用 context.WithCancel + 显式生命周期管理
守护协程需要自己掌控启停,不能依赖外部请求上下文。正确做法是独立创建可取消的 Context,并暴露 cancel 函数供外部控制:
- 启动时调用
context.WithCancel(context.Background()),得到新ctx和cancel - 把
ctx传给守护协程函数,cancel存为包级变量或注入到管理器结构体中 - 服务关闭时显式调用
cancel(),确保所有守护协程收到信号并退出 - 切忌在守护协程里调用
defer cancel()—— 这会让它一启动就立刻取消
示例:
var healthCancel context.CancelFunc
<p>func startHealthCheck() {
ctx, cancel := context.WithCancel(context.Background())
healthCancel = cancel
go func(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case </p><p>func shutdown() {
if healthCancel != nil {
healthCancel()
}
}
</p>
避免把守护 Context 塞进 struct 长期持有
如果把 ctx 直接存进某个 struct 字段(如 type Monitor struct { ctx context.Context }),容易踩两个坑:
- struct 实例可能被复用,但 Context 已取消,后续调用
ctx.Done()返回已关闭 channel,导致逻辑提前退出 - 若该 struct 被 GC 延迟回收,内部持有的
ctx可能阻止其关联的 timer 或 goroutine 释放(尤其timerCtx)
正确方式:只在启动协程时按需传入,不保存引用;取消逻辑由上层统一触发。
要不要加 timeout 或 deadline
一般不需要。守护协程本意就是长期存活,加 WithTimeout 或 WithDeadline 会违背设计初衷。唯一例外是初始化阶段的“等待就绪”逻辑(比如等数据库连接池建连),此时可用带超时的子 Context,但必须与主守护 Context 分离:
- 初始化块内用
context.WithTimeout(parentCtx, 5*time.Second) - 初始化成功后,切换回无超时的主
ctx运行循环 - 绝不让初始化用的 timeout ctx 泄露到主循环中
最易忽略的是:守护协程里调用的第三方库(如 prometheus pusher、opentelemetry exporter)内部可能依赖 Context,务必确认它们接收的是你主动传入的、生命周期可控的 Context,而非误用了全局或请求级 Context。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











