Clean Architecture在Go中适用于业务复杂场景,domain层仅含基础类型、枚举、值对象及纯领域方法,禁用time.Time等外部类型;Repository接口须定义在domain或usecase层,usecase需封装事务、权限、错误转换等业务逻辑而非仅作胶水。

Clean Architecture 在 Go 网络项目中不是万能胶,它只在业务复杂度开始“咬人”时才真正值得投入——比如你已经有 3+ 个跨实体协作的用例、需要支持多数据源(MySQL + Redis + 第三方 API)、或正被测试覆盖率低和改一处崩三处的问题拖慢迭代。否则,一个 main.go + handlers + models 的扁平结构反而更轻快。
domain 层放什么?不放什么?
domain 是整套架构的锚点,它必须能脱离 net/http、database/sql、time 甚至 log 单独编译通过。
-
✅ 只允许:
- 基础类型字段(
string、int64、bool) - 自定义枚举(如
type UserStatus int) - 值对象(
type Email struct{ addr string },含校验方法) - 实体方法只操作自身状态(
u.Activate()、o.CalculateTotal())
- 基础类型字段(
-
❌ 绝对禁止:
-
time.Time字段(应抽象为Clock接口注入) -
log.Printf或任何外部调用 -
json:、bson:tag(序列化是外层适配器的事) - 引用
infrastructure、adapters、usecase包
-
常见错误:把 CreatedAt time.Time 直接写进 User 结构体 → domain 被标准库绑定,换时区或 mock 时间就卡死。
Repository 接口该定义在哪一层?
必须定义在 domain 或 usecase 层,不能在 infrastructure 里定义再被上层 import。
-
正确路径:
-
domain/user_repository.go:声明type UserRepository interface { FindByID(id string) (*User, error) } -
infrastructure/postgres/user_repo.go:实现该接口,importdomain,但domain绝不 import 它
-
-
错误信号:
-
usecase包里出现import "yourapp/infrastructure/mysql" - 接口方法名含
QueryRaw、ScanRows、ExecTx—— 这暴露了 SQL 细节,违反业务语义封装 - 返回值是
*sql.Rows或gorm.DB—— 领域层不该知道这些
-
为什么重要?因为只有接口定义在内层,才能保证 usecase 调用时完全不知道底层是 PostgreSQL 还是内存 map,后续替换数据库才不碰业务逻辑。
usecase 层什么时候变成“胶水代码”?
当它只剩 repo.Find() → entity.Do() → repo.Store() 三行,且没做以下任何事时,就是危险信号:
- 封装事务边界(
tx.Begin()/tx.Commit()应在此层协调) - 做权限检查(
if !user.CanEdit(post)) - 统一错误分类(把
sql.ErrNoRows转成ErrPostNotFound) - 处理领域服务编排(如
paymentService.Charge()+notificationService.Send()的组合)
示例反模式:
func (s *Service) CreatePost(req CreatePostReq) error {
p := domain.NewPost(req.Title, req.Body)
return s.repo.Store(p) // 没有验证、没管作者权限、没发通知、没启事务
}正确做法是让 usecase 成为“业务剧本导演”,而不是“数据库搬运工”。
真正难的从来不是分目录或写接口,而是每次加字段、改流程时,都得问一句:这个东西,属于业务本质,还是技术附着物?一旦混淆,Clean Architecture 就会从解耦工具变成目录套娃负担。


















