go-zero 里一个 API 接口,是怎么「长」出来的

从一份 .api 文件,到路由注册、handler、logic —— 定义路由、注册路由、定义接口、handler 这一整套流程,图文 + 代码一次讲清。

总览 写 .api 生成代码 定义路由 注册路由 定义接口/handler 一次请求 速查

整体流程:你只写 .api,剩下的交给 goctl

go-zero 的 API 开发是「声明式」的:你用一份 DSL(.api)描述接口长什么样,goctl 把骨架代码一次性生成出来,你只在 logic 里填业务。

① 写 .api type + service + 路由 ② goctl 生成 handler/logic/svc/types ③ 填 logic 写业务逻辑 ④ 启动服务 main 加载 yaml + 注册路由
核心心智模型:路由 / handler / types 都是「生成的」,你几乎不手改;只有 logic 是你自由发挥的地方。理解这条,下面每步都顺了。

写 .api 文件:接口契约的源头

一份 .api 文件由三块组成:type(数据结构)、@server(分组配置)、service(路由与 handler 声明)。

一个完整的 user.api 示例

// user.api
syntax = "v1"

info (
    title:   "user api"
    desc:    "用户服务接口"
    version: "1.0"
)

// 1) 数据结构:请求与响应的「形状」
type LoginReq {
    Username string `json:"username"`
    Password string `json:"password"`
}
type LoginResp {
    Token    string `json:"token"`
    ExpireAt int64  `json:"expireAt"`
}

// 2) 分组配置:这一组接口共用 jwt 鉴权 + 路径前缀
@server (
    jwt:    Auth
    prefix: /user
)
// 3) service 块:声明路由与对应的 handler 名
service user-api {
    @handler Login
    post /login (LoginReq) returns (LoginResp)

    @handler GetProfile
    get  /profile
}
type 块
定义所有请求/响应结构体,对应生成到 internal/types/types.go(见 types 解析篇)。
@server 块
给一组路由统一加 jwt、middleware、prefix 等。写在 service 上方,对下面的路由生效。
@handler 指令
给这条路由命名 handler(如 Login)。goctl 据此生成 LoginHandler 函数和 NewLoginLogic
post / get 路由行
定义 HTTP 方法 + 路径 + 入参/出参类型。(LoginReq) returns (LoginResp) 绑定数据类型。

跑 goctl,生成骨架代码

一条命令,把 .api 翻译成可编译的 Go 工程。

生成命令

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

生成后多出这些关键文件(和 types 篇目录树对应):

internal/types/types.go
LoginReq / LoginResp 等结构体
internal/handler/*.go
每个 @handler 一个文件 + routes.go
internal/logic/*.go
每个接口一个 logic 骨架(你填业务)
internal/svc/servicecontext.go
依赖容器(见 svc 注入篇)
internal/config/config.go
配置结构体
etc/user-api.yaml
运行时配置(端口、jwt secret…)

「定义路由」发生在 .api 里

你不用手写 mux.HandleFunc。路由的「定义」就是 .api 里那两行:@handler 决定处理函数名,post /login 决定方法 + 路径。

回顾 .api 里的声明:
@handler Login → 生成函数 LoginHandler
post /login (LoginReq) returns (LoginResp) → 方法 POST、路径 /login、绑定入参/出参类型。

@server 的 prefix / jwt 怎么影响路由

上面 prefix: /user 会让最终路径变成 /user/login(前缀 + 路由路径拼接);jwt: Auth 会让这组路由自动要求 JWT 鉴权。这些是「路由定义」的一部分,写在 .api,由生成代码落地。

路由「注册」在 routes.go 里自动完成

goctl 生成的 internal/handler/routes.go 把所有路由集中注册到 rest server。你通常不用动它。

生成出来的 routes.go(节选)

func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
    server.AddRoutes(
        []rest.Route{
            {
                Method:  http.MethodPost,
                Path:    "/user/login",          // prefix + 路由路径
                Handler: LoginHandler(serverCtx), // 指向生成的 handler
            },
            {
                Method:  http.MethodGet,
                Path:    "/user/profile",
                Handler: GetProfileHandler(serverCtx),
            },
        },
        rest.WithJwt(serverCtx.Config.Auth.AccessSecret), // @server jwt 落地
        rest.WithPrefix("/user"),                     // @server prefix 落地
    )
}
注册在哪发生
main.go 调用 handler.RegisterHandlers(restServer, svcCtx),把路由挂到 server 上。
分组即 AddRoutes 批次
每个 @server 分组对应一次 AddRoutes 调用,用 WithJwt/WithPrefix/WithMiddleware 携带分组配置。

handler 是什么:「接口」的薄薄一层壳

客户端真正看到的「接口」= .api 里的路由 + 类型;而 handler 是把 HTTP 请求翻译成对 logic 调用的胶水函数,由 goctl 生成。

HTTP 请求 /user/login + body LoginHandler 解析/绑定 req → 调 logic LoginLogic.Login 业务逻辑(你写) JSON 响应

生成出来的 LoginHandler(节选)

func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var req types.LoginReq
        if err := httpx.Parse(r, &req); err != nil { // 解析路径/query/body
            httpx.ErrorCtx(r.Context(), w, err)
            return
        }
        l := logic.NewLoginLogic(r.Context(), svcCtx) // 拿到 logic
        resp, err := l.Login(&req)                     // 调业务逻辑
        if err != nil {
            httpx.ErrorCtx(r.Context(), w, err)
            return
        }
        httpx.OkJsonCtx(r.Context(), w, resp)           // 写 JSON 响应
    }
}

handler 三件事:① 用 httpx.Parse 把请求绑定到 types.LoginReq;② 用 NewXxxLogic(ctx, svcCtx) 造 logic;③ 调 logic 方法并写回 JSON。 它不写业务 —— 业务在 logic。

logic 骨架(你在这里写业务)

type LoginLogic struct {
    ctx    context.Context
    svcCtx *svc.ServiceContext   // 所有依赖从这里取
}

func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
    return &LoginLogic{ctx: ctx, svcCtx: svcCtx}
}

func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err error) {
    // TODO: 查库、校验密码、签发 token,用 l.svcCtx.UserModel / l.svcCtx.UserRpc ...
    return
}

一次请求的完整生命周期

把「定义路由 → 注册路由 → handler → logic」连成一条线,看一个 POST /user/login 是怎么走完的。

1

路由匹配(routes.go 已注册)

请求打到 rest server,按方法 + 路径 POST /user/login 匹配到 LoginHandler

2

handler 解析绑定

httpx.Parse 把 body 解析进 types.LoginReq;若开启 jwt,先校验 token(@server jwt 生效)。

3

进入 logic

NewLoginLogic(ctx, svcCtx).Login(&req),业务逻辑从这里开始,通过 svcCtx 取 DB/RPC/Cache 等依赖。

4

写回响应

logic 返回 *types.LoginResp,handler 用 httpx.OkJsonCtx 序列化成 JSON 返回客户端。

Client 请求 routes.go 路由表 LoginHandler LoginLogic JSON 响应

速查:谁来定义、谁来注册、谁写业务

环节你在哪写谁生成的作用
定义路由.api@handler + 方法/路径你手写声明接口契约
数据形状.apitypegoctl → types.go请求/响应结构体
注册路由不用写goctl → routes.go把路由挂到 rest server
定义接口(handler)不用写goctl → handler/*.go解析请求、调 logic、写响应
业务逻辑logic/*.gogoctl 出骨架,你填真正干活的地方
依赖装配svc/servicecontext.gogoctl 出骨架,你补DB/RPC/Cache 注入 logic
一句话记忆:路由和 handler 在 .api 里「声明」、由 goctl「生成注册」;业务在 logic 里「手写」。 想加一个接口?先在 .api 加两行,再跑 goctl,最后去 logic 填代码——三步完事。