go的clean architecture不是简单加目录,而是通过依赖方向控制和接口生命周期管理实现:entity/usecase不得导入外层包,接口声明与实现分离,wire依赖注入需严格匹配签名且避免循环引用。

Go 项目一旦超过 3 个业务模块,不按 Clean Architecture 分层,internal 目录就会开始自我坍缩——接口定义飘在 pkg、仓库实现混在 handlers、用例逻辑里直接调 db.QueryRow,最后连 go test ./... 都跑不全。
为什么 Go 的 Clean Architecture 不是“加一层目录”那么简单
很多人把 Clean Architecture 理解成“建几个文件夹”,结果只是把混乱从扁平结构搬进嵌套结构。真正起作用的是依赖方向控制和接口生命周期管理。
关键点在于:internal/entity 和 internal/usecase 必须完全不 import 任何外层包(比如 database/sql、github.com/gofiber/fiber/v2),否则就破防了。一旦某处 usecase 出现 import _ "github.com/lib/pq",整个内层就失去可测试性。
- 实体(
entity)只含结构体和方法,不带任何外部类型(如*sql.Rows、context.Context) - 用例(
usecase)只依赖接口,且这些接口必须声明在本层或entity层(例如type UserRepository interface { GetByID(int) (*entity.User, error) }) - 接口的实现(比如
postgresRepo)只能放在internal/repo或internal/adapter,且实现里才能 importdatabase/sql等
Wire 生成依赖图时最常卡住的三个地方
wire 不是魔法,它靠静态分析构造函数签名来拼接依赖链。一旦签名不匹配或循环引用,wire gen 就会静默失败或报错模糊。
典型卡点:
-
NewUserUsecase接收*sql.DB,但你实际想传的是Repository接口——Wire 不会自动帮你转型,必须显式提供func NewPostgresRepo(*sql.DB) UserRepository - 两个
usecase互相调用(比如UserUsecase调NotificationUsecase),Wire 无法解析循环依赖,得改用事件机制或回调注入 -
config.Load()返回Config结构体,但多个构造函数都依赖它——必须用wire.Value或wire.Struct显式声明为共享依赖,否则 Wire 会尝试新建 N 次
从 v1 到 v4 的演进中,被悄悄删掉的“便利写法”
早期 go-clean-arch v1 版本允许 controller 直接 new 一个 usecase 并传入 *sql.DB,看起来省事,实则埋雷:
- 测试时无法替换
usecase的依赖,只能跑集成测试 - 换数据库时要改所有
controller初始化代码,而非只改依赖注入配置 -
usecase里出现fmt.Printf或log.Println——因为没抽象日志接口,没人拦得住
v4 后统一要求:所有构造函数必须声明完整依赖,所有外部能力(日志、缓存、DB)必须通过接口注入,哪怕只是 type Logger interface { Info(string, ...any) }。
真正难的不是画四层图,而是每次新增一个 HTTP handler 时,能忍住不直接 db.Query,而是先去 internal/usecase 补接口、再回 internal/repo 补实现、最后让 wire 把它们串起来——这个节奏一旦松懈,架构就从“整洁”滑向“自洽的混乱”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











