Go项目需立即划清handler/service/repo边界,否则代码迅速沦为混杂SQL、API调用等的大杂烩;分层使各层仅依赖下层接口,便于替换数据库、加缓存等,且通过三层独立struct(UserRequest/User/UserModel)隔离数据模型,提升可维护性与安全性。

为什么 Go 项目一上来就该划清 handler / service / repo 边界
因为 Go 没有强制的框架约束,不手动隔离职责,两周后代码就会变成 handler 里拼 SQL、调第三方 API、写日志、做参数校验的大杂烩。这不是风格问题,是维护成本爆炸的起点。
分层不是为了“看起来规范”,而是让每个包只依赖更底层的接口(而非具体实现),后续替换数据库、加缓存、切 RPC 协议时,只需重写 repo 或 service 的实现,handler 完全不动。
-
handler层只处理 HTTP 生命周期:解析请求、校验结构体(用validator)、调service方法、封装响应 -
service层专注业务逻辑:事务控制、领域规则判断、组合多个repo调用;它不能直接 importdatabase/sql或http.Client -
repo层只干一件事:把service给的 domain model 映射成 SQL/Redis/ES 操作;返回的也必须是 domain struct,不能暴露*sql.Rows或redis.Cmdable
如何定义跨层传递的数据结构:别用 database struct 做 API 返回
常见错误是把 UserModel(带 CreatedAt、UpdatedAt、ID 等字段)直接从 repo 透传到 HTTP 响应。这会导致:前端拿到敏感字段、数据库改表结构时所有上层都得改、无法对不同端(App/Web/API)做字段裁剪。
正确做法是三层各用各自的 struct:
-
handler用UserRequest接参,UserResponse返回(字段名可与前端约定一致,比如user_id) -
service用User(纯业务字段,无 DB 元信息,如Balance float64) -
repo用UserModel(含ID,CreatedAt,DeletedAt等 ORM 所需字段)
转换不要手写,用 mapstructure 或轻量级工具(如 copier),但注意:只在层边界做一次转换,别在 service 里又转一遍 User → UserModel。
repo 层怎么写才真正可测试、可替换
关键不是“用了 interface”,而是 interface 是否只描述“我要做什么”,而不是“DB 怎么做”。比如这个定义就错了:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
type UserRepository interface {
GetByID(id int) (*UserModel, error)
Update(u *UserModel) error
}
它绑死了主键类型(int)和更新方式(全量)。换成:
type UserRepository interface {
FindOne(ctx context.Context, filter map[string]interface{}) (*User, error)
Save(ctx context.Context, u *User) error
}
好处是:filter 可以是 map[string]interface{"id": "u_123"}(适配 MongoDB),也可以是 map[string]interface{"email": "a@b.com"}(用于登录查用户),Save 也不限定是 INSERT 还是 UPSERT。
- 测试时用内存 map 实现
UserRepository,完全绕过数据库 - 上线后想切 TiDB?只要新写一个
TiDBUserRepo实现同一接口即可 - 千万别在 interface 里塞
BeginTx()、Exec()这类 DB 特定方法
service 层事务和错误处理的实操陷阱
Go 里事务不是靠“自动回滚”,而是靠显式控制 tx.Commit() / tx.Rollback()。常见错误是:在 service 方法里开了事务,但某个 repo 调用失败后没 return,继续往下走,最后 commit 了部分成功操作。
- 事务必须由
service方法顶层开启,传入各repo方法(通过context.WithValue或参数传递*sql.Tx) - 错误要分类:领域错误(如
ErrInsufficientBalance)用自定义 error,系统错误(DB timeout)直接返回,别统一转成fmt.Errorf("xxx: %w")吞掉原始堆栈 - 别在
repo层做if err != nil { log.Error(...) }—— 日志属于service或handler的职责
复杂业务流程(如下单:扣库存 + 创建订单 + 发消息)建议用状态机或 Saga 模式,而不是在一个 service 方法里硬塞所有逻辑 —— 分层不等于单层巨无霸。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










