Table of contents
Open Table of contents
TL;DR
Clean Architecture 只有一条铁律:内层不能 import 外层的任何东西。通过接口定义在消费方(依赖反转),实现内层调用外层能力、却不依赖外层实现。其余分层、命名都是这条规则的推论。
没有 Clean Architecture 时的典型问题
func GetUserHandler(w http.ResponseWriter, r *http.Request) {
row := db.QueryRow("SELECT * FROM users WHERE id = $1", id)
var u User
row.Scan(&u.ID, &u.Name, &u.Email)
if u.Email == "" {
u.Email = "default@example.com" // 业务规则散落在 handler 里
}
json.NewEncoder(w).Encode(u)
}
- 换数据库? SQL 散落在 handler 里,改到死
- 加 gRPC 入口? 业务逻辑和 HTTP 耦合,得复制一遍
- 写单元测试? 必须启动真数据库才能测业务逻辑
- 业务规则在哪? 散落在 handler、middleware、SQL 里,没人说得清
根源:所有东西耦合在一起,没有边界。
同心圆模型
┌─────────────────────────────────────────────┐
│ Frameworks & Drivers (最外层) │
│ HTTP server, DB driver, gRPC, 第三方 SDK │
│ ┌─────────────────────────────────────────┐ │
│ │ Interface Adapters │ │
│ │ Handler, Repository 实现, Presenter │ │
│ │ ┌─────────────────────────────────────┐│ │
│ │ │ Use Cases (Application Layer) ││ │
│ │ │ 业务流程编排 ││ │
│ │ │ ┌─────────────────────────────────┐││ │
│ │ │ │ Entities (Domain Layer) │││ │
│ │ │ │ 核心业务规则、领域模型 │││ │
│ │ │ └─────────────────────────────────┘││ │
│ │ └─────────────────────────────────────┘│ │
│ └─────────────────────────────────────────┘ │
│ 依赖方向:外 → 内(永远不反过来) │
└─────────────────────────────────────────────┘
Go 项目中的映射
myapp/
├── domain/ # Entities — 最内层,零依赖
│ ├── user.go # 领域模型 + 业务规则
│ └── errors.go # 领域错误定义
├── usecase/ # Use Cases — 业务流程编排
│ ├── user_service.go
│ └── port.go # 接口定义(Repository interface 等)
├── adapter/ # Interface Adapters — 转换层
│ ├── handler/ # HTTP handler(dto ↔ domain 转换)
│ ├── repository/ # Repository 实现(domain ↔ gen 转换)
│ └── dto/ # 请求/响应结构体
├── infra/ # Frameworks & Drivers — 最外层
│ ├── db/ # 数据库连接、sqlc 生成代码
│ ├── server/ # HTTP server 启动配置
│ └── config/ # 配置加载
└── main.go # 组装所有依赖(Dependency Injection)
每一层的职责
Domain(实体层)— 零依赖,只用标准库
package domain
type User struct {
ID int64
Name string
Email string
}
func (u *User) Validate() error {
if len(u.Name) < 2 {
return errors.New("name must be at least 2 characters")
}
if u.Email == "" {
return ErrEmailRequired
}
return nil
}
Use Case(用例层)— 定义接口,编排流程
package usecase
// Port:定义"我需要什么能力",不关心谁提供
type UserRepository interface {
GetByID(id int64) (*domain.User, error)
Save(user *domain.User) error
}
type UserService struct {
repo UserRepository // 依赖接口,不依赖实现
}
func (s *UserService) Register(name, email string) (*domain.User, error) {
user := &domain.User{Name: name, Email: email}
if err := user.Validate(); err != nil {
return nil, err
}
if err := s.repo.Save(user); err != nil {
return nil, err
}
return user, nil
}
Adapter(适配器层)— 做类型转换
// repository 实现:gen.User ↔ domain.User
func (r *PostgresUserRepo) GetByID(id int64) (*domain.User, error) {
row, err := r.q.GetUser(context.Background(), id)
if err != nil {
return nil, domain.ErrUserNotFound
}
return &domain.User{ID: row.ID, Name: row.Name, Email: row.Email}, nil
}
// handler:domain.User ↔ dto
func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
json.NewDecoder(r.Body).Decode(&req)
user, err := h.svc.Register(req.Name, req.Email)
// ...
json.NewEncoder(w).Encode(toUserResponse(user))
}
main.go — 组装一切(依赖注入)
func main() {
repo := repository.NewPostgresUserRepo(gen.New(db))
svc := usecase.NewUserService(repo)
handler := handler.NewUserHandler(svc)
// 路由注册...
}
依赖反转:内层怎么调用外层又不依赖外层
usecase 包定义接口: type UserRepository interface { ... }
↑ 实现
adapter 包实现接口: type PostgresUserRepo struct { ... }
接口定义在消费方(usecase),实现在提供方(adapter)。Go 的隐式接口让这件事特别自然。
这带来的能力:
- 换数据库:写一个新的 repo 实现同一接口,改 main.go 注入
- 写测试:mock 掉 repository,纯内存测试业务逻辑
- 加 gRPC:新 handler 调同一个 service,业务逻辑零改动
sqlc 场景下的三层类型
HTTP Request
↓
dto.CreateUserRequest ← API 边界:JSON tags、字段裁剪
↓ (mapping)
domain.User ← 业务核心:业务规则、Go 原生类型
↓ (mapping)
gen.User ← 数据库镜像:sql.NullString 等 DB 类型
↓
PostgreSQL
三层各自因不同原因变化:schema 迁移只影响 gen,业务规则变化只影响 domain,API 版本变化只影响 dto。mapping 样板代码是解耦的代价,直接写显式转换函数即可。
什么时候不需要
- 纯 CRUD(domain model 和 DB schema 一模一样)→ 省掉 domain 层,两层够了
- 脚本/CLI → 不分层
- 原型阶段 → 先快速验证,业务复杂度上来再重构
判断标准:domain model 和数据库 schema 之间有没有真正的语义差距? 没有就不要硬分。
Pitfalls
- 贫血模型:domain.User 只有字段没有方法,所有逻辑在 service 里 → 分层失去意义,业务规则应下沉到 entity
- 过度抽象:不是每个外部依赖都需要 interface,只在需要替换实现或测试隔离时才抽
- 接口定义在错误的地方:Go 惯例是 accept interfaces, return structs。
UserRepository定义在usecase包而非repository包 - 跨层传指针然后修改:mapping 时创建新对象,不要把
gen.User指针传到 handler 再改字段