You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go语言优雅关闭Worker:Ctrl+C无法终止程序的bug排查

Go Worker优雅关闭程序无法终止的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,内层循环也会一直执行打印和睡眠操作,永远不会退出,导致整个myWorker goroutine无法结束,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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 11:45:34