go项目需严格划分handler/service/repo边界:handler仅解析参数、校验、调service并封装响应;service定义与实现无关的接口(如findone/save)及纯业务结构体;repo专注数据持久化细节;各层使用独立结构体,转换限于边界;目录按限界上下文而非技术分层组织。

Go 项目一上来不划清 handler / service / repo 边界,两周后就没人敢动核心逻辑——不是代码写得差,是职责混在一起后,改个字段要 grep 十个文件,加个缓存得重写三处 SQL,连单元测试都得跑完整 HTTP 链路。
为什么 handler 里不能直接查数据库或调第三方 API
常见错误现象:handler.GetUser 里直接用 db.Where(...).First(),再顺手调一次 mailer.SendWelcomeEmail()。这会导致:
- 无法对业务逻辑做纯内存测试(必须起 DB + mock 邮件服务)
- 换数据库(比如从 MySQL 切到 PostgreSQL 或 MongoDB)时,
handler层也要跟着改 SQL 写法或驱动调用 - 同一份用户查询逻辑,Web 端和 CLI 命令行工具无法复用——因为
handler绑死了*gin.Context或http.ResponseWriter
正确做法:把数据获取、邮件发送等行为全部下沉,handler 只负责解析参数、校验结构体(用 validator)、调 service.GetUser()、封装响应。它不该知道“用户存在不存在”这件事怎么判定,只该知道“我传 ID 过去,拿回一个 UserResponse”。
service 层怎么定义接口才真正可替换
别写这种绑定实现的 interface:
type UserRepository interface {
GetByID(id int) (*UserModel, error)
Update(u *UserModel) error
}
它暴露了主键类型(int)、ORM 模型(*UserModel)、甚至更新语义(全量覆盖)。换成这个:
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"}(适配 Redis/Mongo),也可以是map[string]interface{"email": "a@b.c"}(用于登录场景) -
*User是 service 层定义的纯业务结构体,不含CreatedAt、UpdatedAt等 DB 元字段,也不含 JSON tag - 测试时,
service层可直接注入一个返回固定*User的 mock repo,完全绕过数据库
三层之间传什么结构体,不能偷懒复用
错误示范:所有层都用同一个 type User struct { ID int; Name string; CreatedAt time.Time },然后从 DB 直接 json.Marshal 返回给前端。
后果:
- 前端拿到
CreatedAt字段,但实际想用的是 ISO8601 格式字符串;你改json:"created_at"tag,结果 repo 层插入也受影响 - DB 加了个
deleted_at字段,所有上层函数签名、文档、测试用例全得改 - App 端要返回
avatar_url,Web 端要返回avatar_small,没法按端裁剪
正确分法:
-
handler层用UserRequest接参(带validate:"required")、UserResponse返回(字段名与前端约定一致,如UserID string `json:"user_id"`) -
service层用User(仅含Name、Balance、Status等业务字段) -
repo层用UserModel(含ID、CreatedAt、UpdatedAt、DeletedAt,以及 GORM tag)
转换只在层边界做一次,用 copier.Copy(&resp, &user) 或 mapstructure.Decode,别在 service 里又手动 new 一个 UserModel。
internal 目录下到底该按技术分层还是按业务分包
别建 internal/handler、internal/service、internal/repo 这种目录。这是最典型的“为了分层而分层”,结果每个包都 import 全局 db 实例,user.Service 直接 new order.Repo,边界形同虚设。
推荐方式:按限界上下文切,每个子目录自包含完整能力链。例如:
-
internal/user/下有domain/(User结构体、UserCreatedevent)、application/(CreateUseruse case 函数)、infrastructure/http/(Gin handler,只调application.CreateUser)、infrastructure/gorm/(UserGormRepo实现) -
internal/user/infrastructure/http不能 importinternal/order/domain,跨上下文通信走异步 event
这样做的真实收益:删掉整个 internal/user/infrastructure/gorm,换成 infrastructure/redis,只要 interface 不变,application 和 http 层完全不动。
最容易被忽略的一点:分层不是为了让代码看起来“更规范”,而是让每次改需求时,你能准确说出“这次改动只影响哪几个文件”,而不是打开 IDE 后发现要改 7 个地方、3 种模型、2 个中间件、1 个全局配置——那说明边界早就塌了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











