Skip to content

API 服务开发

本篇深入讲解 go-zero 的 API 服务开发。我们将系统介绍 .api 文件语法、goctl api 命令的常用选项、业务逻辑分层编写规范(Logic/Handler/Types/SVC),最后通过一个完整的用户管理 CRUD 示例把所有知识点串起来。学完本篇,你将能够独立完成一个中等复杂度的 HTTP 服务开发。

一、.api 文件语法详解

.api 文件是 go-zero 自定义的接口描述语言(IDL),用来声明 HTTP 服务的接口契约。它的语法借鉴了 protobuf 和 go-zero 工程实践,目的是让接口定义集中、可版本化、可生成代码。

1. 文件结构

一个完整的 .api 文件通常包含以下块:

api
syntax = "v1"

info (
    title:   "user api"
    desc:    "用户服务 API"
    author:  "gozero-tutorial"
    version: "v1"
)

import "common.api"

type (
    // 通用结构
    BaseResponse {
        Code int    `json:"code"`
        Msg  string `json:"msg"`
    }
)

@server (
    group: user
    prefix: /api/v1
)
service user-api {
    @handler GetUserHandler
    get /user/:id (GetUserRequest) returns (GetUserResponse)

    @handler CreateUserHandler
    post /user (CreateUserRequest) returns (CreateUserResponse)
}

各块含义:

  • syntax:声明 api 文件版本,目前为 v1
  • info:元信息,用于生成文档和注释
  • import:引入其他 .api 文件(用于复用类型定义)
  • type:定义数据结构
  • @server:声明路由组的配置(前缀、中间件、JWT 等)
  • service:定义服务名与路由

2. import 语句

import 用于引入其他 .api 文件中定义的类型,避免重复定义:

api
// common.api
type (
    Pagination {
        Page     int `json:"page"`
        PageSize int `json:"pageSize"`
    }
)
api
// user.api
syntax = "v1"
import "common.api"

type (
    ListUserRequest {
        Pagination
        Keyword string `json:"keyword,optional"`
    }
)

注意:import 的路径是相对于当前 .api 文件的相对路径,且只能引入类型定义,不能引入 service 块。

3. info 块

info 块用于声明文档元信息,goctl 在生成代码时会把它们写入注释和文档:

api
info (
    title:   "用户服务"
    desc:    "提供用户增删改查能力"
    author:  "team-user"
    version: "v1.0.0"
    email:   "dev@example.com"
)

4. type 块

type 块定义请求/响应结构体,语法接近 Go 的 struct:

api
type (
    CreateUserRequest {
        Name     string `json:"name"`
        Email    string `json:"email"`
        Age      int    `json:"age,optional"`
        Password string `json:"password"`
    }

    CreateUserResponse {
        Id int64 `json:"id"`
    }

    // 嵌套结构
    UserVO {
        Id        int64  `json:"id"`
        Name      string `json:"name"`
        Email     string `json:"email"`
        Age       int    `json:"age"`
        CreatedAt string `json:"createdAt"`
    }

    GetUserResponse {
        User UserVO `json:"user"`
    }
)

struct tag 支持:

  • json:"name":JSON 字段名
  • optional:可选字段(不传不报错)
  • range:范围校验,如 range:"[1,100]"
  • default:默认值
  • options:枚举值,如 options:"male|female"

完整示例:

api
type (
    UpdateUserRequest {
        Name  string `json:"name,optional"`
        Age   int    `json:"age,optional,range=[1,150]"`
        Email string `json:"email,optional"`
    }
)

5. service 块与路由定义

service 块定义服务名、路由和 handler。一个 .api 文件可以包含多个 @server + service 组合,用于组织不同分组的路由。

api
// 公开接口(无需鉴权)
@server (
    group: public
    prefix: /api/v1/public
)
service user-api {
    @handler PublicPingHandler
    get /ping returns (PingResponse)
}

// 鉴权接口(需要 JWT)
@server (
    group: user
    prefix: /api/v1
    jwt: Auth
)
service user-api {
    @handler GetUserHandler
    get /user/:id (GetUserRequest) returns (GetUserResponse)

    @handler ListUserHandler
    get /user (ListUserRequest) returns (ListUserResponse)

    @handler CreateUserHandler
    post /user (CreateUserRequest) returns (CreateUserResponse)

    @handler UpdateUserHandler
    put /user/:id (UpdateUserRequest) returns (BaseResponse)

    @handler DeleteUserHandler
    delete /user/:id (DeleteUserRequest) returns (BaseResponse)
}

支持的 HTTP 方法:getpostputdeletepatchheadoptions

路由参数:

  • :id:路径参数,会被解析到请求结构体的 Id 字段
  • *:通配符

6. @server 块配置项

@server 块支持以下配置:

配置项含义示例
grouphandler/logic 分组目录group: user
prefix路由前缀prefix: /api/v1
jwt启用 JWT 鉴权,值为配置字段名jwt: Auth
middleware应用中间件,多个用逗号分隔middleware: LogMiddleware,RateLimit
timeout请求超时(毫秒)timeout: 5s
maxBytes请求体最大字节数maxBytes: 1048576
signature签名鉴权配置signature: Signature

7. JWT 中间件声明

@server 中声明 jwt: Auth 后,goctl 会在配置结构体中生成 Auth 字段,并在路由组上自动应用 JWT 中间件。配置结构如下:

go
type Config struct {
    rest.RestConf
    Auth struct {
        AccessSecret string
        AccessExpire int64
    }
}

后续生成 token 时使用 AccessSecret 签名,框架在请求进入时自动校验。

二、goctl api 命令详解

1. goctl api go

最常用的子命令,从 .api 文件生成 Go HTTP 服务代码。

bash
goctl api go -api user.api -dir . --style=goZero

常用参数:

参数说明默认值
-apiapi 文件路径必填
-dir输出目录当前目录
--style命名风格goZero
--home自定义模板目录内置模板
--import额外 import 包

2. goctl api validate

校验 .api 文件语法:

bash
goctl api validate -api user.api

在 CI 流程中可以先校验再生成,避免语法错误导致构建失败。

3. goctl api format

格式化 .api 文件,统一缩进和风格:

bash
goctl api format -dir ./apis

4. goctl api ts

.api 文件生成 TypeScript 客户端代码,方便前端对接:

bash
goctl api ts -api user.api -dir ./ts-client --unwrap --tsOnly

参数:

  • --unwrap:解包响应(去掉外层 wrapper)
  • --tsOnly:只生成 .ts 文件,不生成请求工具
  • --caller:自定义调用方名称
  • --webPath:web 客户端路径

生成的代码示例(片段):

typescript
// user.ts
export interface CreateUserRequest {
  name: string;
  email: string;
  age?: number;
  password: string;
}

export async function createUser(req: CreateUserRequest) {
  return fetch("/api/v1/user", {
    method: "POST",
    body: JSON.stringify(req),
  }).then((r) => r.json());
}

5. goctl api doc

根据 .api 文件生成 Markdown 接口文档:

bash
goctl api doc -api user.api -dir ./docs

生成的文档包含每个接口的方法、路径、请求参数、响应结构,适合给前端或测试同学参考。

6. goctl api new

快速创建一个 API 项目骨架(不写 .api 文件,直接生成默认 demo):

bash
goctl api new demo

7. goctl template

自定义模板管理,让团队代码风格统一:

bash
# 初始化本地模板
goctl template init

# 查看/编辑模板(位于 ~/.goctl 模板目录)
# 使用自定义模板生成代码
goctl api go -api user.api -dir . --home /path/to/templates

三、业务逻辑编写

go-zero 生成的代码遵循清晰的分层结构,开发者主要在 Logic 层编写业务。下面分别介绍各层职责和写法。

1. Logic 层:业务核心

Logic 层是业务逻辑的唯一入口。每个路由对应一个 Logic 结构和方法。

go
// internal/logic/user/getuserlogic.go
package user

import (
    "context"
    "strconv"

    "user-api/internal/svc"
    "user-api/internal/types"

    "github.com/zeromicro/go-zero/core/logx"
)

type GetUserLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewGetUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserLogic {
    return &GetUserLogic{
        Logger: logx.WithContext(ctx),
        ctx:    ctx,
        svcCtx: svcCtx,
    }
}

func (l *GetUserLogic) GetUser(req *types.GetUserRequest) (resp *types.GetUserResponse, err error) {
    // 1. 参数校验(已在 handler 层做了 struct tag 校验,这里做业务校验)
    id, err := strconv.ParseInt(req.Id, 10, 64)
    if err != nil {
        return nil, fmt.Errorf("invalid user id: %s", req.Id)
    }

    // 2. 调用依赖(DB / Redis / RPC Client)
    user, err := l.svcCtx.UserModel.FindOne(l.ctx, id)
    if err != nil {
        l.Errorf("query user failed, id=%d, err=%v", id, err)
        return nil, err
    }
    if user == nil {
        return nil, fmt.Errorf("user not found")
    }

    // 3. 组装响应
    return &types.GetUserResponse{
        User: types.UserVO{
            Id:        user.Id,
            Name:      user.Name,
            Email:     user.Email,
            Age:       user.Age,
            CreatedAt: user.CreateTime.Format("2006-01-02 15:04:05"),
        },
    }, nil
}

Logic 编写要点:

  • 通过 l.svcCtx 访问所有依赖(DB、Redis、RPC Client)
  • 通过 l.Logger(即 logx.WithContext(ctx))打印日志,会自动关联 TraceID
  • 返回错误时使用 error,handler 会自动转成 HTTP 错误响应
  • 不要在 Logic 里直接操作 http.Request/ResponseWriter,保持框架无关

2. Handler 层:HTTP 处理

Handler 层由 goctl 生成,一般不需要手写。它负责:

  • 解析路径参数、查询参数、请求体
  • 调用 struct tag 校验
  • 调用 Logic 方法
  • 序列化响应
go
// internal/handler/user/getuserhandler.go(goctl 生成,勿手改)
package user

import (
    "net/http"

    "user-api/internal/logic/user"
    "user-api/internal/svc"
    "user-api/internal/types"

    "github.com/zeromicro/go-zero/rest/httpx"
)

func GetUserHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var req types.GetUserRequest
        if err := httpx.Parse(r, &req); err != nil {
            httpx.ErrorCtx(r.Context(), w, err)
            return
        }

        l := user.NewGetUserLogic(r.Context(), svcCtx)
        resp, err := l.GetUser(&req)
        if err != nil {
            httpx.ErrorCtx(r.Context(), w, err)
        } else {
            httpx.OkJsonCtx(r.Context(), w, resp)
        }
    }
}

如果需要自定义错误响应格式(统一返回 {code, msg, data}),可以在 main 中注册错误处理器:

go
func main() {
    // ... 创建 server 后
    httpx.SetErrorHandlerCtx(func(ctx context.Context, err error) (int, any) {
        switch e := err.(type) {
        case *biz.BizError:
            return http.StatusOK, &types.BaseResponse{
                Code: e.Code,
                Msg:  e.Msg,
            }
        default:
            return http.StatusInternalServerError, &types.BaseResponse{
                Code: 500,
                Msg:  err.Error(),
            }
        }
    })
    httpx.SetOkHandler(func(ctx context.Context, v any) any {
        return &types.BaseResponse{Code: 0, Msg: "ok", Data: v}
    })
    // ...
}

3. Types 层:数据类型

Types 层定义所有请求/响应结构体,由 goctl 从 .api 文件生成,不要手改(再次生成会覆盖)。

go
// internal/types/types.go(goctl 生成)
package types

type GetUserRequest struct {
    Id string `path:"id"`
}

type GetUserResponse struct {
    User UserVO `json:"user"`
}

type UserVO struct {
    Id        int64  `json:"id"`
    Name      string `json:"name"`
    Email     string `json:"email"`
    Age       int    `json:"age"`
    CreatedAt string `json:"createdAt"`
}

struct tag 说明:

  • path:"id":路径参数
  • form:"name":查询参数(GET)
  • json:"name":JSON 请求体(POST/PUT)
  • header:"X-Token":请求头参数

4. SVC 层:服务上下文

ServiceContext 是依赖注入的容器,集中管理所有外部依赖:

go
// internal/svc/servicecontext.go
package svc

import (
    "github.com/zeromicro/go-zero/core/stores/sqlx"
    "github.com/zeromicro/go-zero/zrpc"

    "user-api/internal/config"
    "user-api/internal/middleware"
    "user-rpc/userclient"
)

type ServiceContext struct {
    Config       config.Config
    UserModel    model.UserModel
    UserRpc      userclient.UserClient
    LogMiddleware rest.Middleware
}

func NewServiceContext(c config.Config) *ServiceContext {
    // 初始化 MySQL 连接
    conn := sqlx.NewMysql(c.MySQL.DataSource)

    // 初始化 RPC Client(调用 user-rpc)
    rpcClient := zrpc.MustNewClient(c.UserRpc)
    userRpc := userclient.NewUserClient(rpcClient.Conn())

    return &ServiceContext{
        Config:        c,
        UserModel:     model.NewUserModel(conn, c.CacheRedis),
        UserRpc:       userRpc,
        LogMiddleware: middleware.NewLogMiddleware().Handle,
    }
}

ServiceContext 设计要点:

  • 所有外部依赖(DB、Redis、RPC Client、第三方 SDK)都在这里初始化一次
  • Logic 通过 svcCtx.XXX 访问,避免在 Logic 里 new 实例
  • 便于测试:可以传入 mock 的依赖
  • 配置项来自 config.Config,由 main 注入

四、完整示例:用户管理 API(CRUD)

下面把前面所有知识点整合,实现一个完整的用户管理 API。

1. 项目初始化

bash
mkdir user-api && cd user-api
go mod init user-api

2. 编写 user.api

api
syntax = "v1"

info (
    title:   "用户管理 API"
    desc:    "提供用户 CRUD 能力"
    author:  "gozero-tutorial"
    version: "v1"
)

type (
    BaseResponse {
        Code int    `json:"code"`
        Msg  string `json:"msg"`
    }

    UserVO {
        Id        int64  `json:"id"`
        Name      string `json:"name"`
        Email     string `json:"email"`
        Age       int    `json:"age"`
        CreatedAt string `json:"createdAt"`
    }

    // GET /user/:id
    GetUserRequest {
        Id string `path:"id"`
    }
    GetUserResponse {
        User UserVO `json:"user"`
    }

    // GET /user?keyword=xxx&page=1&pageSize=10
    ListUserRequest {
        Keyword  string `form:"keyword,optional"`
        Page     int    `form:"page,default=1"`
        PageSize int    `form:"pageSize,default=10"`
    }
    ListUserResponse {
        Total int      `json:"total"`
        List  []UserVO `json:"list"`
    }

    // POST /user
    CreateUserRequest {
        Name     string `json:"name"`
        Email    string `json:"email"`
        Age      int    `json:"age,range=[1,150]"`
        Password string `json:"password"`
    }
    CreateUserResponse {
        Id int64 `json:"id"`
    }

    // PUT /user/:id
    UpdateUserRequest {
        Id    string `path:"id"`
        Name  string `json:"name,optional"`
        Age   int    `json:"age,optional,range=[1,150]"`
        Email string `json:"email,optional"`
    }

    // DELETE /user/:id
    DeleteUserRequest {
        Id string `path:"id"`
    }
)

@server (
    group: user
    prefix: /api/v1
    jwt: Auth
)
service user-api {
    @handler GetUserHandler
    get /user/:id (GetUserRequest) returns (GetUserResponse)

    @handler ListUserHandler
    get /user (ListUserRequest) returns (ListUserResponse)

    @handler CreateUserHandler
    post /user (CreateUserRequest) returns (CreateUserResponse)

    @handler UpdateUserHandler
    put /user/:id (UpdateUserRequest) returns (BaseResponse)

    @handler DeleteUserHandler
    delete /user/:id (DeleteUserRequest) returns (BaseResponse)
}

3. 生成代码

bash
goctl api go -api user.api -dir . --style=goZero
go mod tidy

4. 数据访问层(model)

这里我们简化使用内存 map 演示(实际项目用 sqlx + goctl model 生成)。创建 internal/model/usermodel.go

go
package model

import (
    "context"
    "fmt"
    "sync"
    "time"
)

type User struct {
    Id         int64
    Name       string
    Email      string
    Age        int
    Password   string
    CreateTime time.Time
    UpdateTime time.Time
}

type UserModel interface {
    Insert(ctx context.Context, data *User) (int64, error)
    FindOne(ctx context.Context, id int64) (*User, error)
    FindList(ctx context.Context, keyword string, page, pageSize int) ([]*User, int64, error)
    Update(ctx context.Context, data *User) error
    Delete(ctx context.Context, id int64) error
}

type inMemoryUserModel struct {
    mu    sync.RWMutex
    idSeq int64
    data  map[int64]*User
}

func NewUserModel() UserModel {
    return &inMemoryUserModel{data: make(map[int64]*User)}
}

func (m *inMemoryUserModel) Insert(ctx context.Context, data *User) (int64, error) {
    m.mu.Lock()
    defer m.mu.Unlock()
    m.idSeq++
    data.Id = m.idSeq
    data.CreateTime = time.Now()
    data.UpdateTime = time.CreateTime
    m.data[data.Id] = data
    return data.Id, nil
}

func (m *inMemoryUserModel) FindOne(ctx context.Context, id int64) (*User, error) {
    m.mu.RLock()
    defer m.mu.RUnlock()
    u, ok := m.data[id]
    if !ok {
        return nil, nil
    }
    cp := *u
    return &cp, nil
}

func (m *inMemoryUserModel) FindList(ctx context.Context, keyword string, page, pageSize int) ([]*User, int64, error) {
    m.mu.RLock()
    defer m.mu.RUnlock()
    var all []*User
    for _, u := range m.data {
        if keyword == "" || contains(u.Name, keyword) || contains(u.Email, keyword) {
            cp := *u
            all = append(all, &cp)
        }
    }
    total := int64(len(all))
    // 简单分页
    start := (page - 1) * pageSize
    if start > len(all) {
        return nil, total, nil
    }
    end := start + pageSize
    if end > len(all) {
        end = len(all)
    }
    return all[start:end], total, nil
}

func (m *inMemoryUserModel) Update(ctx context.Context, data *User) error {
    m.mu.Lock()
    defer m.mu.Unlock()
    u, ok := m.data[data.Id]
    if !ok {
        return fmt.Errorf("user not found")
    }
    if data.Name != "" {
        u.Name = data.Name
    }
    if data.Age != 0 {
        u.Age = data.Age
    }
    if data.Email != "" {
        u.Email = data.Email
    }
    u.UpdateTime = time.Now()
    return nil
}

func (m *inMemoryUserModel) Delete(ctx context.Context, id int64) error {
    m.mu.Lock()
    defer m.mu.Unlock()
    delete(m.data, id)
    return nil
}

func contains(s, sub string) bool {
    return len(s) >= len(sub) && (s == sub || (len(sub) > 0 && strings.Contains(s, sub)))
}

注意:上面省略了 strings 的 import,实际使用时需要补齐。生产环境请使用 goctl model mysql 生成的 sqlx 实现。

5. ServiceContext 注入 model

go
package svc

import (
    "user-api/internal/config"
    "user-api/internal/model"
)

type ServiceContext struct {
    Config    config.Config
    UserModel model.UserModel
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config:    c,
        UserModel: model.NewUserModel(),
    }
}

6. Logic 层实现

go
// internal/logic/user/createuserlogic.go
package user

import (
    "context"
    "time"

    "user-api/internal/model"
    "user-api/internal/svc"
    "user-api/internal/types"

    "github.com/zeromicro/go-zero/core/logx"
)

type CreateUserLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewCreateUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateUserLogic {
    return &CreateUserLogic{
        Logger: logx.WithContext(ctx),
        ctx:    ctx,
        svcCtx: svcCtx,
    }
}

func (l *CreateUserLogic) CreateUser(req *types.CreateUserRequest) (resp *types.CreateUserResponse, err error) {
    // 业务校验:邮箱不能重复(简化示例,实际查 DB)
    id, err := l.svcCtx.UserModel.Insert(l.ctx, &model.User{
        Name:     req.Name,
        Email:    req.Email,
        Age:      req.Age,
        Password: req.Password, // 生产环境应做哈希
        CreateTime: time.Now(),
    })
    if err != nil {
        l.Errorf("create user failed: %v", err)
        return nil, err
    }
    return &types.CreateUserResponse{Id: id}, nil
}
go
// internal/logic/user/getuserlogic.go
package user

import (
    "context"
    "strconv"

    "user-api/internal/svc"
    "user-api/internal/types"

    "github.com/zeromicro/go-zero/core/logx"
)

type GetUserLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewGetUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserLogic {
    return &GetUserLogic{
        Logger: logx.WithContext(ctx),
        ctx:    ctx,
        svcCtx: svcCtx,
    }
}

func (l *GetUserLogic) GetUser(req *types.GetUserRequest) (resp *types.GetUserResponse, err error) {
    id, err := strconv.ParseInt(req.Id, 10, 64)
    if err != nil {
        return nil, fmt.Errorf("invalid user id: %s", req.Id)
    }
    u, err := l.svcCtx.UserModel.FindOne(l.ctx, id)
    if err != nil {
        return nil, err
    }
    if u == nil {
        return nil, fmt.Errorf("user not found")
    }
    return &types.GetUserResponse{
        User: types.UserVO{
            Id:        u.Id,
            Name:      u.Name,
            Email:     u.Email,
            Age:       u.Age,
            CreatedAt: u.CreateTime.Format("2006-01-02 15:04:05"),
        },
    }, nil
}
go
// internal/logic/user/listuserlogic.go
package user

import (
    "context"

    "user-api/internal/svc"
    "user-api/internal/types"

    "github.com/zeromicro/go-zero/core/logx"
)

type ListUserLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewListUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListUserLogic {
    return &ListUserLogic{
        Logger: logx.WithContext(ctx),
        ctx:    ctx,
        svcCtx: svcCtx,
    }
}

func (l *ListUserLogic) ListUser(req *types.ListUserRequest) (resp *types.ListUserResponse, err error) {
    if req.Page <= 0 {
        req.Page = 1
    }
    if req.PageSize <= 0 {
        req.PageSize = 10
    }
    list, total, err := l.svcCtx.UserModel.FindList(l.ctx, req.Keyword, req.Page, req.PageSize)
    if err != nil {
        return nil, err
    }
    var vos []types.UserVO
    for _, u := range list {
        vos = append(vos, types.UserVO{
            Id:        u.Id,
            Name:      u.Name,
            Email:     u.Email,
            Age:       u.Age,
            CreatedAt: u.CreateTime.Format("2006-01-02 15:04:05"),
        })
    }
    return &types.ListUserResponse{Total: total, List: vos}, nil
}

Update 和 Delete 的 Logic 类似,省略。结构如下:

go
// internal/logic/user/updateuserlogic.go
func (l *UpdateUserLogic) UpdateUser(req *types.UpdateUserRequest) (resp *types.BaseResponse, err error) {
    id, err := strconv.ParseInt(req.Id, 10, 64)
    if err != nil {
        return nil, fmt.Errorf("invalid user id")
    }
    err = l.svcCtx.UserModel.Update(l.ctx, &model.User{
        Id:    id,
        Name:  req.Name,
        Age:   req.Age,
        Email: req.Email,
    })
    if err != nil {
        return nil, err
    }
    return &types.BaseResponse{Code: 0, Msg: "ok"}, nil
}
go
// internal/logic/user/deleteuserlogic.go
func (l *DeleteUserLogic) DeleteUser(req *types.DeleteUserRequest) (resp *types.BaseResponse, err error) {
    id, err := strconv.ParseInt(req.Id, 10, 64)
    if err != nil {
        return nil, fmt.Errorf("invalid user id")
    }
    err = l.svcCtx.UserModel.Delete(l.ctx, id)
    if err != nil {
        return nil, err
    }
    return &types.BaseResponse{Code: 0, Msg: "ok"}, nil
}

7. 配置文件

etc/user-api.yaml

yaml
Name: user-api
Host: 0.0.0.0
Port: 8888

# JWT 配置(@server jwt: Auth 时使用)
Auth:
  AccessSecret: "your-secret-key-change-me"
  AccessExpire: 86400

# MySQL 配置(本示例用内存 model,可省略)
# MySQL:
#   DataSource: root:123456@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=true&loc=Local

# Redis 配置(缓存)
# CacheRedis:
#   - Host: 127.0.0.1:6379
#     Type: node

8. 启动并测试

bash
go run user.go -f etc/user-api.yaml

测试创建用户(注意:因为启用了 JWT,需要先获取 token;为简化示例,可暂时去掉 .api 中的 jwt: Auth 行重新生成):

bash
# 创建用户
curl -X POST http://localhost:8888/api/v1/user \
  -H "Content-Type: application/json" \
  -d '{"name":"alice","email":"alice@example.com","age":25,"password":"secret"}'
# 响应:{"id":1}

# 查询用户
curl http://localhost:8888/api/v1/user/1
# 响应:{"user":{"id":1,"name":"alice","email":"alice@example.com","age":25,"createdAt":"..."}}

# 列表查询
curl "http://localhost:8888/api/v1/user?page=1&pageSize=10"

# 更新
curl -X PUT http://localhost:8888/api/v1/user/1 \
  -H "Content-Type: application/json" \
  -d '{"age":26}'

# 删除
curl -X DELETE http://localhost:8888/api/v1/user/1

五、参数校验进阶

go-zero 的参数校验基于 struct tag,由 httpx.Parse 自动执行。常用 tag:

tag用途示例
optional可选字段json:"age,optional"
range数值范围json:"age,range=[1,150]"
default默认值form:"page,default=1"
options枚举json:"gender,options=male|female"
path路径参数path:"id"
form查询参数form:"keyword"
header请求头header:"X-Token"

校验失败时,httpx.Parse 返回 error,handler 自动返回 400。可以在 main 中自定义错误格式:

go
httpx.SetErrorHandlerCtx(func(ctx context.Context, err error) (int, any) {
    switch err.(type) {
    case *validation.Error:
        return http.StatusBadRequest, map[string]any{
            "code": 400,
            "msg":  err.Error(),
        }
    default:
        return http.StatusInternalServerError, map[string]any{
            "code": 500,
            "msg":  err.Error(),
        }
    }
})

六、统一响应格式

实际项目中通常需要统一响应格式 {code, msg, data}。推荐做法:

  1. .api 中定义 BaseResponse
  2. 在 main 中用 httpx.SetOkHandler 包装成功响应
  3. httpx.SetErrorHandlerCtx 处理错误响应
go
// main.go
func main() {
    // ... 加载配置、创建 server ...

    // 统一成功响应
    httpx.SetOkHandler(func(ctx context.Context, v any) any {
        return map[string]any{
            "code": 0,
            "msg":  "ok",
            "data": v,
        }
    })

    // 统一错误响应
    httpx.SetErrorHandlerCtx(func(ctx context.Context, err error) (int, any) {
        return http.StatusOK, map[string]any{
            "code": 500,
            "msg":  err.Error(),
            "data": nil,
        }
    })

    // ...
}

这样 Logic 里 return &types.GetUserResponse{...}, nil 会自动包装成:

json
{
  "code": 0,
  "msg": "ok",
  "data": {
    "user": {...}
  }
}

七、文件上传与下载

文件上传

go-zero 没有内置专门的上传中间件,直接用标准库 r.ParseMultipartForm + r.FormFile

go
func (l *UploadLogic) Upload(req *types.UploadRequest) (resp *types.UploadResponse, err error) {
    // 在 handler 中读取 multipart file 后传给 logic
    // 或者直接在 handler 中处理
    return
}

// handler 中
func UploadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        _ = r.ParseMultipartForm(10 << 20) // 10MB
        file, header, err := r.FormFile("file")
        if err != nil {
            httpx.ErrorCtx(r.Context(), w, err)
            return
        }
        defer file.Close()
        // 保存文件...
        httpx.OkJson(w, map[string]any{"name": header.Filename, "size": header.Size})
    }
}

文件下载

go
func DownloadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/octet-stream")
        w.Header().Set("Content-Disposition", "attachment; filename=export.csv")
        // 写入文件内容
        w.Write([]byte("id,name\n1,alice\n"))
    }
}

八、小结

本篇系统讲解了 go-zero API 服务的开发流程。要点回顾:

  • .api 文件是接口契约,支持 import、info、type、@server、service 等块
  • struct tag 控制参数来源(path/form/json/header)和校验(optional/range/default/options)
  • @server 块配置路由分组、前缀、JWT、中间件
  • goctl api go/ts/doc/new/format/validate 命令覆盖生成、文档、校验、格式化场景
  • 业务分层:Handler(HTTP 处理,goctl 生成)→ Logic(业务核心,手写)→ SVC(依赖注入)→ Types(数据类型,goctl 生成)
  • 通过 httpx.SetOkHandlerSetErrorHandlerCtx 实现统一响应格式
  • 完整 CRUD 示例演示了从 .api 到运行的端到端流程

下一篇我们将进入 RPC 服务开发,讲解 .proto 文件、goctl rpc 生成、服务端/客户端实现,以及 API 调用 RPC 的完整链路。