select监听ctx.Done()是唯一可靠退出路径:Go中goroutine只能自行响应信号后return,轮询ctx.Err()会导致忙等和延迟,必须用select阻塞等待ctx.Done()。

select监听ctx.Done()是唯一可靠退出路径
Go 里没有“杀掉 goroutine”这回事,所有退出必须由 goroutine 自己响应信号后 return。轮询 ctx.Err() != nil 是常见错误——它会忙等、浪费 CPU、延迟响应取消。真正该用的是 select 阻塞等待 ctx.Done(),这是官方唯一推荐的协作式退出方式。
关键点:
-
select中的case 分支会在 context 被 cancel 或超时后立即就绪,不占 CPU - 不能把
if ctx.Err() != nil { return }放在 for 循环开头又不 sleep,否则变成死循环 - 若 goroutine 内部有阻塞调用(如
http.Client.Do、db.Query),必须透传 context,例如用db.QueryContext(ctx, ...),否则阻塞点根本收不到信号
HTTP Server Shutdown 必须配超时 context
http.Server.Shutdown() 本身不阻塞,它只是发通知然后立刻返回。如果你不包一层带超时的 context,调用后程序可能卡住、永远等不到实际关闭完成。
正确写法:
立即学习“go语言免费学习笔记(深入)”;
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := srv.Shutdown(ctx)
if err != nil && err != http.ErrServerClosed {
log.Printf("shutdown error: %v", err)
}注意:http.ErrServerClosed 是正常返回值,不是错误,要显式忽略;也绝对不要在 Shutdown() 前调 srv.Close(),后者会强制断开连接,让“优雅”失效。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
定时任务中 ticker 和 context 要配合使用
用 time.Ticker 做周期任务时,如果只靠 for range ticker.C,cancel 信号无法中断当前 tick 的等待。必须把 ticker.C 和 ctx.Done() 一起放进 select。
示例结构:
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
<p>for {
select {
case <-ctx.Done():
return // 立即退出
case <-ticker.C:
doWork()
}
}常见坑:
- 忘记
defer ticker.Stop(),导致 ticker 泄漏 - 用
for range ticker.C+ 单独检查ctx.Err(),仍会多执行一次 tick 后才退出 - 在 handler 或 worker 中启动 ticker 但没把 ctx 传进去,导致无法统一控制生命周期
多 goroutine 协同退出时,WaitGroup 和共享 cancel 缺一不可
单个 HTTP server 关了,不代表数据库连接、后台消费者、定时 worker 也安全退出。它们之间有依赖顺序,必须统一协调。
典型组合:
- 用
context.WithCancel(context.Background())创建根 context,所有子 goroutine 共享同一个ctx - 用
sync.WaitGroup记录活跃 worker 数量,在主 goroutine 中wg.Wait()确保全部退出后再做最终清理 - 收到系统信号(如
os.Interrupt)后,先调cancel()广播,再wg.Wait()等待,最后释放 DB 连接等资源 -
signal.Notify的 channel 必须带缓冲,比如make(chan os.Signal, 1),否则并发发信号可能丢一次
最易被忽略的一点:cancel 函数必须在函数返回前调用,否则 context 泄漏;而 defer cancel() 在 panic 时也可能不执行,所以更稳妥的是在明确退出路径上显式调用。

















