直接用 router.run() 无法终止后台 goroutine,因其是阻塞调用且不暴露 *http.server 实例,无法注入 cancel 信号;quit channel 必须是跨请求共享的单例,启动时只传一次,停止时需 close 而非 send。

为什么直接用 router.Run() 无法终止后台 goroutine
因为 router.Run() 是阻塞调用,内部封装了 http.ListenAndServe(),它不暴露 *http.Server 实例,也没给你机会注入 cancel 信号。你发 SIGINT,进程直接退出,所有正在运行的 goroutine(比如定时任务、消息消费、日志上传)全被粗暴中断——没清理、没 flush、没 close,连接泄漏、数据丢失、Redis 连接卡死都是常态。
quit channel 必须是跨请求共享的单例
常见错误是每次 HTTP 请求都新建一个 quit channel,比如在 handler 里写 quit := make(chan struct{}),然后传给 goroutine。这样“start”和“stop”操作根本不是同一个 channel,goroutine 永远收不到退出信号。
- 把
quit定义为包级变量或结构体字段,确保 start/stop 能访问同一实例 - 启动 goroutine 时只传一次
quit,不要复制或重造 - 停止逻辑必须 close(
quit),而不是 send,否则可能阻塞(尤其无缓冲 channel) - goroutine 内部用
select { case 响应,别轮询 <code>quit != nil
goroutine 里用了 time.Ticker 怎么安全停
time.Ticker 不会自动响应 context 取消,必须显式 stop,否则 ticker goroutine 永远存在,导致主进程关不掉。
- 启动时保存
ticker := time.NewTicker(...),并 deferticker.Stop()(但注意:不能在 goroutine 外部 defer,得在 goroutine 内部) - 在 select 中同时监听
ticker.C和quit,收到 quit 后立即 break 并调ticker.Stop() - 别用
for range ticker.C—— 这种写法无法提前退出 - 示例片段:
for { select { case
DB/Redis 客户端、HTTP client 都要配合 quit 或 ctx
光关掉主 goroutine 没用,底层资源不释放,Shutdown() 就算完成,进程仍卡在 GC 等连接回收上。
-
sql.DB必须调db.Close(),它会等所有活跃连接归还;别依赖 GC -
redis.Client同样要client.Close(),否则 net.Conn 不关闭 - 如果用了
http.DefaultClient.Do(),得换成req.WithContext(ctx),且 ctx 要从quit构建(如ctx, _ := context.WithCancel(context.Background()),close 时cancel()) - 所有自定义 long-running goroutine(如上报 metrics、心跳)都要监听同一
quit或其衍生 ctx
最容易被忽略的是:channel 关了、server shutdown 了、日志也 flush 了,但某个 goroutine 里忘了 defer redisClient.Close(),整个进程就 hang 在那里,ps 看着像死了,其实还在等 TCP 连接超时。











