Goroutine未响应ctx.Done(),如何实现多场景优雅退出?
解决Go程序正常退出时监控goroutine未打印日志的问题
这个问题的核心原因是Go程序在main函数退出时会立即终止所有未完成的后台goroutine。当你的定时器正常触发后,main函数的select分支执行完毕,紧接着就会进入退出流程,执行defer函数里的cancel()——但此时main函数马上就要结束了,Go runtime不会等待监控goroutine完成后续的打印操作,导致"Stopped monitoring"还没来得及输出,程序就终止了。
解决方案:用sync.WaitGroup等待goroutine完成
我们可以通过sync.WaitGroup来让main函数等待监控goroutine执行完所有退出逻辑后再退出,同时调整cancel()的调用时机,确保信号能被及时处理。
修改后的完整代码如下:
package main import ( "context" "fmt" "os" "os/signal" "sync" "time" ) func main() { ctx := context.Background() ctx, cancel := context.WithCancel(ctx) // 确保cancel最终会被调用,避免context泄漏 defer cancel() c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) defer signal.Stop(c) var wg sync.WaitGroup wg.Add(1) // 注册一个等待任务 go func() { defer wg.Done() // goroutine结束时标记任务完成 select { case <-c: fmt.Println("Got interrupt signal") cancel() case <-ctx.Done(): } fmt.Println("Stopped monitoring") }() select { case <-ctx.Done(): fmt.Println("notified to quit") case <-time.NewTimer(time.Second * 2).C: fmt.Println("done something") } // 主动触发context取消,通知监控goroutine退出 cancel() // 等待监控goroutine完成所有操作(包括打印日志) wg.Wait() fmt.Println("Cleaned up") }
关键修改点说明:
- 添加
sync.WaitGroup:通过wg.Add(1)注册等待任务,监控goroutine退出前调用wg.Done(),main函数最后用wg.Wait()阻塞,直到goroutine完成。 - 调整
cancel()和清理逻辑的顺序:将Cleaned up的打印移到wg.Wait()之后,确保监控goroutine的日志先输出。 - 拆分
defer逻辑:原来的单个defer函数拆分成多个,让职责更清晰,同时确保signal.Stop(c)和cancel()都能被执行。
验证输出
- 正常退出(定时器触发):
done something Stopped monitoring Cleaned up - Ctrl+C中断退出:
Got interrupt signal Stopped monitoring notified to quit Cleaned up
这样两种场景下的输出都符合你的预期了。
内容的提问来源于stack exchange,提问作者Ron
相关产品推荐
相关产品推荐

