9.2 mockgen 与 sqlc/ent
9.1 的 stringer 只生成一个方法,是代码生成的「最小演示」。工程里真正消耗人力的重复代码在另外两个地方:测试里的 mock 和 数据访问层(DAO/Repository)。这两块都有成熟的生成器。
这一节实测三个主流工具:mockgen(接口 mock)、sqlc(SQL → 类型安全代码)、ent(schema → ORM)。它们覆盖了「测试」与「数据访问」这两个最值得生成的领域。
本节要回答的问题是:三类重型生成器各自解决什么问题、怎么接入工程、有哪些实测坑。结论先行:
mockgen(go.uber.org/mock v0.6.0)从接口生成 mock,用go get -tool锁定版本即可;sqlc(v1.31.1)从 SQL 查询生成类型安全的 Go 代码,是「SQL 优先」路线的代表;ent(v0.14.6)从 schema 生成 ORM,但实测在 Go 1.27.0 工具链下生成失败(报internal error: package "context" without types),需要换用更旧的 Go 工具链(把go指令降到1.24.0后以GOTOOLCHAIN=local运行)才能跑通——这是本节最重要的诚实记录。
9.2.1 三类生成器的定位
先把三个工具放回它们各自的位置:
| 工具 | 输入 | 输出 | 解决的问题 |
|---|---|---|---|
mockgen | 一个 Go 接口 | 该接口的 mock 实现 | 测试时替换依赖 |
sqlc | SQL schema + query | 类型安全的 Go 函数 | 手写 DAO 的重复 |
ent | Go 写的 schema 定义 | 完整的 ORM 客户端 | 实体关系与查询构建 |
它们的共同点是:都有一个「单一事实来源」——mockgen 的事实来源是接口,sqlc 是 SQL,ent 是 schema。生成器把这份事实翻译成 Go 代码,从而消除手工同步。
9.2.2 mockgen:接口的 mock 实测
先定义一个接口,并在它上方写 go:generate:
package store
//go:generate go tool mockgen -source=store.go -destination=mock_store.go -package=store
import "context"
type Store interface {
Get(ctx context.Context, key string) (string, error)
Put(ctx context.Context, key, val string) error
}
安装并锁定版本:
$ GOTOOLCHAIN=go1.27.0 GOPROXY=https://goproxy.cn,direct \
go get -tool go.uber.org/mock/mockgen@latest
go: added go.uber.org/mock v0.6.0
生成并验证:
$ GOTOOLCHAIN=go1.27.0 go generate ./store/
$ ls store/
mock_store.go store.go
$ GOTOOLCHAIN=go1.27.0 go build ./... && GOTOOLCHAIN=go1.27.0 go vet ./...
build+vet OK
生成物开头:
// Code generated by MockGen. DO NOT EDIT.
// Source: store.go
//
// Generated by this command:
//
// mockgen -source=store.go -destination=mock_store.go -package=store
//
// Package store is a generated GoMock package.
package store
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockStore is a mock of Store interface.
type MockStore struct {
ctrl *gomock.Controller
recorder *MockStoreMockRecorder
isgomock struct{}
}
注意生成物把生成命令原样记在了文件头——这是好习惯,任何人看到文件就知道它怎么来的。isgomock struct{} 是一个编译期标记字段,用于防止 mock 类型被误当作真实实现传递。
9.2.3 mockgen 的两种模式
mockgen 有两种常用工作模式,很多人只用过一种:
| 模式 | 命令 | 原理 | 适用 |
|---|---|---|---|
| source | -source=store.go | 解析 Go 源文件 | 接口在本地文件 |
| package | -destination=... example.com/pkg Store | 编译期反射 | 接口来自依赖包 |
source 模式(上面用的)直接读 .go 文件,简单直接,但要求接口源码在本地。package 模式会真正编译并加载那个包,用反射拿到接口的完整方法集,能处理来自第三方依赖的接口——代价是更慢,且要求那个包能被编译。
生成 mock 后的典型用法:
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := NewMockStore(ctrl)
m.EXPECT().Get(gomock.Any(), "k").Return("v", nil)
go.uber.org/mock 是 github.com/golang/mock 的官方继任者——后者已归档,新项目应当用前者。这是本节要提醒的一个易错点:网上大量教程还在用已停止维护的旧路径。
9.2.4 sqlc:从 SQL 生成类型安全代码
sqlc 走的是「SQL 优先」路线:你写 SQL,它生成与之对应的类型安全 Go 函数。
安装:
$ GOTOOLCHAIN=go1.27.0 GOPROXY=https://goproxy.cn,direct \
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
$ $(go env GOPATH)/bin/sqlc version
v1.31.1
配置 sqlc.yaml:
version: "2"
sql:
- engine: "postgresql"
queries: "query.sql"
schema: "schema.sql"
gen:
go:
package: "db"
out: "db"
schema 与 query 各一个文件:
-- schema.sql
CREATE TABLE authors (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
bio TEXT
);
-- query.sql
-- name: GetAuthor :one
SELECT * FROM authors WHERE id = $1;
-- name: ListAuthors :many
SELECT * FROM authors ORDER BY name;
关键在查询上方的 -- name: GetAuthor :one 注释:sqlc 靠它识别「这是一个要生成的查询」,:one/:many 决定返回单条还是切片。生成:
$ $(go env GOPATH)/bin/sqlc generate
$ ls db/
db.go models.go query.sql.go
生成的 db.go 开头:
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package db
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
sqlc 生成的东西有三个特点值得注意:
- 它不生成 ORM,生成的代码直接用
database/sql。这让它很轻,也容易理解。 - 参数类型是静态的:
GetAuthor(ctx, id int64)的参数类型来自 schema 里id BIGSERIAL的推导,写错类型编译期就报错。 - 它把「SQL 是事实来源」落到实处:改 SQL → 重跑
sqlc generate→ Go 代码自动跟上。不存在「改了 SQL 忘了改 Go 结构体」的问题。
9.2.5 ent:从 schema 生成 ORM
ent 的路线是「schema 即代码」:你用 Go 写实体定义,它生成整套 ORM 客户端。
安装并脚手架:
$ GOTOOLCHAIN=go1.27.0 GOPROXY=https://goproxy.cn,direct \
go install entgo.io/ent/cmd/ent@latest
$ $(go env GOPATH)/bin/ent new User
$ ls ent/schema/
user.go
注意
ent new的实体名必须以大写字母开头,否则报错schema names must begin with uppercase(实测踩到)。
编辑 schema,加上字段:
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
type User struct {
ent.Schema
}
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.Int("age"),
}
}
func (User) Edges() []ent.Edge { return nil }
9.2.6 实测踩坑:ent 与 Go 1.27 的 go.mod
这是本节最需要如实记录的一段。在 go.mod 写着 go 1.27.0 的项目里直接生成:
$ GOTOOLCHAIN=go1.27.0 ent generate ./ent/schema
internal error: package "context" without types was imported from "entgo.io/ent"
生成失败,报的是一句含义模糊的 internal error: package "context" without types。这是 ent 内部的 go/packages 加载器与新版 Go 工具链不兼容导致的——它无法正确加载标准库包的类型信息。
把 go.mod 的 go 指令降到 1.24.0,让本地更旧的 Go 工具链(go1.26.0)被选中后重试:
$ sed -i '' 's/^go 1.27.0/go 1.24.0/' go.mod
$ GOTOOLCHAIN=local ent generate ./ent/schema
$ find ent -name '*.go' | wc -l
20
成功了,生成了 20 个 .go 文件,包括 user.go、user_create.go、user_query.go、user_update.go、user_delete.go 以及 migrate/、predicate/、hook/ 等子包。构建:
$ GOTOOLCHAIN=local go build ./ent/...
ent build OK
结论与提醒:ent v0.14.6 在本机用 Go 1.27.0 工具链会生成失败,换用旧工具链(go1.26.0)即可;把 go 指令降到 1.24 只是为了让本地旧工具链被选中。这不代表 ent 不能用,而是提醒你——生成器自身的工具链兼容性也是依赖治理的一部分。遇到这种 internal error,第一反应应该是「换一个 Go 工具链版本」,而不是怀疑自己的 schema 写错了。同时,ent 生成的代码量远大于 sqlc(20 个文件 vs 3 个),这是「全功能 ORM」的代价。
9.2.7 三者对照表
| 维度 | mockgen | sqlc | ent |
|---|---|---|---|
| 事实来源 | Go 接口 | SQL | Go schema |
| 生成量 | 1 个文件 | 3 个文件 | 20 个文件 |
| 运行期依赖 | go.uber.org/mock | 仅 database/sql | entgo.io/ent |
| 学习成本 | 低 | 低 | 中高 |
| 实测版本 | v0.6.0 | v1.31.1 | v0.14.6 |
| 本机实测坑 | 无 | 无 | Go 工具链须 ≤ 1.26 |
选型建议:
- 测试用 mock →
mockgen,几乎无争议。 - 喜欢手写 SQL、要轻量 →
sqlc。 - 需要实体关系、迁移、复杂查询构建 →
ent。
9.2.8 生成器接入工程的通用姿势
三个工具虽然不同,但接入工程的姿势是统一的,可以总结成四条:
- 用
//go:generate收口:生成命令写在源文件里,go generate ./...一把梭。 - 用
go get -tool锁版本:别依赖「我本机装了什么」。 - 提交生成物 + CI 校验一致:
go generate ./... && git diff --exit-code。 - 生成物只读:文件头的
DO NOT EDIT是契约,要改就改事实来源。
stringer、mockgen、sqlc、ent 都是「现成的生成器」。当你需要的东西没有现成工具时,就得自己写一个——这正是下一节的主题:用 go/ast 造一个属于自己的生成器。
阅读导航:上一节:9.1 go generate 与 stringer · 下一节:9.3 自研代码生成器与 go/ast 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。