Repository接口必须按业务动作命名、返回具体领域结构体、不暴露SQL等底层细节,如FindRecentUnCanceledOrdersByCustomerID而非通用拼凑签名,参数应封装为结构体,禁用泛型和透传底层error。

Go 里写 Repository 不是为了套设计模式,而是为了不让业务逻辑里出现 db.QueryRow、collection.FindOne 或 fmt.Sprintf("SELECT ... WHERE id = %d", id) 这类代码。接口必须按业务动作命名、返回具体结构体、不暴露 SQL/JSON/BSON 细节——否则就只是把数据库调用换个地方写而已。
Repository 接口怎么定义才不算白写
接口方法名要反映真实业务场景,不是数据库操作动词。比如订单模块需要“查客户最近三笔未取消订单”,就该有 FindRecentUnCanceledOrdersByCustomerID,而不是 FindByStatusAndCustomerID 这种通用拼凑型签名。
- 返回类型必须是领域结构体(如
*domain.Order),不能是map[string]interface{}或sql.Rows - 参数尽量收束成一个结构体(如
FindOptions{CustomerID: 123, Limit: 3}),避免后期加字段时疯狂改方法签名 - 别在接口里塞泛型约束(如
Repository[T]),Go 的接口本就靠实现多态,强行泛型反而让 mock/fake 更难写 - 错误统一用自定义 error(如
domain.ErrNotFound),绝不透传sql.ErrNoRows或mgo.ErrNotFound
SQL 和 MongoDB 实现层最容易翻车的点
手拼 SQL 是最常见也是最危险的错误。PostgreSQL 报错 ERROR: operator does not exist: text = integer,往往是因为写了 "WHERE customer_id = " + strconv.Itoa(id) 而不是用占位符。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 所有查询参数必须走
db.Query/db.Exec的变参,由驱动处理绑定,MySQL 用?,PostgreSQL 用$1 - MongoDB 实现里,
bson.M{"email": email}是安全的,但别在 map 里直接拼字符串键(如bson.M{field + "_ts": time.Now()}),容易引入不可控字段名 - 复杂条件别写 if-else 拼 query,SQL 用
squirrel,MongoDB 用bson.D或bson.M组合,保持可读和可测试 - 事务内操作必须共用同一个
*sql.Tx或context.Context(MongoDB),不能每个 repo 自己db.Begin()
测试 Repository 别 mock,用内存实例跑真查询
用 gomock 模拟 FindByID 返回一个假 *User,测不出字段类型不匹配、NULL 值没处理、时间字段没注册 time.Time scan hook 这些问题。
立即学习“go语言免费学习笔记(深入)”;
- SQL 场景优先用 SQLite 内存模式:
sql.Open("sqlite3", ":memory:"),配合db.Exec("CREATE TABLE ...")建表后直接测 JOIN 和分页 - MongoDB 场景可用
mongomock或本地mongod --port 27018 --dbpath /tmp/mongo-test,但 CI 中更推荐前者 - fake 实现比 mock 更可靠:写个
memUserRepo,底层用sync.Map存数据,Create和FindByID走真实逻辑,其余方法直接panic("not implemented") - 每个 test case 必须用新 DB 实例或事务 rollback,否则数据污染会导致偶发失败
真正难的不是写出 Repository 接口,而是每次新增一个查询需求时,克制住“快速加个 FindByXXX” 的冲动,先问一句:这个动作在业务里叫什么?它是否该属于当前 domain?有没有隐藏的缓存、权限或审计要求?这些才是接口签名背后的真实重量。

















