Repository是业务数据访问契约,接口需按用例命名(如FindUnshippedOrdersSince)、接收完整结构体、禁用SQL拼接、测试用SQLite内存模式、避免过早泛型抽象。

Repository 在 Go 里不是 ORM 封装层,也不是通用 CRUD 工具类——它是一份业务数据访问契约。写错接口定义、过早抽象、手拼 SQL、mock 数据库连接,这四件事干一件,后续半年都在填坑。
Repository 接口命名必须体现业务动作,不能照搬数据库操作
常见错误是定义 FindByID、Update、Delete 这类泛化方法,结果业务方要查“过去7天未发货的订单”,只能硬塞进 FindByStatusAndCreatedAtBefore,接口越加越多,语义越来越模糊。
正确做法是按用例来:
-
FindUnshippedOrdersSince(days int) 返回[]Order -
FindActiveOrdersByCustomerID(id int) 不返回*sql.Rows,也不暴露status = 'active'这种条件细节 -
Create接收完整结构体(如order.Order),而非一堆字段参数或*sql.DB
这样 Service 层调用时意图清晰,后续换成缓存或搜索服务,只要实现该接口,上层完全无感。
立即学习“go语言免费学习笔记(深入)”;
Repository 实现里禁止字符串拼接 SQL,必须用占位符或查询构建器
翻车现场:"SELECT * FROM orders WHERE customer_id = " + strconv.Itoa(id)
这行代码同时触发三类问题:SQL 注入、PostgreSQL 类型报错(整数不能直接拼进字符串)、无法复用预编译语句。
必须走标准路径:
- 使用
db.Query/db.Exec的变参机制,让驱动自动绑定参数(?或$1) - 复杂条件用
squirrel构建:squirrel.Select("*").From("orders").Where(squirrel.Eq{"status": "unshipped", "created_at": squirrel.Gt{time.Now().AddDate(0,0,-7)}}) - 绝不出现
fmt.Sprintf拼 SQL、绝不透传sql.ErrNoRows、绝不返回map[string]interface{}
如果连 squirrel 都嫌重,至少用 sqlx 的命名参数(sqlx.Named)保底。
测试 Repository 别 mock,用 SQLite 内存模式跑真实查询
Mock QueryRow 看似快,但掩盖了最要命的问题:
- 字段名拼错导致
Scan失败 -
NULL值没处理,sql.NullString被当成普通string - JOIN 后列顺序错乱,
Scan顺序对不上 - 时间字段类型不匹配(
datetimevstext)
推荐方案:
-
gorm.Open(sqlite.Open(":memory:"), ...)或sqlx.Connect到内存 DB - 每个 test case 初始化新 DB 实例,或用
tx+Rollback隔离 - 提前
AutoMigrate表结构(GORM)或手动Exec("CREATE TABLE...")(原生database/sql)
fake 实现(如 memOrderRepo)只在需要验证并发逻辑或快速原型时用,别让它成为默认测试手段。
泛型 BaseRepository[T] 是早期陷阱,先写死接口再抽象
有人一上来就定义:
type BaseRepository[T any] interface {
Save(*T) error
FindByID(id int) (*T, error)
}结果发现:
-
T无法约束含ID字段,FindByID实现不了通用逻辑 -
UserRepository和OrderRepository仍是独立接口,泛型没带来复用 -
GORM的First、Where方法签名强依赖具体 struct,泛型擦除后根本套不上
务实路径是:
- 先为每个领域实体写独立接口(
UserRepository、OrderRepository) - 抽取共用逻辑到工具函数(如
Paginate(db *sqlx.DB, req PageReq, dest interface{}) error) - 等真有 3+ 个 repo 出现高度相似的增删改查流程,再考虑组合或有限泛型(如
type GenericRepo[T IDer] struct {...})
真正的复杂点从来不在“怎么抽象”,而在于怎么让接口签名守住业务边界——一旦开始往里塞 context.Context、sql.Tx、cache.Client,说明契约已经失焦。


















