Golang主程序在goroutine终止后如何优雅退出

Golang主程序在goroutine终止后如何优雅退出 在构建微服务的过程中,我设计出了一种模式,能够在使用 SIGTERM 信号时优雅地关闭服务器。在下面的程序中,为了说明问题,我还包含了一个“testGoroutine”。假设这是应用程序的重要组成部分,并且使用上下文来优雅地退出 goroutine。

我还希望在这个 goroutine 产生错误时,能够优雅地关闭主应用程序——例如关闭主程序中任何已打开的连接等。我的理解是,我不应该在 goroutine 内部执行 log.Fatal。

我能想到的另一个选项是向存活探针发送信号,但这只会让这个示例从 Kubernetes 的角度生效。

有什么建议吗?

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"

	"golang.org/x/net/context"
)

func main() {

	s := &http.Server{
		Addr:    ":8080",
		Handler: router(),
	}

	wg := &sync.WaitGroup{}
	wg.Add(1)

	tc, cancel := context.WithCancel(context.Background())

	go func() {
		if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatal("ListenAndServe:", err)
		}
	}()

	sigChan := make(chan os.Signal)
	signal.Notify(sigChan, os.Interrupt, os.Kill, syscall.SIGTERM, syscall.SIGINT)
	defer signal.Stop(sigChan)

	go func() {
		defer wg.Done()
		testGoroutine(tc)
	}()

	<-sigChan
	log.Println("terminate signal received")

	if err := s.Shutdown(tc); err != nil {
		log.Printf("Server shutdown error: %v", err)
	}
	cancel()
	wg.Wait()
}

func testGoroutine(ctx context.Context) {

	defer fmt.Println("testGroutine was exited")
	for {
		select {
		case <-ctx.Done():
			log.Println("Context has been cancelled")
			return
		default:
			for i := 0; i < 100; i++ {
				time.Sleep(1 * time.Second)
				log.Println("testGoroutine :", i)
				i++
				if i == 8 {
					log.Fatal("error")  // I am aware of that log.Fatal will immediately exit without running any defer
				}
			}
		}
	}
}

更多关于Golang主程序在goroutine终止后如何优雅退出的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

我对您的代码做了一些修改,以实现您想要的效果。我已经对关键部分和差异进行了注释,但如果您需要我进一步澄清,请告诉我。

关于 testGoroutine() 函数,我没有修改它,因为该函数不会在 main 中运行。它将是下游处理程序的一部分,错误应该在那里处理。

func main() {

s := &http.Server{
	Addr: "localhost:8080",
}

//声明变量,这里我们想要使用带缓冲的通道来立即处理任何错误
var (
	shutdown    = make(chan os.Signal, 1)
	serverError = make(chan error, 1)
)

tc, _ := context.WithCancel(context.Background())

//在新的协程中启动服务器,但将任何错误发布到 serverError 通道中。如果服务器启动失败,程序将立即退出
go func() {
	log.Printf("http server listening on %v", s.Addr)
	serverError <- s.ListenAndServe()
}()

signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)

select {
case <-shutdown:
	log.Println("terminate signal received")
	s.Shutdown(tc) //如果我们有任何活跃的连接,s.Shutdown() 将等待它们关闭后再关闭。
	_ = s.Close()  //需要检查这个错误。s.Close() 与 s.Shutdown() 不同,因为它会关闭监听器。由于 s.Shutdown() 阻止了任何新连接的创建,我们不应该有任何连接。
case err := <-serverError:
	log.Printf("server error, unable to start: %v", err)
}}

更多关于Golang主程序在goroutine终止后如何优雅退出的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Golang中优雅处理goroutine错误并退出主程序,可以使用错误通道和上下文取消机制。以下是改进后的代码示例:

package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "sync"
    "syscall"
    "time"
)

func main() {
    // 创建错误通道
    errChan := make(chan error, 1)
    
    s := &http.Server{
        Addr:    ":8080",
        Handler: router(),
    }

    wg := &sync.WaitGroup{}
    wg.Add(1)

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // 启动HTTP服务器
    go func() {
        if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            errChan <- fmt.Errorf("ListenAndServe: %w", err)
        }
    }()

    // 启动工作goroutine
    go func() {
        defer wg.Done()
        if err := testGoroutine(ctx); err != nil {
            errChan <- fmt.Errorf("testGoroutine: %w", err)
        }
    }()

    // 信号通道
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)

    select {
    case sig := <-sigChan:
        log.Printf("terminate signal received: %v", sig)
    case err := <-errChan:
        log.Printf("error received: %v", err)
        cancel() // 取消上下文以通知其他goroutine
    }

    // 优雅关闭HTTP服务器
    shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer shutdownCancel()
    
    if err := s.Shutdown(shutdownCtx); err != nil {
        log.Printf("Server shutdown error: %v", err)
    }
    
    wg.Wait()
    log.Println("Application shutdown completed")
}

func testGoroutine(ctx context.Context) error {
    defer fmt.Println("testGoroutine was exited")
    
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()
    
    counter := 0
    for {
        select {
        case <-ctx.Done():
            log.Println("Context has been cancelled")
            return nil
        case <-ticker.C:
            log.Printf("testGoroutine: %d", counter)
            counter++
            
            if counter == 8 {
                return errors.New("simulated error at count 8")
            }
        }
    }
}

func router() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("OK"))
    })
    return mux
}

关键改进点:

  1. 错误通道模式:创建缓冲为1的错误通道,goroutine通过此通道报告错误
  2. select多路复用:主程序同时监听信号通道和错误通道
  3. 上下文传播:当错误发生时,取消上下文以通知所有相关goroutine
  4. 结构化错误处理:goroutine返回错误而不是调用log.Fatal
  5. 超时控制:为服务器关闭设置超时上下文

这个模式确保:

  • goroutine错误能触发主程序优雅关闭
  • 所有资源都能被正确清理
  • 避免使用log.Fatal导致的立即退出
  • 支持信号触发和错误触发的双重关闭机制
回到顶部