Go项目落地整洁架构需严守cmd、internal、pkg三层目录边界:cmd仅初始化依赖树并调用app.Run(),internal按职责分包且禁止框架/DB类型侵入,pkg须独立可测、不依赖internal,所有import路径须与go.mod严格一致。

Go 项目要真正落地整洁架构,不是靠画四层图或套命名模板,而是靠三处目录边界的硬约束:cmd、internal、pkg。只要这三层的 import 流向写错一次,业务逻辑就立刻和框架/数据库耦合,后续换 Gin 为 Echo、MySQL 换 TiDB 就得改满屏 SQL 错误处理。
cmd/ 只能 import internal,不能被反向 import
这是最常破防的第一道墙。很多项目在 cmd/main.go 里初始化了 Redis 客户端、加载配置、甚至写了健康检查 handler——这些本该属于 internal/infrastructure 或 internal/api 的职责,全挤进入口文件,导致 internal 包为了复用逻辑,不得不反过来 import cmd,彻底打破依赖单向性。
正确做法只做两件事:
- 用
flag或spf13/cobra解析命令行参数 - 组装依赖树:new App → new Service → new Repository → new DBClient,并调用
app.Run()
示例片段(非完整):
立即学习“go语言免费学习笔记(深入)”;
func main() {
cfg := loadConfig()
db := sql.Open("mysql", cfg.DBDSN)
repo := mysql.NewUserRepo(db)
svc := service.NewUserService(repo)
api := http.NewUserAPI(svc)
app := &App{api: api}
app.Run(cfg.Port)
}
注意:http.NewUserAPI 来自 internal/api/http,不是 cmd 自己实现 handler;mysql.NewUserRepo 在 internal/infrastructure/mysql,不在 cmd 里 new DB 实例后直接传给 service。
internal/ 下必须按职责分包,且禁止出现框架/DB 类型名
你看到 internal/service/user_service.go 里有 gin.H{}、tx.Exec()、c.Param("id"),这就是典型的“service 层塌陷”。它已不再是业务编排,而是胶水代码。
关键判断标准:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
service.Register(*RegisterRequest)✅ —— 输入是纯结构体 -
service.Register(*gin.Context)❌ —— 输入是框架类型,无法给 CLI 复用 -
internal/model里只有type User struct和方法,不 importgorm或sqlx -
internal/port(或internal/repository)定义接口:type UserRepository interface { FindByID(ctx context.Context, id int) (*User, error) } - 具体实现如
internal/infrastructure/mysql/user_repo.go才 importdatabase/sql并实现该接口
错误现象:go test ./internal/service/... 报错 “cannot find package github.com/gin-gonic/gin”,说明测试跑不起来——因为业务核心依赖了 Web 框架。
pkg/ 必须能独立 go test,且不能 import internal
pkg 是你的“工具箱抽屉”,不是“业务中转站”。一旦你在 pkg/idgen 里 import internal/model 或调用 service.GetUserByID,这个包就失去了可拔插性,也无法被其他项目安全复用。
典型合规场景:
-
pkg/logger:封装zerolog,提供logger.Info().Str("user", u.Name).Send(),不碰任何业务字段 -
pkg/uid:生成 Snowflake ID,返回int64,不关心这个 ID 是存用户还是订单 -
pkg/validate:校验邮箱格式、手机号长度,输入是string,输出是error,不 importinternal/model.User
检查方式很简单:cd pkg/logger && go test 必须成功;如果失败,八成是误引入了业务包路径。
go mod download 失败?先盯死 module 路径和 import 路径一致性
Clean 架构对模块路径极其敏感。常见硬伤不是网络问题,而是:
-
go.mod第一行写的是module github.com/yourname/project,但你把代码 clone 到了~/code/myproject目录下,go run cmd/app/main.go就会报main redeclared或cannot find package -
internal/service里写import "github.com/yourname/project/pkg/logger",但实际项目根路径是github.com/othername/project,路径不匹配,IDE 和go list表现不一致 -
pkg子包没有导出函数/类型(首字母小写),外部调用时静默失败,而非编译报错
修复动作只有三个:go mod edit -module github.com/yourname/project、确保所有 import 路径与 go.mod 一致、检查每个 pkg/xxx 下的导出标识符是否大写开头。
最易被忽略的一点:internal 下的包名(package user)可以和目录名(internal/service/user)不同,但 import 路径永远以 module 名开头,和目录结构严格对应。写错一个斜杠,整个依赖流就断了。

















