Go HTTP服务优雅关闭触发Race Condition竞态问题如何修复
竞态产生的原因
你代码中的数据竞争来源于serve函数的返回值变量err被两个goroutine并发读写,未做同步:
- 你启动的后台goroutine会执行
err = srv.ListenAndServe()对外部的err变量赋值 - 主goroutine在收到停止信号后会执行
err = srv.Shutdown(ctxShutDown)对同一个err变量赋值
两个goroutine同时操作同一块内存地址的变量,就触发了竞态检测。
修复方案
只需要把后台goroutine中用到的err替换为goroutine内部的局部变量即可,不需要修改外部的返回值err:srv.ListenAndServe只有两种返回场景:
- 服务启动失败(比如端口被占用),此时直接
log.Fatal退出进程,不需要把错误赋值给外部err - 服务被
Shutdown主动关闭,返回固定的http.ErrServerClosed,这个返回值不需要额外处理,后续Shutdown方法的返回值才是serve函数需要返回的错误。
修改的核心代码片段:
// 原代码 go func() { if err = srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("listen:%+s\n", err) } }() // 修改后代码 go func() { // 使用内部局部变量listenErr,不修改外部的返回值err listenErr := srv.ListenAndServe() if listenErr != nil && listenErr != http.ErrServerClosed { log.Fatalf("listen:%+s\n", listenErr) } }()
完整修复后代码
package main import ( "context" "fmt" "log" "net/http" "os" "os/signal" "time" ) func serve(ctx context.Context) (err error) { mux := http.NewServeMux() mux.Handle("/", http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "okay") }, )) srv := &http.Server{ Addr: ":8000", Handler: mux, } go func() { listenErr := srv.ListenAndServe() if listenErr != nil && listenErr != http.ErrServerClosed { log.Fatalf("listen:%+s\n", listenErr) } }() log.Printf("server started") <-ctx.Done() log.Printf("server stopped") ctxShutDown, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err = srv.Shutdown(ctxShutDown); err != nil { log.Fatalf("server Shutdown Failed:%+s", err) } log.Printf("server exited properly") if err == http.ErrServerClosed { err = nil } return } func main() { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) ctx, cancel := context.WithCancel(context.Background()) go func() { oscall := <-c log.Printf("system call:%+v", oscall) cancel() }() if err := serve(ctx); err != nil { log.Printf("failed to serve:+%v\n", err) } }
修复后重新执行go run -race main.go,触发SIGINT信号就不会再出现竞态提示。
内容的提问来源于stack exchange,提问作者Jeffy Mathew
相关产品推荐
相关产品推荐

