Appearance
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 文件版本,目前为v1info:元信息,用于生成文档和注释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 方法:get、post、put、delete、patch、head、options。
路由参数:
:id:路径参数,会被解析到请求结构体的Id字段*:通配符
6. @server 块配置项
@server 块支持以下配置:
| 配置项 | 含义 | 示例 |
|---|---|---|
group | handler/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常用参数:
| 参数 | 说明 | 默认值 |
|---|---|---|
-api | api 文件路径 | 必填 |
-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 ./apis4. 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 demo7. 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-api2. 编写 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 tidy4. 数据访问层(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: node8. 启动并测试
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}。推荐做法:
- 在
.api中定义BaseResponse - 在 main 中用
httpx.SetOkHandler包装成功响应 - 用
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.SetOkHandler和SetErrorHandlerCtx实现统一响应格式 - 完整 CRUD 示例演示了从 .api 到运行的端到端流程
下一篇我们将进入 RPC 服务开发,讲解 .proto 文件、goctl rpc 生成、服务端/客户端实现,以及 API 调用 RPC 的完整链路。