signal.notify 不能直接用于高并发服务,因其无缓冲、易阻塞内核信号队列并丢失信号,且无法保证接收 goroutine 和退出顺序,导致 os.interrupt 仅触发一次或服务卡在 sig :=。

为什么 signal.Notify 不能直接用在高并发服务里?
因为 signal.Notify 本身不带缓冲,一旦信号频繁到达(比如压测时反复 Ctrl+C)、或接收端处理慢,就容易阻塞发送方(即内核信号队列),甚至丢失信号。更麻烦的是,它和 goroutine 调度无绑定——你无法保证哪个 goroutine 收到信号,也无法控制退出顺序。
常见错误现象:os.Interrupt 只触发一次、服务卡在 sig := 不退出、HTTP server 已关但 goroutine 还在跑。
- 必须用带缓冲的 channel,容量至少为 1(推荐 2–3),避免信号被丢弃
- 不要在多个 goroutine 中同时读同一个
signal.Notifychannel,否则竞争收信号 - 接收后应立刻关闭 channel 或设标志位,防止重复消费
如何用 signal.Notify 配合 context.WithCancel 实现可控退出?
核心思路:把信号转成 context 取消,让所有依赖该 context 的组件(HTTP server、worker pool、数据库连接等)统一响应。
示例关键片段:
sigChan := make(chan os.Signal, 2)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
ctx, cancel := context.WithCancel(context.Background())
<p>go func() {
</p><p>// 启动 HTTP server,传入 ctx
srv := &http.Server{Addr: ":8080"}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Printf("server error: %v", err)
}
}()
<!-- 其他 worker goroutine 都用 ctx 做超时/取消判断 --></p>
-
context.WithCancel是协调点,不是装饰;所有长期运行逻辑必须显式检查ctx.Done() - 不要在
defer cancel()里调用 cancel —— 它会立即触发,不是等信号 - 如果用了第三方库(如
pgxpool、redis.Conn),确认它们支持context.Context参数
HTTP server 关闭时为什么还报错 http: Server closed?
这不是错误,是 srv.ListenAndServe() 正常返回的信号。真正的问题在于:没等 handler 完全退出就结束进程,导致请求被中断、连接未 graceful close。
正确做法是先调用 srv.Shutdown(),再等待完成:
// 收到信号后
if err := srv.Shutdown(context.WithTimeout(context.Background(), 5*time.Second)); err != nil {
log.Printf("HTTP shutdown error: %v", err)
}
log.Println("HTTP server stopped")
-
srv.Close()是硬关闭,会丢请求;srv.Shutdown()才是优雅退出 -
Shutdown的 timeout 必须大于最长 handler 执行时间,否则仍可能强制中断 - 务必在
Shutdown前停止接收新连接(srv.SetKeepAlivesEnabled(false)可选,但非必需)
goroutine 泄漏怎么排查?
最典型场景:worker 启动了无限循环,但没监听 ctx.Done(),信号来了也不停。
检查点:
- 每个
go func()是否都带select { case 分支 - 是否用了
time.AfterFunc或time.Ticker却没调用Stop() - channel 发送是否可能阻塞(如无缓冲 channel + 接收方已退出)
- 用
runtime.NumGoroutine()在退出前打日志,对比启动时数量
复杂点往往不在主流程,而在定时任务、日志 flusher、metric reporter 这类“后台小线程”——它们最容易被忽略,也最难 debug。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











