在开发网络服务时,经常会遇到需要重启或更新程序的情况。比如你写了一个后台服务跑在服务器上,突然要升级版本,直接杀掉进程可能会导致正在处理的请求出错,用户收到一堆错误提示,体验很不好。这时候,就需要让程序“优雅地”关闭——也就是先停止接收新请求,把正在处理的做完,再退出。
信号监听是第一步
Go 程序默认对操作系统信号没有特殊处理,按 Ctrl+C 或者被 kill 命令终止时会直接退出。但我们可以通过 os/signal 包来捕获中断信号,比如 SIGINT(Ctrl+C)和 SIGTERM(系统建议终止),然后执行清理逻辑。
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second) // 模拟耗时操作
w.Write([]byte("Hello World"))
})
server := &http.Server{Addr: ":8080", Handler: mux}
go func() {
log.Println("服务器启动在 :8080")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("服务器异常: %v", err)
}
}()
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
<--c // 等待信号
log.Println("收到中断信号,准备关闭...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("服务器关闭出错: %v", err)
}
log.Println("服务器已安全退出")
}
这段代码启动了一个 HTTP 服务,同时用 channel 监听系统信号。当收到退出信号后,并不立即终止,而是调用 server.Shutdown(),它会拒绝新连接,同时等待已有请求完成,最长等 10 秒。
实际场景中的意义
想象一下你在点外卖,订单刚提交,后端服务正在写数据库、发通知,这时候运维同学上线新版本,顺手把进程 kill 了。结果订单状态没保存成功,你发现钱扣了却没下单成功。这种问题就可以通过优雅关闭避免。
除了 HTTP 服务,像 Kafka 消费者、定时任务、WebSocket 长连接等场景,也都适用类似的模式:监听信号,停止拉取新任务,把手上正在做的处理完,再退出。
注意超时时间设置
上面例子中用了 10 秒的上下文超时,这个值不是随便写的。如果你的服务里最长请求可能耗时 8 秒,那设成 5 秒就可能导致还没处理完就被强制中断。所以这个时间要结合业务实际情况来定,不能太短也不能无限等。
有时候还会配合健康检查一起用,比如在 Kubernetes 中,先将 Pod 标为不可用,等一小段时间后再发终止信号,给程序留足缓冲期。这样整个流程更稳妥。