signal.notify必须显式调用才能捕获sigint/sigterm,否则进程被直接终止;监听须在主业务启动前完成,且应同时监听syscall.sigint和syscall.sigterm;通道需带缓冲并持续读取,避免信号丢失。

signal.Notify 必须显式调用,否则信号直接杀进程
Go 不会自动拦截 SIGINT 或 SIGTERM,不调 signal.Notify 就等于没装保险丝——kill -15 $PID 或 Ctrl+C 会立刻终止程序,清理逻辑一概执行不到。
- 监听必须在主业务启动前完成,比如
http.Serve或长期for循环之前;否则信号可能在监听注册前就已到达,被内核静默丢弃 - 别只监听
os.Interrupt(即SIGINT),漏掉syscall.SIGTERM是生产环境高频翻车点——Kubernetes、systemd 发的都是SIGTERM -
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)比signal.Notify(c)更安全:后者默认监听所有可捕获信号(含SIGUSR1等),语义模糊且易引发意外行为
通道必须带缓冲,且要持续读,否则第二次信号就丢了
用 make(chan os.Signal, 1) 创建带缓冲通道是底线;但更关键的是——goroutine 不能只 一次就退出。否则后续信号写入时缓冲区已满,Go 运行时会直接丢弃,毫无提示。
- 错误写法:
go func() { sig := → 只处理第一个信号 - 正确写法:用
for循环持续消费,例如for { sig := - 缓冲大小设为 1 足够:信号是异步事件,不是流数据;设更大反而掩盖设计缺陷(比如本该阻塞等待清理完成,却因缓冲“吞掉”信号而误判)
优雅退出 ≠ 立即退出,Shutdown 要配超时和依赖治理
http.Server.Shutdown 不是关开关,而是发通知+等收尾。如果数据库查询卡住、HTTP client 没设 timeout、后台 time.Ticker goroutine 没监听 ctx.Done(),它就会超时失败,最终强制关闭连接,导致 500 或事务中断。
-
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)—— 超时建议 10–30 秒,太短易误杀,太长拖慢发布 -
db.SetMaxOpenConns(1)+db.PingContext(ctx)再db.Close(),比裸调db.Close()安全得多 - 所有外部调用(DB、HTTP、RPC)必须显式设 timeout,不能依赖默认值;否则 Shutdown 一定等不到它们
常见信号怎么分发:SIGTERM/SIGINT 做退出,SIGHUP 做热重载
一个通道可以监听多个信号,但处理逻辑要按语义拆开:终止类信号(SIGTERM、SIGINT)走关机流程,配置类信号(SIGHUP、SIGUSR1)走重载流程,混在一起会逻辑耦合、难测试。
- 推荐用
switch sig分支,而不是 if/else 堆砌;case syscall.SIGHUP:里 reload config,case syscall.SIGTERM:里触发Shutdown -
SIGHUP在容器环境常被忽略,需确认运行时是否透传(如 Docker 默认不转发SIGHUP,得加--sig-proxy) -
SIGKILL(9)和SIGSTOP(19)无法捕获或忽略,任何试图监听它们的代码都无效,删掉省得误导
信号处理真正的难点不在接收,而在“响应之后的等待”——你永远不知道哪个 goroutine 忘了 select ctx.Done(),或者哪个第三方库的 timeout 漏设了。上线前务必用 kill -15 + 日志埋点验证整条链路是否真能走到 db.Close() 那一行。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











