go-zero 后端开发完整流程:从「定义接口」到「logic 上线」

把「写 .api 定义接口 → 建表 → goctl 生成 Model → 装配 svc → 写 logic 业务逻辑 → 跑起来」这条端到端链路一次讲清,并专门说清楚每一步对应的 goctl 自动化命令。

一句话结论 完整全景图 ① 定义接口 ② 建表 ③ 生成代码 ④ 装配 svc ⑤ 写 logic ⑥ 运行验证 goctl 命令速查 端到端示例 规范清单 框架对比

一句话结论

先把最关键的说在前面。

go-zero 的后端开发是「声明式契约 + 代码生成」模式:你用 .api(HTTP)/ .proto(RPC)描述接口、用 .sql 或现成的库描述表,goctl 把骨架代码(handler / logic / types / model / svc 骨架)一次性生成出来,你只在 logic 里填业务。大部分重复性代码(路由注册、参数绑定、CRUD、缓存)都不用你手写。

记住这条主线就够了:你写「契约」→ 命令「生成」→ 你填「logic」。下面每一步都会标注「这一步是你手写,还是 goctl 生成」,以及对应的命令。

完整全景图:一个接口从零到上线

左到右:你在哪写、命令在哪跑、产物去哪、最终谁干活。

① 你写契约 .api / .proto / .sql ② goctl 生成 handler/logic/model ③ 你填 logic 业务逻辑 ④ svc 装配 DB/RPC 注入 ⑤ 启动部署 main + Docker 每一步「谁来做」对照: · 定义接口(路由/类型) → 你写 .api,goctl 生成 handler+types+route · 定义 model(数据访问) → 你建表,goctl 生成 model 包(含缓存) · 业务逻辑 → 你写 logic(svc 给你注入好 DB/RPC/Cache 依赖)

① 定义接口:写 .api 契约

这是「接口」的源头——你描述接口长什么样,不写实现。

一份 user.api:声明路由、分组、请求/响应类型

// user.api
syntax = "v1"
info ( title: "user api" desc: "用户服务" version: "1.0" )

// 数据结构:入参与出参
type RegisterReq {
    Username string `json:"username"`
    Password string `json:"password"`
}
type RegisterResp { Id int64 `json:"id"` }

// 分组配置:统一前缀 + jwt
@server ( jwt: Auth prefix: /user )
// 路由声明
service user-api {
    @handler Register
    post /register (RegisterReq) returns (RegisterResp)

    @handler Login
    post /login (LoginReq) returns (LoginResp)
}
这是你唯一需要「手写声明」的地方:type 定义数据形状、@handler + 方法/路径定义路由、service 把它们归到一个服务。RPC 接口同理,只是换成 .proto

这一份文件决定了三件事:① 会生成哪些 handler(RegisterHandler / LoginHandler);② 会生成哪些 types(RegisterReq 等);③ 路由怎么挂(/user/register 走 POST,且整组要 jwt)。 详细的「API 怎么长出来」见 go-zero API 定义全流程

② 建表:准备数据表(DDL)

go-zero 不帮你建表,它「读表生成 Model」。所以表要你先准备好。

写一份建表 SQL(user.sql)

CREATE TABLE user (
  id       BIGINT NOT NULL AUTO_INCREMENT,
  username VARCHAR(64) NOT NULL DEFAULT '',
  password VARCHAR(128) NOT NULL DEFAULT '',
  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  KEY idx_username (username)   -- 二级索引 → 生成 FindOneByUsername
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

然后你(或 DBA / 迁移工具)把表建进库:mysql -uroot -p test < user.sql。也可以用 golang-migrate 管理建表。

坑:表没建就去连库生成 Model 会报「表不存在」。顺序永远是「先建表 → 再生成 Model」。usernameKEY idx_username 是为了让 goctl 自动生成 FindOneByUsername,少写自定义 SQL。

③ 跑 goctl,生成全部骨架代码

这一步是自动化的核心:两条命令,把 .api 和 .sql 翻译成可编译工程。

3a · 生成 API 工程(handler / logic / types / svc 骨架)

goctl api go -api user.api -dir .     # 在当前目录生成工程

生成后目录多出来:

. ├── internal/ │ ├── types/types.go # RegisterReq 等结构体(来自 .api 的 type) │ ├── handler/ │ │ ├── registerhandler.go │ │ ├── loginhandler.go │ │ └── routes.go # 路由集中注册(自动) │ ├── logic/ │ │ ├── registerlogic.go # 你填业务的骨架 │ │ └── loginlogic.go │ ├── svc/servicecontext.go # 依赖容器骨架 │ └── config/config.go # 配置结构体 ├── etc/user-api.yaml # 运行时配置 └── user.go # main 入口(自带 -f flag)

3b · 生成 Model(数据访问层,含可选缓存)

goctl model mysql ddl \
  --src user.sql \     # 你的建表 SQL
  --dir ./internal/model \
  --cache              # 开 Redis 缓存(主键/唯一索引自动缓存)

生成的 model 包:

internal/model/ ├── usermodel.go # 接口定义 + 自定义方法(手写区) ├── usermodel_gen.go # 自动 CRUD 实现(别手改,会覆盖) ├── types.go # User 结构体(列→字段映射) ├── vars.go / errors.go # 缓存前缀 / ErrNotFound
两条命令的产物完全解耦、互不覆盖:goctl api 管 HTTP 那侧,goctl model 管数据访问。详细的建表/Model 机制见 go-zero 建表与 Model

现在你有了一个能编译但逻辑空的工程:路由、参数绑定、CRUD 全有了,只差往 logic 里塞业务,以及把 Model 接进 svc。

④ 装配 svc:把 Model 收编进依赖容器

go-zero 的依赖注入就发生在这:config 声明要什么,svc 把它们建出来一次、供所有 logic 复用。

4a · config.go 声明数据源

type Config struct {
    rest.RestConf
    Mysql     sqlx.MysqlConf   // 来自 etc/*.yaml 的 DataSource
    CacheRedis redis.RedisConf // 开了 --cache 就必须配
    Auth      struct {
        AccessSecret string
        AccessExpire int64
    }
}

4b · 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), // 共享同一个 conn
    }
}
这一步是「依赖装配」的标准做法:连接池只建一份、多个 Model 共用;所有 logic 通过 l.svcCtx.UserModel 取依赖。不要在每个 logic 里现 new 连接。更深入的 svc 机制见 svc 注入解析初始化写在哪

⑤ 写 logic:真正干活的地方

骨架已就位,你只实现每个接口的 Login/Register 方法,依赖全从 svcCtx 取。

registerlogic.go 填业务

func (l *RegisterLogic) Register(req *types.RegisterReq) (*types.RegisterResp, error) {
    // 1) 查重:直接用生成的 FindOneByUsername
    if _, err := l.svcCtx.UserModel.FindOneByUsername(l.ctx, req.Username); err == nil {
        return nil, errors.New("用户已存在")
    } else if !errors.Is(err, model.ErrNotFound) {
        return nil, err
    }
    // 2) 加密密码(包级工具放 internal 或 pkg,见下方说明)
    hash := utils.HashPassword(req.Password)
    // 3) 入库:生成的 Insert
    res, err := l.svcCtx.UserModel.Insert(l.ctx, &model.User{
        Username: req.Username, Password: hash,
    })
    if err != nil { return nil, err }
    id, _ := res.LastInsertId()
    return &types.RegisterResp{Id: id}, nil
}
logic 里能用的
l.svcCtx 取 Model / RPC 客户端 / Redis;l.ctx 透传 trace / 取消 / 超时(见 ctx 传递规范)。
工具函数放哪
密码哈希这类无状态工具放 internal/utils(单服务)或 pkg/utils(多服务复用),别写成包级全局 var(见 全局变量与 svc)。
如果业务还要调别的内部服务:走 zrpc 客户端(生成到 svc 里);调外部 HTTP:走 httpc(见 内/外部服务调用)。这些都是在 svc 装配、logic 调用,模式完全一致。

⑥ 本地运行与验证

工程齐全,跑起来即可。

本地启动(dev 配置)

go mod tidy              # 拉依赖(首次)
go run user.go -f etc/user-api.yaml   # -f 指定配置文件(main 自带该 flag)
# 验证
curl -X POST localhost:8888/user/register \
  -H 'Content-Type: application/json' \
  -d '{"username":"alice","password":"123"}'
多环境启动时换 -f 指向不同 yaml(dev/test/prod)即可,详见 多环境区分。生产用 Dockerfile 构建镜像,见 服务部署规范

goctl 自动化命令:什么时候跑、生成啥

这是你问的「自动化使用命令的流程」——把每类命令、触发时机、产物一次列清。

命令何时用生成/做了啥常见参数
goctl api go写好 .api 后,新建/整体生成 HTTP 工程handler / logic 骨架 / types / routes / svc 骨架 / config / main / etc yaml-api -dir -style
goctl rpc protoc写好 .proto 后,生成 RPC 服务pb.go / 服务端骨架 / 客户端 stub(zrpc 封装)--go_out --go-grpc_out --zrpc_out
goctl model mysql ddl有建表 SQL 时生成 Modelmodel 包(CRUD + 类型 + 缓存逻辑)--src --dir --cache
goctl model mysql datasource表已在库里,直接反查生成 Model同 ddl(信息源是活库,不用写 SQL)--url --table --dir --cache
goctl api doc想出接口文档时从 .api 生成 markdown / openapi 文档--dir
goctl docker部署前,生成 Dockerfile多阶段构建 Dockerfile(含 -f 启动)--go -o
goctl kube deploy上 K8s 前,生成 yamlDeployment / Service yaml--o
goctl template init想改生成代码风格时拉取可编辑的代码模板(自定义生成产物)--home

典型「首次开发」命令序列:
goctl api go -api user.api -dir .  (出 HTTP 骨架)
goctl model mysql ddl --src user.sql --dir ./internal/model --cache  (出 Model)
③ 手改 config.go + servicecontext.go 把 Model 接进 svc
④ 在 logic/*.go 填业务
goctl docker 出 Dockerfile → 部署

改了表结构?重跑 goctl model 重新生成 _gen.go自定义 SQL 写在 usermodel.go 不会被覆盖。改了 .api?重跑 goctl api go 会刷新对应 handler/logic/types,但已填的 logic 内容一般在合并时保留(建议小步多次生成、别大改 .api 后整体覆盖)。

一个完整示例:Register 接口的「全链路」

把上面五步用一条 Register 请求串起来,看数据流怎么走。

Client POST RegisterHandler解析+绑定 RegisterLogic你写的业务 UserModelInsert→MySQL JSON 响应回 Client

请求 POST /user/registerroutes.go 匹配到 RegisterHandler(goctl 生成的)→ 解析进 RegisterReq(types,goctl 生成)→ 调 RegisterLogic.Register你写的)→ 用 l.svcCtx.UserModel.Insert(goctl 生成的 Model)写库 → 返回 RegisterResp JSON。全程只有 logic 是手写,其余皆生成。

这条链路上每一环都对应前面的某一节:路由/类型来自 ①,handler/route 来自 ③,Model 来自 ②+③,依赖来自 ④,业务来自 ⑤。看不懂哪一环就跳回对应章节。

开发流程规范与踩坑

照着做,流程不翻车。

该做 / 别做说明
✅ 先写契约(.api/.proto/.sql)再生成声明式开发,生成产物以契约为准
✅ 业务只写在 logichandler / _gen.go / routes.go 都不手改,留给 goctl
✅ 自定义 Model 方法写 usermodel.go别碰 _gen.go,重生成会被覆盖
✅ 依赖在 svc 一次性装配连接池/客户端只建一份,logic 通过 svcCtx 取
✅ 建表顺序先于 goctl model表不存在 → 连库生成 Model 直接报错
⚠️ 别在 logic 里现 new 连接破坏连接池复用,且难测试;统一走 svc
⚠️ 改表/改 .api 后重跑对应 goctl否则代码与契约/表不一致,运行时才暴露
❌ 别把环境差异写回代码环境靠 -f + ${ENV} + Mode,不在 logic 散 if env

和「手写 Gin + GORM」比,流程差在哪

目标都是写接口,但「谁生成代码、谁建表」机制不同。

环节go-zero(本文)Gin + GORM(手写)
定义接口写 .api → goctl 生成 handler/route/types手写 route + struct + 绑定
建表你先建表,goctl 读表生成 ModelAutoMigrate 运行时自动建/改表
数据访问goctl 生成 sqlx Model(可选缓存)手写 struct + GORM 调用,或自己封装
依赖注入svc 容器集中装配(编译期)通常 wire / 手动 / 全局 var
重复性代码少(大量生成)多(路由、绑定、CRUD 多手写)
代价多学一套 .api DSL + 生成流程灵活、自由,但样板代码多
go-zero 的标签是「用声明式契约 + 代码生成,把重复劳动交给工具」——适合中大型、多服务、要规范的团队;代价是你需要熟悉 .api / goctl 这套工作流(也就是本篇讲的)。