Go项目必须分层,否则两周后handler将混杂SQL、HTTP调用等逻辑;Repository接口须定义在service或domain层以实现解耦,确保换数据库时仅需重写repo实现。

Go 项目不做分层,两周后 handler 就会塞满 SQL、HTTP 调用、日志和校验逻辑,再改一个字段就得 grep 全局、提心吊胆。
为什么必须用 interface 定义 Repository
Repository 接口不能写在 repository/ 目录下,而要定义在 service/ 或 domain/ 层——这是 Go 解耦的铁律。否则 service 层直接 import *sql.DB 或 pgxpool.Pool,等于把数据实现细节焊死在业务里。
- 接口定义位置错误:把
UserRepo接口放在repository/下 → service 不得不依赖具体 repo 包,换数据库时要改所有 service 文件 - 正确做法:在
service/user/service.go里声明type UserRepo interface { GetByID(id int64) (*entity.User, error) },让 repo 实现它 - 好处:单元测试时可轻松注入 mock 实现;上线后切换 PostgreSQL → Redis → ES,只需重写
repository/pgx/,service 和 handler 一行不动
Handler 层只许做三件事:解析、转换、响应
Handler 不是“逻辑入口”,而是“协议翻译器”。它连 database/sql 都不该 import,更别说拼 SQL 或调第三方 API。
- 允许做的事:
c.ShouldBindJSON(&req)、validator.Validate(req)、service.CreateUser(req.ToEntity())、res.JSON(c, 201, res.Data(resp)) - 禁止做的事:在 handler 里写
if user.Status == 0 { return }(业务规则)、log.Printf("create user %d", id)(日志应由 middleware 或 service 统一埋点)、http.Post(...)(应封装进 service 的 notifier 模块) - DTO struct 必须隔离:handler 用
UserCreateRequest(含username,email),service 用entity.User(不含CreatedAt),repo 用userModel(含ID,CreatedAt)
Service 层事务与错误处理的实操边界
事务控制必须落在 Service,但事务本身不是业务逻辑——它是执行业务的容器。错误码映射也必须在这里完成,不能甩给 Handler。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 事务开启位置:在 service 方法开头调用
tx, err := repo.BeginTx(),结尾统一tx.Commit()或tx.Rollback();Handler 绝不碰BeginTx - 错误不能裸传:service 返回
errors.New("user already exists")是错的;应返回自定义错误类型如errUserExists,并在 service 内部映射为 HTTP 状态码(如http.StatusConflict) - 跨 repo 调用要谨慎:比如借书操作需查
UserRepo+BookRepo+BorrowRepo,必须在一个 service 方法内完成,且共用同一事务,不能拆成多个 service 调用
目录结构里最容易被忽略的陷阱
Go 没有命名空间,目录即包路径,包名混乱会直接破坏依赖流向。internal 下的层级不是摆设,是编译期强制的访问控制。
- 错误结构:
internal/repository/postgres/里 importinternal/handler/→ 编译失败,但开发者常绕过:把 repo 接口硬塞进 handler 包,导致 handler 成为“上帝包” - 推荐结构:
internal/handler/→internal/service/→internal/repository/,且service/包内定义所有 repo 接口,repository/只实现它们 - model 放哪?不要建
model/包:Entity 放internal/entity/(纯结构体),Repository 实现用的 DB 结构体放internal/repository/postgres/model.go,绝不暴露给上层
最常被跳过的一步:在 service 接口方法签名里明确写出 error 类型,比如 GetUser(ctx context.Context, id int64) (*entity.User, *apperror.NotFoundError),而不是笼统的 error。这强迫你在设计阶段就思考错误分类,而不是等线上报 500 internal server error 才去翻日志。

















