go-zero学习 第二章 进阶之API
- 重要提示
- 相关命令
- 2 API 语法
- 3 API服务代码实战
- 3.1 请求参数、文件上传/预览、分组示例
- 3.2 中间件、统一返回信息 代码示例
- 3.3 修改统一返回信息的代码生成模板
重要提示
- 因官网重新改版,本文是基于官网最新版本的文档并整合旧文档重新进行全面总结、归纳。
- 本文主要对官网 指南 进行提炼总结,未涉及部分将在后续章节陆续补充完善。
- API部分主要是对外提供服务,go-zero可以通过编写一个 api 文件,生成一个完整的api服务。
相关命令
-
api
生成api服务
命令
go-zero-api\api> goctl api go -api ./doc/ucenter.api -dir ./code
2 API 语法
参考1:语句
api
文件中包含以下语句,按从上到下的顺序:
-
syntax
语句 -
info
语句 -
import
语句 - 结构体语句(
HTTP
服务的请求/响应体语句) -
@server
语句 -
@doc
语句 -
@handler
语句 -
HTTP
路由语句 -
HTTP
服务声明语句 - 注释语句
完整的 api 语法示例:
syntax = "v1"
info (
title: "api 文件完整示例写法"
desc: "演示如何编写 api 文件"
author: ""
date: "2022 年 12 月 26 日"
version: "v1"
)
type UpdateReq {
Arg1 string `json:"arg1"`
}
type ListItem {
Value1 string `json:"value1"`
}
type LoginReq {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginResp {
Name string `json:"name"`
}
type FormExampleReq {
Name string `form:"name"`
}
type PathExampleReq {
// path 标签修饰的 id 必须与请求路由中的片段对应,如
// id 在 service 语法块的请求路径上一定会有 :id 对应,见下文。
ID string `path:"id"`
}
type PathExampleResp {
Name string `json:"name"`
}
@server (
jwt: Auth // 对当前 Foo 语法块下的所有路由,开启 jwt 认证,不需要则请删除此行
prefix: /v1 // 对当前 Foo 语法块下的所有路由,新增 /v1 路由前缀,不需要则请删除此行
group: g1 // 对当前 Foo 语法块下的所有路由,路由归并到 g1 目录下,不需要则请删除此行
timeout: 3s // 对当前 Foo 语法块下的所有路由进行超时配置,不需要则请删除此行
middleware: AuthInterceptor // 对当前 Foo 语法块下的所有路由添加中间件,不需要则请删除此行
maxBytes: 1024 // 对当前 Foo 语法块下的所有路由添加请求体大小控制,单位为 byte,goctl 版本 >= 1.5.0 才支持
)
service Foo {
// 定义没有请求体和响应体的接口,如 ping
@handler ping
get /ping
// 定义只有请求体的接口,如更新信息
@handler update
post /update (UpdateReq)
// 定义只有响应体的结构,如获取全部信息列表
@handler list
get /list returns ([]ListItem)
// 定义有结构体和响应体的接口,如登录
@handler login
post /login (LoginReq) returns (LoginResp)
// 定义表单请求
@handler formExample
post /form/example (FormExampleReq)
// 定义 path 参数
@handler pathExample
get /path/example/:id (PathExampleReq) returns (PathExampleResp)
}
其他示例:/docs/tasks/dsl/api#快速入门
1 syntax 语句syntax
语句用于标记 api
语言的版本,不同的版本可能语法结构有所不同,随着版本的提升会做不断的优化,当前版本为 v1
。
示例:
syntax = "v1"
2 info 语句info
语句是 api
语言的 meta
信息,其仅用于对当前 api
文件进行描述,暂不参与代码生成,其和注释还是有一些区别,注释一般是依附某个 syntax
语句存在,而 info
语句是用于描述整个 api
信息的,当然,不排除在将来会参与到代码生成里面来。
示例:
info(
title : "XXX系统"
author: "哈哈哈"
email: "123456@"
version: "1.0.0.0"
)
3 import 语句import
语句是在当前 api
中引入其他 api
文件的语法块,其支持相对/绝对路径,不支持 package
的设计,如 。
示例:
// 单行 import
import "foo"
import "/path/to/file"
// import 组
import ()
import (
"bar"
"relative/to/file"
)
4 结构体语句(HTTP 服务的请求/响应体语句)api
中的数据类型基本沿用了 Golang
的数据类型,用于对 rest
服务的请求/响应体结构的描述。
示例1:
// 别名类型 [1]
type Int int
type Integer = int
// 空结构体
type Foo {}
// 单个结构体
type Bar {
Foo int `json:"foo"`
Bar bool `json:"bar"`
Baz []string `json:"baz"`
Qux map[string]string `json:"qux"`
}
type Baz {
Bar `json:"baz"`
// 结构体内嵌 [2]
Qux {
Foo string `json:"foo"`
Bar bool `json:"bar"`
} `json:"baz"`
}
// 空结构体组
type ()
// 结构体组
type (
Int int
Integer = int
Bar {
Foo int `json:"foo"`
Bar bool `json:"bar"`
Baz []string `json:"baz"`
Qux map[string]string `json:"qux"`
}
)
示例2(实际使用):
type (
/* 1 公用类 */
BaseModel {
Id int64 `json:"id"` // id
Name string `json:"name,optional"` // 名称
Data string `json:"data,optional"` // 数据:string
}
/* 2 公共返回 */
BaseModelResp {
Code string `json:"code"` // 状态码
Message string `json:"message"` // 提示信息
Data []*BaseModel `json:"data,optional"`
}
/* 3 分类标签接口的返回数据 */
GetClassLabelResp {
Code string `json:"code"` // 状态码
Message string `json:"message"` // 提示信息
ClassLabelList []*BaseModel `json:"classLabelList"` // 分类标签集合:id-名称
}
)
注意
:
- 虽然语法上支持别名,但是在语义分析时会对别名进行拦截,这或在将来进行放开。
- 虽然语法上支持结构体内嵌,但是在语义分析时会对别名进行拦截,这或在将来进行放开。
除此之外
:
- 目前
api
语法中虽然支持了数组的语法,但是在语义分析时会对数组进行拦截,目前建议用切片替代,这或在将来放开。 - 不支持
package
设计,如。
5 @server 语句@server
语句是对一个服务语句的 meta
信息描述,其对应特性包含但不限于:
-
jwt
开关 - 中间件
- 路由分组
- 路由前缀
示例1:
// 空内容
@server()
// 有内容
@server (
// jwt 声明
// 如果 key 固定为 “jwt:”,则代表开启 jwt 鉴权声明
// value 则为配置文件的结构体名称
jwt: Auth
// 路由前缀
// 如果 key 固定为 “prefix:”
// 则代表路由前缀声明,value 则为具体的路由前缀值,字符串中没让必须以 / 开头
prefix: /v1
// 路由分组
// 如果 key 固定为 “group:”,则代表路由分组声明
// value 则为具体分组名称,在 goctl生成代码后会根据此值进行文件夹分组
group: Foo
// 中间件
// 如果 key 固定为 middleware:”,则代表中间件声明
// value 则为具体中间件函数名称,在 goctl生成代码后会根据此值进生成对应的中间件函数
middleware: AuthInterceptor
// 超时控制
// 如果 key 固定为 timeout:”,则代表超时配置
// value 则为具体中duration,在 goctl生成代码后会根据此值进生成对应的超时配置
timeout: 3s
// 其他 key-value,除上述几个内置 key 外,其他 key-value
// 也可以在作为 annotation 信息传递给 goctl 及其插件,但就
// 目前来看,goctl 并未使用。
foo: bar
)
示例2(实际使用):
@server(
jwt: Auth
middleware: CheckUrl
group: app/appSystem
prefix: /app/appSystem
)
6 @doc 语句@doc
语句是对单个路由的 meta
信息描述,一般为 key-value
值,可以传递给 goctl
及其插件来进行扩展生成。
示例1:
// 单行 @doc
@doc "foo"
// 空 @doc 组
@doc ()
// 有内容的 @doc 组
@doc (
foo: "bar"
bar: "baz"
)
示例2(实际使用):
@doc(
summary: "XXX列表"
)
7 @handler 语句@handler
语句是对单个路由的 handler
信息控制,主要用于生成 golang
的实现转换方法。
示例:
@handler foo
@handler GetClassLabelHandler
注意
:不论定义的handler
是否有handler
后缀,goctl
根据api
实际生成的代码文件均会以xxxhandler
为后缀,所以在api
中定义handler
时加不加handler
后缀没有意义。
8 HTTP 路由语句
路由语句是对此 HTTP
请求的具体描述,包括请求方法,请求路径,请求体,响应体信息。
示例:
// 没有请求体和响应体的写法
get /ping
// 只有请求体的写法
get /foo (foo)
// 只有响应体的写法
post /foo returns (foo)
// 有请求体和响应体的写法
post /foo (foo) returns (bar)
9 HTTP 服务声明语句
示例:
// 带 @server 的写法
@server (
prefix: /v1
group: Login
)
service user {
@doc "登录"
@handler login
post /user/login (LoginReq) returns (LoginResp)
@handler getUserInfo
get /user/info/:id (GetUserInfoReq) returns (GetUserInfoResp)
}
@server (
prefix: /v1
middleware: AuthInterceptor
)
service user {
@doc "登录"
@handler login
post /user/login (LoginReq) returns (LoginResp)
@handler getUserInfo
get /user/info/:id (GetUserInfoReq) returns (GetUserInfoResp)
}
// 不带 @server 的写法
service user {
@doc "登录"
@handler login
post /user/login (LoginReq) returns (LoginResp)
@handler getUserInfo
get /user/info/:id (GetUserInfoReq) returns (GetUserInfoResp)
}
10 注释语句
在 api
领域特性语言中有 2 种格式:
- 单行注释以
//
开始,行尾结束。 - 多行注释(文档注释)以
/*
开始,以第一个*/
结束。
3 API服务代码实战
参考1:请求参数
前面讲解了api
文件的语法,这里在使用时具体讲解一些细节,主要有以下内容:
-
path
参数 -
form
参数 -
json
参数 -
header
参数 - 参数修饰符
- 文件上传/下载
- 请求体大小限制
-
api
分组 - 中间件
- 统一返回信息
- 鉴权【需要结合rpc服务,下一节再讲解】
1、2、3、4、5、6、7都是针对请求参数,1、2、3、4可以划归为一组,在绑定参数时,只能选择1、2、3、4其中一个。
请求参数:
标签类型 | 描述 | 提供方 | 使用范围 | 示例 |
---|---|---|---|---|
path | 路由path,如/foo/:id | go-zero | request | path:"id" |
form | 标志请求体是一个form(POST 方法时)或者一个query(GET 方法时/search?name=keyword) |
go-zero | request | form:"name" |
json | json序列化tag | golang | request、response | json:"fooo" |
header | HTTP header,如 Name: value
|
go-zero | request | header:"name" |
参数修饰符:
标签类型 | 描述 | 提供方 | 使用范围 | 示例 |
---|---|---|---|---|
optional | 定义当前字段为可选参数 | go-zero | request | json:"name,optional" |
options | 定义当前字段的枚举值,多个以,隔开 | go-zero | request | json:"gender,options=male,emale" |
default | 定义当前字段默认值 | go-zero | request | json:"gender,default=male" |
range | 定义当前字段数值 范围 |
go-zero | request | json:"age,range=[0:120]" |
3.1 请求参数、文件上传/预览、分组示例
代码:请求参数、文件上传/预览、分组示例 代码示例
1 api文件:
syntax = "v1"
info(
title : "go-zero-api"
desc: "ucenter"
author: "ximuqi"
email: "xxx"
version: "0.0.1"
)
type (
/* 1 公用类(json) */
BaseModelJson {
Id int64 `json:"id"` // id
Name string `json:"name,optional"` // 名称
Data string `json:"data,optional"` // 数据
}
/* 2 公用类(form) */
BaseModelForm {
Id int64 `form:"id"` // id
Name string `form:"name,optional"` // 名称
Data string `form:"data,optional"` // 数据
}
/* 3 公用类 Path */
PathReq {
Id int64 `path:"id"` // id
Name string `path:"name"` // 名称
}
/* 4 上传文件 */
FileUploadReq {
Id int64 `form:"id"` // 父级-id
Type int64 `form:"type,optional"` // 类型 1:证书文件;2:私钥文件
FileList []*byte `form:"fileList,optional"` // 文件列表
}
/* 5 下载/预览文件 */
FileShowReq {
Id int64 `form:"id"` // 文件-id
FileUrl string `form:"fileUrl,optional"` // 文件地址
}
/* 6 用户账号密码登录 model */
UserModel {
Account string `json:"account"` // 账号
Password string `json:"password"` // 密码
Gender int64 `json:"gender,options=1:2:3"` // 性别 1:未设置;2:男性;3:女性
Current int64 `json:"current,optional,default=1"` // 当前页码
PageSize int64 `json:"pageSize,optional,default=20"` // 每页数量
}
/* 7 用户登录返回 model */
UserLoginResp {
Id int64 `json:"id"` // 用户id
Account string `json:"account"` // 账号
Username string `json:"username"` // 登录账号
Gender int64 `json:"gender"` // 性别 1:未设置;2:男性;3:女性
Avatar string `json:"avatar"` // 头像
AccessToken string `json:"token"` // token
AccessExpire int64 `json:"accessExpire"` // 过期时间戳
RefreshAfter int64 `json:"refreshAfter"` //
}
)
@server(
middleware: Check
group: ucenter
prefix: /ucenter
)
service uCenter {
@doc(
summary: "1 json参数"
)
@handler jsonParam
post /jsonParam (BaseModelJson) returns (BaseModelJson)
@doc(
summary: "2 表单参数(post)"
)
@handler formParamPost
post /formParamPost (BaseModelForm) returns (BaseModelJson)
@doc(
summary: "3 表单参数(Get)"
)
@handler formParamGet
get /formParamGet (BaseModelForm) returns (BaseModelJson)
@doc(
summary: "4 path参数"
)
@handler pathParam
get /pathParam/:id/:name (PathReq) returns (BaseModelJson)
@doc(
summary: "5 文件上传"
)
@handler uploadFile
post /uploadFile (FileUploadReq) returns (BaseModelJson)
@doc(
summary: "6 文件下载"
)
@handler downloadFile
get /downloadFile (FileShowReq)
@doc(
summary: "7 文件预览"
)
@handler previewFile
get /previewFile (FileShowReq)
}
@server(
group: group2
prefix: /group2
)
service uCenter {
@doc(
summary: "8 登录"
)
@handler login
post /login (UserModel) returns (UserLoginResp)
}
2 api
生成api服务
命令
go-zero-api\api> goctl api go -api ./doc/ -dir ./code
3 代码结构
#查看 目录文件结构
tree /f
#查看 目录结构
tree
目录结构:
└─api
├─code
│ ├─etc
│ └─internal
│ ├─config
│ ├─handler
│ │ ├─group2
│ │ └─ucenter
│ ├─logic
│ │ ├─group2
│ │ └─ucenter
│ ├─middleware
│ ├─svc
│ └─types
└─doc
目录结构说明:etc
:静态配置文件目录conf
:静态配置文件对应的结构体声明目录handler
:handler 为固定后缀,其下的两个分组一般不需要修改,只有在文件上传下载时需要特殊处理。logic
:业务目录,所有业务编码文件都存放在这个目录下面,logic 为固定后缀。middleware
:中间件。svc
:依赖注入目录,所有 logic 层需要用到的依赖都要在这里进行显式注入。types
:结构体存放目录。
通过目录结构可以看到,在handler
和logic
目录下都有ucenter
、group2
文件夹,这是分组的结果,我们的逻辑代码主要在logic
下。
4 请求测试
根据.api
文件api模块
代码生成时默认的端口是 8888
,查看及修改是在etc
目录下的yaml
文件,具体修改详见代码。
1 json参数请求测试
在logic/ucenter
目录下编写代码逻辑。
请求地址:http://localhost:8888/ucenter/jsonParam
请求方式:POST
请求格式:JSON
请求数据:{"id":1,"name":"测试"}
返回结果:{"id":1,"name":"测试","data":""}
2 form参数请求测试【POST】
在logic/ucenter
目录下编写代码逻辑。
请求地址:http://localhost:8888/ucenter/formParamPost
请求方式:POST
请求格式:FORM
请求数据:id=2,name="hello"
返回结果:{"id":2,"name":"hello","data":""}
3 form参数请求测试【GET】
在logic/ucenter
目录下编写代码逻辑。
请求地址:http://localhost:8888/ucenter/formParamGet?id=3&name=hello&data=测试
请求方式:GET
请求格式:URL参数
请求数据:id=3&name=hello&data=测试"
返回结果:{"id":3,"name":"hello","data":"测试"}
4 path参数请求测试
在logic/ucenter
目录下编写代码逻辑。
请求地址:http://localhost:8888/ucenter/pathParam/4/hello
请求方式:GET
请求格式:URL参数
请求数据:id=4&name=hello"
返回结果:{"id":4,"name":"hello"}
5 文件上传请求测试
在logic/ucenter
目录下编写代码逻辑。
注意:go-zero
对上传文件有大小限制,默认最大为1MB,可在etc
目录下的yaml
文件中修改大小MaxBytes
,并设置上传文件的存储路径。
Name: uCenter
Host: 0.0.0.0
Port: 8888
#web请求到此api服务的超时时间
Timeout: 10000
# 将请求体最大允许字节数从1MB改为1000MB
MaxBytes: 1048576000
#文件
UploadFile:
MaxFileNum: 1000
MaxFileSize: 1048576000 # 1000MB
SavePath: projects/go-zero-api/uploadFiles/
#日志配置
Log:
Mode: file
Path: log/go-zero-api
Level: error
Compress: true
KeepDays: 180
请求地址:http://localhost:8888/ucenter/downloadFile
请求方式:GET
请求格式:FORM
请求数据:id=1&name=1_aab7803750b6412e98a9c63196efc6e8.png
返回结果:{"id":5,"name":"","data":"操作成功"}
6 文件下载
文件下载是把数据写到响应流中,所以需要同时修改和
,详见代码。
7 文件预览
文件预览也是把数据写到响应流中,所以需要同时修改和
,预览针对的是多媒体文件,如音视频、照片等,比下载少了在响应流中的流配置以及文件名称,详见代码。
3.2 中间件、统一返回信息 代码示例
代码:中间件、统一返回信息 代码示例
全局中间件的讲解
- 中间件
- 统一返回信息
中间件:
在3.1 请求参数、文件上传/预览、分组示例 中的api文件中已经涉及了【局部】中间件,但未深入讲解。实际上api服务
的中间件分为局部中间件
和全局中间件
,在中间件中可以拦截请求/响应并作处理。
全局中间件是加在启动类上的,全局中间件、局部中间件都是一样的,只是位置不一样罢了:
// 全局中间件:用法一
(func(next ) {
return func(w , r *) {
("全局中间件")
next(w, r)
}
})
// 全局中间件:用法二
(().Handle)
在中间件中处理数据时,不论是操作Redis、数据库,还是其他RPC,需要什么,在创建中间件时就New对应的环境即可。
全局中间件、局部中间件的使用顺序:
全局中间件→局部中间件1→局部中间件2→全局中间件
统一返回信息:
参考:HTTP 扩展
- 响应信息的统一封装处理涉及两部分,分别是错误信息的封装、响应信息的封装。
├─errorx
│
│
│
├─response
- 使用示例
改造逻辑的返回结果改为使用封装的错误信息,
使用统一封装的响应。
3.3 修改统一返回信息的代码生成模板
参考:模板定制化
- 下载生成代码的模板
查看参考文档 - 修改
/.goctl/${goctl版本号}/api/
文件
原始模板:
package {{.PkgName}}
import (
"net/http"
"/zeromicro/go-zero/rest/httpx"
{{.ImportPackages}}
)
func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
{{if .HasRequest}}var req types.{{.RequestType}}
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
{{end}}l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx)
{{if .HasResp}}resp, {{end}}err := l.{{.Call}}({{if .HasRequest}}&req{{end}})
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
{{if .HasResp}}httpx.OkJsonCtx(r.Context(), w, resp){{else}}httpx.Ok(w){{end}}
}
}
}
修改后的模板:
package {{.PkgName}}
import (
"net/http"
"/zeromicro/go-zero/rest/httpx"
{{.ImportPackages}}
)
func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
{{if .HasRequest}}var req types.{{.RequestType}}
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
{{end}}l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx)
{{if .HasResp}}resp, {{end}}err := l.{{.Call}}({{if .HasRequest}}&req{{end}})
/*
if err != nil {
((), w, err)
} else {
{{if .HasResp}}((), w, resp){{else}}(w){{end}}
}
*/
response.Response(r.Context(), w, resp, err)
}
}
修改后生成的代码:
package ucenter
import (
"go-zero-api/common/response"
"net/http"
"/zeromicro/go-zero/rest/httpx"
"go-zero-api/api/code/internal/logic/ucenter"
"go-zero-api/api/code/internal/svc"
"go-zero-api/api/code/internal/types"
)
func FormParamGetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.BaseModelForm
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := ucenter.NewFormParamGetLogic(r.Context(), svcCtx)
resp, err := l.FormParamGet(&req)
/*
if err != nil {
((), w, err)
} else {
((), w, resp)
}
*/
response.Response(r.Context(), w, resp, err)
}
}