应弃用 gin.default() 改用 gin.new() 手动组装中间件,分层路由差异化挂载中间件,repository 层解耦数据库驱动,http server 需配置超时并实现带 context 透传的优雅退出。

为什么不用 gin.Default() 直接启动微服务
直接用 gin.Default() 会自动加载 Logger 和 Recovery 中间件,这在开发期方便,但进生产就是隐患:日志打满磁盘、panic 后堆栈暴露敏感信息、无法控制信任代理头。微服务要求每个环节可观察、可收敛、可隔离。
手写脚手架的第一步,就是弃用 gin.Default(),改用 gin.New() 手动组装中间件链:
-
gin.New()创建空引擎,不带任何默认行为 - 按需注入
logger(如 zap)、recovery(带错误上报)、cors(明确 origin 白名单) - 必须调用
r.Use(gin.RecoveryWithWriter(...))而非裸gin.Recovery(),避免 panic 信息泄露到响应体 - 若服务部署在 Nginx 或 ALB 后,务必设置
r.ForwardedByClientIP = true并配置r.TrustedProxies,否则c.ClientIP()拿到的是负载均衡器 IP
如何组织多服务共存的路由与中间件
一个微服务脚手架常需同时暴露 API 接口、健康检查、指标接口(如 /metrics)、Swagger 文档(如 /swagger/*any),它们对中间件的需求完全不同。硬塞进同一组中间件会导致指标被鉴权拦截、文档路径被 CORS 阻断。
正确做法是分层路由 + 差异化中间件:
- 根路由
r := gin.New()不挂任何业务中间件 - API 组:
api := r.Group("/api/v1")→ 挂auth、rateLimit、traceID - 运维组:
admin := r.Group("/admin")→ 挂basicAuth、ipWhitelist - 公开组:
public := r.Group("")→ 只挂cors,不挂鉴权 - Swagger 单独挂到
r.GET("/swagger/*any", ...),且中间件只保留cors和logger
注意:中间件顺序很重要。traceID 必须在最前,否则后续中间件拿不到上下文 ID;auth 必须在 rateLimit 之后(避免未认证请求耗尽配额)。
双数据库与 Repository 分层怎么落地
MVC 脚手架里常提“双数据库”,比如用户用 MySQL、订单用 PostgreSQL。但实际代码里若把 gorm.Open() 写死在 handler 或 service 层,会导致测试难 mock、切换库成本高、事务边界模糊。
Repository 层必须解耦驱动和业务逻辑:
- 定义接口:
type UserRepository interface { GetByID(ctx context.Context, id int) (*User, error) } - 实现 MySQL 版:
type mysqlUserRepo struct { db *gorm.DB },构造时传入已配置好的*gorm.DB - 实现 PostgreSQL 版:
type pgOrderRepo struct { db *sql.DB },用database/sql原生驱动更可控 - 在
main()初始化阶段,按配置创建对应实例,注入到 Service 层,而非让 Service 自己 new DB - 事务控制交给 Service 层统一管理,Repository 方法不开启事务,只执行单条语句
容易踩坑的是:跨库事务不可行,订单创建成功但用户积分更新失败时,得靠 RocketMQ 补偿,不能幻想 ACID 跨库。
HTTP Server 超时与优雅退出怎么写才不丢请求
微服务上线后最怕的是滚动更新时连接被粗暴中断。Gin 本身不处理 graceful shutdown,全靠 http.Server 的 Shutdown() 方法,但很多人漏掉关键细节。
手写脚手架必须显式控制 server 生命周期:
- 设置
ReadTimeout、WriteTimeout、IdleTimeout,三者缺一不可(常见错误:只设ReadTimeout,导致长连接 hang 死) - 监听
os.Interrupt和syscall.SIGTERM,收到信号后先关闭 listener,再调用srv.Shutdown() -
Shutdown()超时时间必须 > 最长业务处理时间(比如支付回调可能耗时 5s,则设 10s) - 在
Shutdown()前,主动通知注册中心下线(如 Consul 的/v1/agent/service/deregister),避免新流量打入 - handler 内部要支持 context.Done(),比如 DB 查询加
ctx参数,避免Shutdown()时还在跑慢查询
真正难的不是写这几行代码,而是所有 handler、middleware、repository 调用链都得透传 context.Context —— 这个约束一旦漏一处,优雅退出就形同虚设。











