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
我对您的代码做了一些修改,以实现您想要的效果。我已经对关键部分和差异进行了注释,但如果您需要我进一步澄清,请告诉我。
关于 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的错误通道,goroutine通过此通道报告错误
- select多路复用:主程序同时监听信号通道和错误通道
- 上下文传播:当错误发生时,取消上下文以通知所有相关goroutine
- 结构化错误处理:goroutine返回错误而不是调用log.Fatal
- 超时控制:为服务器关闭设置超时上下文
这个模式确保:
- goroutine错误能触发主程序优雅关闭
- 所有资源都能被正确清理
- 避免使用log.Fatal导致的立即退出
- 支持信号触发和错误触发的双重关闭机制

