生产项目禁用 gin.Default(),应改用 gin.New() 显式注册中间件;路由需按业务域抽离并独立注册;Viper 配置须支持多路径、多格式及环境变量覆盖;HTTP 服务必须实现带超时的优雅关机。

gin.Default() 不能直接用在生产项目里——它自动加了 recovery 和 logger 中间件,日志格式锁死、panic 捕获逻辑不可控,测试时还容易漏掉中间件副作用。轻量脚手架的关键不是“省代码”,而是“可调试、可替换、可隔离”。
别用 gin.Default(),改用 gin.New() 显式构造引擎
真实项目里,路由引擎必须是你自己组装的,而不是一键生成的黑盒。
-
gin.Default()等价于gin.New()+Use(logger(), recovery()),但这两个中间件你没法改参数、没法换实现、没法跳过 - 比如你想把日志打到
zerolog或zap,或者想在 recovery 里上报 Sentry,gin.Default()就卡死你 - 测试时,
gin.Default()的 logger 会往 stdout 写,干扰断言;recovery 会吞 panic,导致单元测试不报错却实际失败 - 正确写法:
engine := gin.New()<br>engine.Use(middleware.ZapLogger())<br>engine.Use(middleware.RecoveryWithSentry())<br>engine.Use(middleware.CORS())
路由注册必须抽离成独立函数,禁止堆在 main.go
所有 r.GET、r.POST 塞进 main.go,等于把业务耦合进启动入口——改个用户接口就得跑全量测试,加个新模块还得手动翻 main 查冲突路径。
- 每个业务域建一个
internal/user/handler.go,暴露RegisterHandlers(r *gin.Engine) - main.go 只做装配:
user.RegisterHandlers(r)、order.RegisterHandlers(r),不碰任何 handler 实现 - 如果用
chi替代gin做路由层(推荐),就用r.Route("/api/v1", func(r chi.Router) { ... })隔离前缀,避免跨模块路径覆盖 - 这样单测
go test ./internal/user就能跑通全部用户路由逻辑,CI 可以按模块并行执行
viper 配置加载必须支持多路径 + 多格式 + 环境变量 fallback
硬编码 viper.SetConfigFile("config.yaml") 是线上事故高发点:K8s ConfigMap 给 JSON,Secret 注入是 ENV,本地开发用 YAML——三者不统一,服务一启就 panic。
- 正确初始化顺序:
viper.SetConfigName("config")(不带后缀)→viper.AddConfigPath("./config")→viper.AddConfigPath("/etc/myapp/")→viper.AddConfigPath(".") - 启用环境变量覆盖:
viper.SetEnvPrefix("APP")+viper.AutomaticEnv(),支持APP_HTTP_PORT=8081动态覆盖配置项 - YAML/JSON/TOML 自动识别,不用手动判断格式;viper 会按添加顺序依次尝试读取,第一个成功即停
- 务必在
viper.ReadInConfig()后检查错误,不要忽略if err != nil
HTTP 服务必须实现优雅关机,否则 K8s rolling update 会丢请求
r.Run(":8080") 是 demo 写法,生产环境直接 kill 进程会导致正在处理的请求被中断,尤其长耗时接口或 WebSocket 连接。
- 用
http.Server显式管理生命周期:srv := &http.Server{Addr: ":8080", Handler: r}<br>go func() { if err := srv.ListenAndServe(); err != http.ErrServerClosed { log.Fatal(err) } }() - 监听系统信号:
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - 收到信号后调用
srv.Shutdown(ctx),传入带 timeout 的 context(建议 5–10 秒),等待活跃连接自然结束 - 记得在
Shutdown前关闭数据库连接池、Redis 客户端等资源,顺序不能反


















