Clean Architecture 在 Go 中通过 cmd、internal、pkg 目录边界强制隔离依赖:cmd 仅组装启动,internal 分 model/service/api/router 实现业务核心,pkg 提供无业务依赖的可测试工具。

怎么组织 cmd、internal 和 pkg 这三个核心目录
Go 的 Clean Architecture 不是靠抽象层堆出来的,而是靠目录边界强制隔离依赖流向。你写错一个 import 路径,就等于在整洁架构上凿了个洞。
常见错误现象:internal/service 里直接 import database/sql 或 gin.Context;pkg 里出现业务逻辑(比如用户积分计算);cmd/main.go 里初始化了 Redis 客户端却没抽到 global 层。
-
cmd/只做一件事:组装依赖 + 启动。它应该 importinternal所有子包,但绝不被internal反向 import -
internal/是你的业务心脏,分model(纯结构体)、service(不含框架、不碰 HTTP 或 DB)、api(只接收请求、调 service、返回响应)、router(仅注册路由,不写 handler 逻辑) -
pkg/是“可拔插工具箱”——必须能独立 go test,不能 importinternal或任何业务代码;时间格式化、JSON 序列化、ID 生成都放这儿
为什么 service 层不能碰 http 或 database/sql
因为你要让核心逻辑能脱离 Web 框架单独跑测试。一旦 UserRegisterService 里用了 gin.H{} 或 tx.Exec(),它就不再是“业务”,而是“胶水”。
使用场景:当你需要给 CLI 工具、定时任务、gRPC 接口复用同一套注册逻辑时,如果 service 层干净,你只要换个入口函数就行;否则就得 copy-paste 改一堆 SQL 错误处理和 status code 映射。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 正确做法:把数据库操作封装进
repository接口(定义在internal/model或internal/port),由service依赖接口,具体实现(如mysql_user_repo)放在internal/infrastructure - 错误参数差异:
service.Register(req *RegisterRequest)✅;service.Register(c *gin.Context)❌ - 性能影响:看似多了一层 interface 调用,实测 Go 编译器几乎全内联,零 runtime 开销
go mod download 失败或 main.go 找不到入口?检查这三处硬伤
不是环境问题,大概率是模块路径或包名写崩了。Clean 架构项目对 Go Module 的路径敏感度远高于普通项目。
常见错误现象:go run main.go 报 main redeclared;go mod download 卡住或报 unknown revision;IDE 提示 “cannot find package” 但 go list ./... 又能列出。
- 检查
go.mod第一行的 module 名是否与你实际 clone 的路径一致(比如module github.com/khannedy/golang-clean-architecture,但你 clone 到了~/myproj,就必须改 module 名或用go mod edit -module重写) -
cmd/main.go的package main上方不能有任何 import,且必须包含func main()—— 很多人把启动逻辑拆到internal/app却忘了在main.go里调用 - 所有
internal/xxx子包的 import 路径必须以module_name/internal/xxx开头,不能写相对路径或本地文件路径
配置加载失败、数据库连不上?别急着改 global.go
90% 的配置问题出在加载时机不对,而不是解析逻辑有 bug。Clean 架构里,配置应该在 cmd 层解析完毕,再以结构体形式注入到各层,而不是让 service 自己去读 YAML。
容易被忽略的地方:YAML 文件字段名大小写、struct tag 拼错、嵌套 map 未加 mapstructure:"xxx" 导致静默忽略字段。
- 推荐做法:用
viper在cmd/main.go里加载,然后 new 一个Configstruct,传给global.Init(config);global包里只存变量,不做解析 - 典型错误:
global.DB = sql.Open(...)写在 init() 函数里 —— 这会导致测试时无法 mock,且并发启动可能 panic - 兼容性注意:如果你用
etc/dev_conf.yaml,确保viper.SetConfigFile("etc/dev_conf.yaml")的路径是相对于cmd/还是执行目录;建议统一用viper.AddConfigPath(filepath.Dir(os.Args[0]))

















