gin.default()不适合生产环境,因其默认注入logger和recovery中间件,导致日志格式不可控、panic捕获逻辑无法定制,且单元测试易漏中间件副作用;应改用gin.new()显式注册中间件。

为什么不用 gin.Default() 启动服务
它会悄悄注入 recovery 和 logger 中间件,导致日志格式不可控、panic 捕获逻辑无法定制,测试时还容易漏掉中间件副作用。比如你 mock 一个 gin.Context 做单元测试,实际运行时却多了一层 recovery 包裹,panic 不会按预期抛出,结果测试通过但线上 crash。
正确做法是显式构造:r := gin.New()
再按需注册:r.Use(middleware.Recovery())r.Use(middleware.LoggerWithWriter(...))
这样中间件顺序、生命周期、错误行为全部可控。
路由必须分离到独立函数,不能堆在 main.go
把所有 r.GET、r.POST 写进 main.go,后期没法做模块级单元测试,CI 也不能单独跑 go test ./internal/user;改一个用户路由,订单模块的测试覆盖率可能莫名掉 20%。
推荐结构:
- internal/user/handler.go 定义 RegisterHandlers(r *gin.Engine)
- internal/order/handler.go 同样定义自己的 RegisterHandlers
- main.go 只负责调用:user.RegisterHandlers(r)order.RegisterHandlers(r)
如果用 chi 替代 gin.Engine,还能进一步解耦:r.Route("/api/v1", func(r chi.Router) { user.RegisterHandlers(r) }) —— 避免跨模块路径冲突。
viper 加载配置必须支持多路径 + 多格式 + 环境变量覆盖
硬编码 viper.SetConfigFile("config.yaml") 是最常见错误:本地开发用 YAML,K8s ConfigMap 给的是 JSON,Secret 注入又是 ENV,三者不统一直接 panic。
正确初始化顺序:viper.SetConfigName("config")(不带后缀)viper.AddConfigPath("./config")viper.AddConfigPath("/etc/myapp/")viper.AddConfigPath(".")viper.SetEnvPrefix("APP")viper.AutomaticEnv()
最后再 viper.ReadInConfig()。
这样它会依次尝试读取:./config/config.yaml → /etc/myapp/config.json → ./config.toml → 环境变量 APP_HTTP_PORT=8081 覆盖字段。
HTTP 服务必须实现优雅关机,别只写 r.Run()
r.Run() 是阻塞式启动,进程收到 SIGTERM(比如 k8s rolling update)时直接 kill,正在处理的请求会被中断,TCP 连接重置,客户端收到 ECONNRESET。
必须手动接管:srv := &http.Server{Addr: ":8080", Handler: r}go func() { log.Fatal(srv.ListenAndServe()) }()
然后监听信号:sig := make(chan os.Signal, 1)signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)<br><code>srv.Shutdown(context.WithTimeout(context.Background(), 10*time.Second))
注意:Shutdown() 不会等所有 goroutine 结束,业务 handler 里如果有长耗时操作(如上传、导出),得自己加 context 控制超时或取消。











