优雅停机与请求超时:http.Server 配置
本章解决什么问题:直接 kill 进程会掐断正在处理的请求,可能丢数据、留脏状态。优雅停机让服务“先停止接收新请求、把手头请求处理完再退出”;超时配置则防止慢请求无限拖住连接。官方文档 gin-gonic.com 的优雅停机示例与本例思路一致。
用 http.Server 包裹引擎
gin 的引擎本身就是 http.Handler,可以直接交给 http.Server,从而获得标准库的完整超时与关闭能力:
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) { c.String(200, "pong") })
srv := &http.Server{
Addr: ":8080",
Handler: r,
ReadHeaderTimeout: 5 * time.Second,
}
go func() {
err := srv.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen: %v", err)
}
}()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done() // 收到 Ctrl+C 或 SIGTERM
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Fatalf("shutdown: %v", err)
}
log.Println("server exited gracefully")
}
流程拆解:ListenAndServe 放 goroutine;主协程等信号;收到信号后用带超时的 context 调 srv.Shutdown——它先停止接收新连接,再等待存量请求完成,超过 10 秒则强制返回。若服务持有数据库连接池、后台任务等资源,可在 Shutdown 前后统一清理,或注册 srv.RegisterOnShutdown 回调,保证资源释放也走同一流程。
http.Server 的四个超时
- ReadHeaderTimeout:读请求头超时,防御慢连接攻击,务必设置;
- ReadTimeout:读取整个请求体的时间上限;
- WriteTimeout:从请求头读完到响应写完的时间上限;
- IdleTimeout:keep-alive 空闲连接的超时。
注意 WriteTimeout 是“一整个请求的总预算”,长任务接口可能超时被断,这正是触发业务超时设计的信号。四个值都设 0 表示不限时:本地调试可以宽松,生产环境务必全部收紧。
请求级超时
服务端超时只保护连接,业务自身还要防“依赖下游慢”。两种常用做法:
- 用 http.TimeoutHandler 包住引擎:srv.Handler = http.TimeoutHandler(r, 5*time.Second, "timeout"),超时自动回 503;
- 在 handler 里基于请求上下文派生超时:ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second),做数据库调用、调用下游时都传这个 ctx,让取消信号一路传递。
关键点
- gin 引擎就是 http.Handler,直接嵌入 http.Server 即获得超时与优雅关闭能力。
- 优雅停机 = 信号监听 + Shutdown:先拒新请求,再等旧请求办完,最后兜底强制退出。
- 连接超时与业务超时要各司其职:前者防慢连接,后者用 context 取消链路。
- 客户端断连时 c.Request.Context() 会自动取消,长任务要监听它而不是闷头跑完。
- 超时与停机参数适合交给配置管理(见第 21 章),不同环境各有一份而不改代码。
小结
优雅停机解决“怎么退”,超时配置解决“怎么限”,两者合起来就是生产级服务的生命周期管理。至此,从环境搭建到运行治理的 Gin 主线已经贯通。