Go语言优雅关闭Worker:Ctrl+C无法终止程序的bug排查
我参考相关示例编写了如下Go代码,用于实现Worker优雅关闭功能,但按下Ctrl+C(SIGINT)时程序完全无法终止。请问代码中的bug是什么?
package main import ( "context" "fmt" "log" "net/http" "os" "os/signal" "sync" "syscall" "time" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) var wg sync.WaitGroup go func() { signals := make(chan os.Signal, 1) signal.Notify(signals, os.Interrupt, syscall.SIGTERM) <-signals cancel() }() wg.Add(1) go func() { if err := myWorker(ctx); err != nil { cancel() } wg.Done() }() wg.Add(1) go func() { if err := startServer(ctx); err != nil { cancel() } wg.Done() }() wg.Wait() } func myWorker(ctx context.Context) error { shouldStop := false go func() { <-ctx.Done() shouldStop = true }() for !shouldStop { for { fmt.Println("https://gosamples.dev is the best") time.Sleep(1 * time.Second) } } return nil } func startServer(ctx context.Context) error { var srv http.Server go func() { <-ctx.Done() // Wait for the context to be done // Shutdown the server if err := srv.Shutdown(context.Background()); err != nil { // Error from closing listeners, or context timeout: log.Printf("HTTP server Shutdown: %v", err) } }() if err := srv.ListenAndServe(); err != http.ErrServerClosed { // Error starting or closing listener: return fmt.Errorf("HTTP server ListenAndServe: %w", err) } return nil }
代码中的核心Bug分析
myWorker内层无限循环无退出条件:myWorker函数外层循环虽然检查shouldStop变量,但内层嵌套了一个完全没有退出逻辑的无限for循环。即便ctx被取消、shouldStop被设为true,内层循环也会一直执行打印和睡眠操作,永远不会退出,导致整个myWorkergoroutine无法结束,sync.WaitGroup永远无法完成等待,程序也就无法终止。shouldStop变量缺乏同步机制(次要问题):shouldStop是普通布尔变量,在goroutine中赋值后,主循环读取时可能存在内存可见性问题(不过这个问题被内层死循环完全掩盖)。正确做法是使用sync.atomic原子变量,或者直接在循环中检查ctx.Done(),避免额外的布尔变量。startServer的潜在问题:
初始化的http.Server未设置Addr字段,调用ListenAndServe时会默认使用:http端口,若端口无法绑定(如权限不足)会直接返回错误,但这不是程序无法终止的主要原因。另外,调用srv.Shutdown时传入context.Background(),应该使用带超时的context,避免关闭操作无限阻塞。
修复建议
修改myWorker函数,移除内层死循环,直接在主循环中检查ctx状态:
func myWorker(ctx context.Context) error { for { select { case <-ctx.Done(): return ctx.Err() default: fmt.Println("https://gosamples.dev is the best") time.Sleep(1 * time.Second) } } }
这样当ctx被取消时,select会立刻命中ctx.Done()分支,函数返回,goroutine正常退出。
内容的提问来源于stack exchange,提问作者Cmag

