go-zero 怎么建表、怎么创建 Model

「数据库表谁建的?Model 怎么来的?整个流程是什么?」—— 这篇把表与 Model 的关系、goctl 两条生成路线、生成的文件清单、接入 svc 再到在 logic 里用,一条龙讲清。

一句话结论 谁建表 整体流程 方式一·DDL 方式二·已建库 生成了啥 接入 svc logic 使用 自定义 SQL 缓存 踩坑 其他框架 结论

表是「你先建」的,Model 是「goctl 生成的」

go-zero 不负责帮你建库建表——它只负责「读了表结构之后,自动生成一套数据访问代码(Model)」。所以流程是:你先准备表(手写 DDL 或连已有库)→ 跑 goctl model → 得到 model 包 → 注入 svc → 在 logic 里调。

记得住的一句话:建表是 DBA / 迁移工具 / 你自己执行 SQL 的事;写 Model 是 goctl 的事。go-zero 的 model = 基于 sqlx 封装的数据访问层(可选 Redis 缓存),只生成代码、不执行建表语句

go-zero 不建表,它「读表生成代码」

这一点最容易被误会成「goctl 会自动建表」。其实 goctl 只做代码生成,表本身得先存在(或先写好 DDL 文件)。

✅ go-zero / goctl 做
读 DDL 文件 连上已有数据库读表结构 → 生成 model 包(CRUD 代码 + 结构体 + 缓存逻辑)。
❌ go-zero / goctl 不做
不会执行 CREATE TABLE 帮你把表落到库里;不会做数据库迁移(migration)。
坑点先讲:如果你表还没建、直接 goctl model mysql datasource 连空库,会报错「表不存在」。先建表,再生成。建表方式任选其一:手写 .sql 在 MySQL 客户端执行、用 Flyway / golang-migrate 等迁移工具、或 DBA 在库里建好。

从「一张表」到「logic 里调一行代码」

四步主线:准备表 → 生成 Model → 注入 svc → 在 logic 使用。

① 准备表 DDL 文件 / 已建库 ② goctl 生成 model mysql ddl/ds ③ 接入 svc NewXxxModel(conn) ④ logic 用 FindOne/Insert… go-zero 本身不执行 CREATE TABLE —— 表在 ① 就存在了,goctl 只「读」它来生成代码

先写 DDL,再 goctl model mysql ddl

适合:你手头有一份建表 SQL,或想「表结构即文档」,从 SQL 文件生成。

Step 1 · 写建表 SQL(user.sql)

CREATE TABLE user (
  id          BIGINT NOT NULL AUTO_INCREMENT,
  name        VARCHAR(64) NOT NULL DEFAULT '',
  age         INT    NOT NULL DEFAULT 0,
  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  KEY idx_name (name)   -- 二级索引会被识别,生成 FindOneByName
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Step 2 · 真正把表建进库(你执行,不是 goctl)

mysql -uroot -p test < user.sql   # 用客户端 / 迁移工具执行

Step 3 · 生成 Model

goctl model mysql ddl \
  --src user.sql \      # 一个或多个 .sql 文件
  --dir ./model \       # 输出目录
  --cache \             # 开启 Redis 缓存(可选)
  --style gozero        # 代码风格(默认 gozero)
goctl 会扫描 DDL:主键 → 生成 FindOne/Insert/Update/Delete;每个二级索引 KEY idx_xxx(col) → 额外生成 FindOneByXxx。索引写得越全,自带查询越多。

连上「已经建好的库」goctl model mysql datasource

适合:表已经在数据库里了(别人建的 / 老项目),直接反查结构生成代码,连 SQL 文件都不用写。

goctl model mysql datasource \
  --url "root:password@tcp(127.0.0.1:3306)/test" \
  --table user \          # 指定表,可多次 --table 或 --tables "a,b"
  --dir ./model \
  --cache                # 开启 Redis 缓存
ddl 方式
输入:.sql 文件。好处:表结构可纳入版本管理,先有 SQL 再有代码。
datasource 方式
输入:活的数据库连接。好处:不用准备 SQL,库里有啥直接生成啥,最省事。
两条路生成的东西完全一样——都是 model/ 包。区别只在于「信息源」是 SQL 文件还是数据库。

model 目录里都有啥

执行完命令,./model 下会冒出这样一批文件。理解它们角色,才不会手改错文件。

model/ ├── usermodel.go # 接口定义 + 自定义方法实现(手写区) ├── usermodel_gen.go # 自动生成的 CRUD 实现(别手改!) ├── vars.go # 缓存 key 前缀等常量 ├── types.go # User 结构体(列 -> 字段映射) └── errors.go # ErrNotFound 等
文件内容会重生成覆盖吗 usermodel_gen.goInsert / FindOne / FindOneByName / Update / Delete 等基础 CRUD 的真实实现 + 缓存代码,重跑 goctl 即覆盖 usermodel.go定义 UserModel 接口,并声明 customUserModel 内嵌 defaultUserModel;你写的自定义查询方法放这里接口部分会刷新;手写的方法保留 types.go列到 Go 结构体的映射,字段带 db:"create_time" 标签会覆盖(按表结构) vars.go缓存 key 前缀 cacheUserPrefix 等会覆盖 errors.go查询不到时的 ErrNotFound会覆盖

types.go 长这样(列 → 字段映射)

type User struct {
    Id         int64     `db:"id"`
    Name       string    `db:"name"`
    Age        int64     `db:"age"`
    CreateTime time.Time `db:"create_time"`
    UpdateTime time.Time `db:"update_time"`
}
snake_case 列名自动对应 CamelCase 字段,映射靠生成时写入的 db 标签;改列名后重新生成即可,不要手改这个结构体

在 ServiceContext 里把 Model 建出来

和 DB 连接、Redis 一样,Model 也是「进程级共享依赖」,在 svc.NewServiceContext 里建一次、注入 svc。

① config.go 加两行

type Config struct {
    rest.RestConf
    Mysql     sqlx.MysqlConf   // 数据源
    CacheRedis redis.RedisConf // 开 --cache 才需要
}

② svc/servicecontext.go 建 Model

type ServiceContext struct {
    Config   config.Config
    UserModel model.UserModel // 注入 Model
}

func NewServiceContext(c config.Config) *ServiceContext {
    conn := sqlx.NewMysql(c.Mysql.DataSource) // 一个连接池
    return &ServiceContext{
        Config:    c,
        UserModel: model.NewUserModel(conn, c.CacheRedis), // 传连接 + 缓存
    }
}
一个 sqlx.NewMysql 连接池可以被多个 Model 共用(NewUserModel(conn, ...)NewOrderModel(conn, ...) 都传同一个 conn),连接只建一份。这正是 go-zero 把依赖都收编进 svc 的好处——见「svc 目录」「初始化」「全局变量」几篇。

logic 里直接调,像本地方法

Model 注入 svc 后,任意 logic 通过 l.svcCtx.UserModel 使用,自带 context 透传与(若开 cache)缓存。

// 查
user, err := l.svcCtx.UserModel.FindOne(l.ctx, userId)
if errors.Is(err, model.ErrNotFound) {
    return nil, err
}

// 按索引查(建表时写了 KEY idx_name 才有)
u, err := l.svcCtx.UserModel.FindOneByName(l.ctx, name)

// 增
_, err = l.svcCtx.UserModel.Insert(l.ctx, &model.User{Name: "张三", Age: 12})

// 改
user.Age = 13
l.svcCtx.UserModel.Update(l.ctx, user)

// 删
l.svcCtx.UserModel.Delete(l.ctx, user.Id)

整个链条回顾:表(你建)→ goctl 生成 model → svc 注入 UserModel → logic 里 l.svcCtx.UserModel.FindOne(...)。go-zero 帮你省掉的是:手写 SQL、手写 struct 映射、手写增删改查、手写缓存——这些全在 _gen.go 里自动完成。

自带 CRUD 不够?在接口里加方法

聚合查询、连表、复杂 WHERE —— goctl 生成不了,但你可以在 _gen 文件里扩展,重生成不会被覆盖。

// 1) 在 usermodel.go 的 UserModel 接口加签名
UserModel interface {
    model.UserModel      // 内嵌生成的基础接口
    FindAdults(ctx context.Context) ([]*User, error) // 自定义
}

// 2) 在同一文件的 customUserModel 上实现(手写,不会被覆盖)
func (m *customUserModel) FindAdults(ctx context.Context) ([]*User, error) {
    query := `SELECT `+userRows+` FROM `+m.table+` WHERE age >= ?`
    var resp []*User
    err := m.conn.QueryRowsCtx(ctx, &resp, query, 18)
    return resp, err
}
千万别把自定义 SQL 写进 usermodel_gen.go——那个文件下次 goctl model 会被整体覆盖,你的代码瞬间消失。约定:生成的写 _gen.go,手写的写 usermodel.go

--cache 开了什么

--cache 后,goctl 把主键 / 唯一索引查询的 Redis 缓存逻辑直接写进 _gen.go:查先打缓存、命中即返;写操作自动删对应缓存。

自动做了
FindOne / FindOneByXxx 读缓存;Insert/Update/Delete 删缓存;缓存 key 在 vars.go 里按 cacheUserPrefix + 主键拼。
要你给
config 里的 CacheRedis redis.RedisConf(连 Redis 的地址),否则启动报错。
一致性提醒:自动缓存只覆盖「主键 / 唯一索引」类的生成查询。你自己写的自定义 SQL(如 FindAdults)默认不进缓存——要么别缓存,要么在自定义实现里手动管缓存 key。涉及金额、库存等强一致数据,可以针对该 Model 关掉缓存或走「写后强制失效」。

六个最容易翻车的地方

坑现象 / 说明正确做法 以为 goctl 建表连空库生成报「表不存在」先建表(SQL/迁移工具),再生成代码 手改 _gen.go重生成后自定义逻辑消失自定义只写 usermodel.go 忘了加二级索引没有 FindOneByXxx,只能手写建表时把常用查询列写成 KEY idx_xxx 开 --cache 却没配 Redis启动即报错缺 CacheRedisconfig 加 CacheRedis 并配地址 字段类型不匹配decimal / json 列映射错用 string 接、或自定义类型 + 扫描方法 改了表结构代码还用旧字段改表后重跑 goctl 重新生成 model

其他框架怎么「建表 + 建 Model」

目标都是「表 ↔ 代码」对齐,机制差异很大。

框架建表建 Model / 映射 go-zero不建(你先建表)goctl model 读表/DDL 生成 sqlx 代码;不建表 GORMdb.AutoMigrate(&User{}) 自动建/改表struct 即表,tag 定义列;运行时反射 sqlx(裸)你全手写手写 struct + 手写 SQL,最自由最累 entschema 代码生成迁移Schema-as-Code,codegen 出类型安全的 client XORMengine.Sync(&User{}) 同步表struct tag 映射,Sync 自动补表结构
go-zero 的哲学和「结构体即表 + 自动迁移」相反:它把建表权和代码生成权拆开——表由你(或 DBA)掌控,代码由 goctl 批量产出。好处是生产环境表结构稳定、不靠框架偷偷改表;代价是多一个「建表」步骤。

记住这条主线就够

建表是「你」的事(DDL / 迁移工具)→ Modelgoctl model mysql ddl|datasource 从表或 SQL 生成的事(只产代码,不落表)→ 接入是 svc 里 NewXxxModel(conn, cache) 的事 → 使用是 logic 里 l.svcCtx.XxxModel.FindOne(...) 的事。

一句话
go-zero 不建表,只「读表生成 Model 代码」。
两条生成路线
ddl(从 SQL 文件)/ datasource(从活库),产物相同。
文件边界
自定义代码写 usermodel.go,别碰 _gen.go
缓存
--cache 自动管主键/唯一索引缓存,自定义查询自管。
下一篇可接着看:「model 自定义 SQL 与事务」「多表关联的 Model 组装」「go-zero + 迁移工具(golang-migrate)协同」。需要我补哪篇说一声。