必须配缓冲 channel,否则未读信号会被内核静默丢弃;无缓冲 channel 在未被读取时会阻塞信号投递,而 Go 运行时不保证重试,导致连续信号(如多次 kill -15)丢失。

Go 语言里处理信号不是靠“注册回调函数”,而是用 os/signal.Notify 把信号投递到 channel,再由你主动接收和响应——这个模型决定了所有优雅退出、配置重载的实现方式。
为什么 os/signal.Notify 必须配缓冲 channel?
如果 channel 没缓存,SIGTERM 到来时恰好还没执行到 ,信号就丢了。这不是偶发问题,是确定性行为。
-
make(chan os.Signal)是无缓冲的,signal.Notify(c, syscall.SIGTERM)发送信号会直接阻塞或丢弃 - 哪怕只监听一个信号,也得至少
make(chan os.Signal, 1) - 若同时监听
SIGTERM和SIGHUP,且业务逻辑可能延迟响应,建议设为2 - 别依赖
signal.Ignore去“屏蔽”信号——它不阻止内核发送,只是让 Go 运行时不转发,os.Kill仍会立刻终止进程
go-daemon 的信号处理实际发生在哪一层?
它不是在用户代码里裸调 os/signal,而是在 daemon.Daemon 的 Run() 方法内部封装了信号监听逻辑,并把 SIGTERM、SIGINT、SIGHUP 映射成结构化事件。
- 你嵌入
daemon.Daemon后,只需实现OnStop()或OnReload()方法,不用手动写Notify -
OnStop()被触发时,子进程已收到SIGTERM,但父进程还在等子进程退出完成 - 如果你在
OnStop()里调用了阻塞操作(比如等待 HTTP server 关闭超时),会导致整个守护进程 stop 命令卡住 - 注意:
go-daemon默认不监听SIGUSR1/SIGUSR2,想用它们做自定义 reload,得自己加signal.Notify并转发
如何避免 Graceful shutdown 中的常见死锁?
典型场景:HTTP server 正在处理请求,你调用 srv.Shutdown(),但它等所有连接关闭;而连接又在等你的数据库连接池释放,而连接池关不掉,因为你在等 HTTP server 先结束——环形依赖。
立即学习“go语言免费学习笔记(深入)”;
- 必须设置
srv.SetKeepAlivesEnabled(false),否则长连接会让Shutdown()等得没完没了 - 数据库连接池要设
SetMaxOpenConns(0)或显式调用db.Close(),不能只靠 defer - 监听信号的 goroutine 不能被其他逻辑阻塞,建议单独起 goroutine 处理,且不要在里面做 IO
- 如果用了
context.WithTimeout控制 shutdown 时间,超时后应直接调用os.Exit(1),而不是继续等待
真正难的不是监听信号,而是判断哪些资源该在哪个时机释放、哪些状态该持久化、哪些 goroutine 必须被强制中断——这些没法靠库自动完成,得你根据业务路径一条条梳理。


















