因为企业项目必须可维护、可协作、可扩展,而单文件main.go会导致路由/业务/数据逻辑混杂、代码臃肿、无法复用、难以测试和团队协作,故需立即采用路由→控制器→服务→模型四层架构实现彻底解耦。

为什么 Gin 项目一上来就该分层,而不是写个 main.go 跑通就完事?
因为 gin.Default() 确实能三行跑起来,但加到第5个接口时,main.go 就开始发烫——路由、中间件、DB 初始化、日志全挤在一起,改个数据库配置得 grep 全局,加个新模块容易循环 import。Gin 本身不强制结构,但工程化不是“能跑就行”,而是“谁接手都能快速定位、安全修改”。
- 不分层的典型症状:
main.go超过 300 行、go mod graph出现环形依赖、改个 Redis 配置要动 4 个文件 - 分层不是为了炫技,而是把变化频率不同的东西隔开:接口层(高频变更)、应用层(中频编排)、领域层(低频稳定)、基础设施层(可替换)
- 脚手架里最常被忽略的一点:
dao和repository不是同义词——前者管连接池和 SQL 执行,后者只面向聚合根提供Save()/FindByID()这类语义方法
router 目录下怎么组织路由注册才不会后期失控?
别把所有 r.GET 堆在 router.go 里。按业务域拆,比如用户相关路由进 user_router.go,订单进 order_router.go,每个文件只负责自己那块的 r.Group 和中间件绑定。
- 必须用
r.Group("/api/v1")统一前缀,避免每个 handler 自己拼路径,否则版本升级时全局搜替换太危险 - 中间件别直接写在
GET里,统一在 group 上挂:userGroup := r.Group("/users").Use(authMiddleware, logMiddleware) - 路由文件里不要初始化 DB 或调用 service —— 只做“谁来、走哪条路、带什么中间件”,具体逻辑交给 controller 或 handler 层去调
- 如果用了 DDD,
router层只能引用application层的 usecase,绝对不能 importdomain实体或infrastructure的 MySQL 实现
配置加载顺序错乱会导致 mysql.Init() panic,怎么稳住?
Go 没有构造函数顺序保证,main() 里先 init logger 再 init mysql 是硬性要求。很多脚手架崩在启动时连日志都打不出来,就是因为配置没加载完就去 dial DB。
- 配置解析必须早于任何依赖初始化:
setting.Init("config.yaml")→logger.Init()→mysql.Init()→redis.Init() - YAML 配置里字段名大小写必须和 struct tag 严格一致,
type MysqlConfig struct { Host string `yaml:"host"` },写成`yaml:"Host"`就会静默为空 - 数据库连接池参数别照抄示例:
MaxOpenConns: 200在容器环境可能远超实际需要,结合压测调整,否则 MySQL 服务端扛不住 - 用
defer mysql.Close()放在main()函数末尾,而不是某个 init 函数里——确保进程退出前释放连接
DDD 脚手架里 event.Publish() 为什么总丢消息?
RocketMQ 生产者没设置 SendMsgTimeout 或没等 SendResult 就返回,看起来发成功了,其实 broker 拒绝了。Gin handler 里直接调 event.Publish() 是常见坑点。
立即学习“go语言免费学习笔记(深入)”;
- 领域事件发布必须异步且带重试:
event.PublishAsync()底层用 goroutine + channel 缓冲,别卡住 HTTP 请求 - 本地事务和消息发送必须解耦:先 commit DB 事务,再发 event;不能在事务里发,否则回滚时消息已发出去
- 消费者端要幂等:RocketMQ 无法保证 exactly-once,
OrderCreatedEvent处理前先查 DB 是否已存在该订单 ID - 开发环境别省事关掉 RocketMQ——用 Docker 启一个
apache/rocketmq:5.3,否则 event 流程永远测不全
sql.ErrNoRows,应用层不该 import gin.Context,领域实体不能有 json:"id" tag。这些细节漏一点,半年后重构成本就翻倍。


















