clean architecture 在 go 中靠包职责隔离和接口下沉实现,domain 层仅含业务实体与行为、不依赖外部包;repository 接口须定义在 domain 或 usecase 层;usecase 封装业务动作而非胶水代码;handler 仅为协议适配器。

Clean Architecture 在 Go 里不是靠模板套出来的,而是靠包职责隔离和接口下沉做出来的;没有 domain、usecase、repository 这三层物理结构,光靠目录名喊口号没用。
Go 里 domain 层到底该放什么
Domain 层必须只含业务核心实体(struct)和领域行为(method),不能依赖任何外部包,包括 database/sql、net/http、甚至 time(时间应抽象为接口传入)。常见错误是把 CreatedAt 字段直接设为 time.Time,导致 domain 被标准库绑定。
- 实体字段只用基础类型(
string、int64、自定义枚举)或 domain 内定义的值对象 - 领域方法只操作自身状态,不调用外部服务、不读写数据库、不发 HTTP 请求
- 若需时间,定义
type Clock interface { Now() time.Time },由外层注入 - 示例:
type User struct { ID string Name string Status UserStatus // 自定义枚举,非 string } func (u *User) Activate() error { if u.Status == Active { return errors.New("already active") } u.Status = Active return nil }
repository 接口为什么必须定义在 domain 或 usecase 包里
Repository 是“被依赖方”,不是“依赖方”。如果把 UserRepository 接口放在 infrastructure 包里,上层 usecase 就得 import 下层,违反依赖倒置。Go 没有 Java 的 package visibility 控制,只能靠目录约束。
- 接口定义在
domain(推荐)或usecase,例如domain/user_repository.go -
infrastructure包实现该接口,importdomain,但domain绝不 importinfrastructure - 避免在接口中暴露 SQL 细节(如
QueryRaw、Scan),只暴露业务语义方法(FindByEmail、Store) - 返回值用 domain 实体,不返回
*sql.Rows或gorm.DB
usecase 层如何避免变成“胶水代码”
Usecase 不是函数集合,而是业务用例的封装。它协调 domain 实体与 repository 接口,处理事务边界、权限校验、错误分类,但不碰具体实现。
- 每个 usecase 对应一个明确业务动作,如
CreateUser、TransferMoney,不要写UserUsecase这种大而全的 struct - 构造函数接收 repository 接口(而非具体实现),例如
func NewCreateUserUC(repo UserRepository) *CreateUserUC - 参数和返回值用 domain 类型或简单 DTO(如
type CreateUserInput struct { Email string }),不暴露http.Request或gorm.Model - 错误要分层:domain 错误(
ErrInvalidEmail)、infra 错误(ErrDBConnection)、usecase 封装后统一返回error,不 panic
HTTP handler 怎么和 Clean Architecture 对齐
Handler 是最外层适配器,只负责协议转换、输入解析、响应序列化,不包含业务逻辑。容易踩的坑是把 validation、auth、logging 塞进 handler 导致膨胀。
- handler 只调用一个 usecase 方法,例如
uc.CreateUser(ctx, input) - 输入校验可前置(如用
go-playground/validator解析 JSON 后验证),但规则要和 domain 约束一致(比如 email 格式检查应在 domain 实体构造时也触发) - auth 和 logging 用中间件,不要在 handler 函数体里写
if !user.IsAdmin { ... } - 不要在 handler 里 new usecase 实例——通过 DI 容器或 main 函数组装好再传入
- 示例:
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) { var input CreateUserInput json.NewDecoder(r.Body).Decode(&input) user, err := h.uc.CreateUser(r.Context(), input) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } json.NewEncoder(w).Encode(user) }
真正难的不是分层命名,而是每次加功能时都问一句:“这个新类型/函数/错误,放在哪一层不会让其他层被迫 import 它?”——只要某次为了方便把 infra 的类型导出到 domain,整个架构就塌了一角。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











