API DSL
goctl API
DSL是一种描述HTTP服务的简洁语言,来源于.api文件
文件结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
syntax = "v1" // 必须的版本声明
info ( // 可选元数据块
title: "用户 API"
version: "1.0"
)
import "shared.api" // 导入其他 .api 文件
type (...) // 类型定义
service name-api { // 服务块
@server (...)
@handler HandlerName
method /path (RequestType) returns (ResponseType)
}服务块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
service user-api {
@server (
jwt: Auth // 启用 JWT 中间件
middleware: AccessLog,Cors // 应用命名中间件
prefix: /v1 // URL 前缀
timeout: 3s // 超时时间
)
@handler Login
post /user/login (LoginReq) returns (LoginResp)
@handler GetUser
get /user/:id (UserReq) returns (UserResp)
}API 类型
类型声明要满足如下规则:
- 以
type开头 - 不需要声明
struct关键字 - 不支持泛型和弱类型
参数
参数接收规则
可以在类型中用tag声明参数接收规则
| 接收规则 | 说明 | 生效范围 | 接收tag示例 | 请求示例 |
|---|---|---|---|---|
| json | json 序列化 | 请求体&响应体 | json:“foo” | {“key”:“value”} |
| path | 路由参数 | 请求体 | path:“id” | /foo/:id |
| form | post 请求的表单参数请求接收标识,get 请求的 query 参数接收标识 | 请求体 | form:“name” | GET /search?key=value |
| header | http 请求体接收标识 | 请求体 | header:“Content-Length” | origin: https://go-zero.dev |
其中POST支持 content-type 为 form-data 和
x-www-form-urlencoded
注意:gozero中不支持多tag接收参数,即一个字段只能有一个tag
参数校验规则
还可以在tag中添加参数校验,只对请求体有效,支持的规则如下:
| 接收规则 | 说明 | 示例 |
|---|---|---|
| optional | 当前字段是可选参数,允许为零值(zero value) | json:“foo,optional” |
| options | 当前参数仅可接收的枚举值 | json:“gender,options=foo|bar” |
| default | 当前参数默认值 | json:“gender,default=male” |
| range | 当前参数数值有效范围,仅对数值有效 | json:“age,range=[0:120]” |
注意,range表达式用小括号表示开区间,中括号表示闭区间,当min值缺省时,min代表0,max值缺省时,mnax代表无穷,两者不能同时缺省
路由规则
API描述语言中,路由需要满足如下规则:
- 以
/开头 - 路由节点以
/分隔 - 路由节点中可以包含
:,但:必须是路由节点的第一个字符,它后面的节点值必须在请求体中有pathtag声明,用于接收路由参数 - 路由节点可以包含数字、字母、下划线和中划线
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
service Demo {
// 示例路由 /foo
@handler demoPath1
get /foo (DemoReq) returns (DemoResp)
// 示例路由 /foo/bar
@handler demoPath2
get /foo/bar (DemoReq) returns (DemoResp)
// 示例路由 /foo/bar/:id,其中 id 为请求体中的字段
@handler demoPath3
get /foo/bar/:id (DemoPath3Req) returns (DemoResp)
// 示例路由 /foo/bar/:id/:name,其中 id,name 为请求体中的字段
@handler demoPath4
get /foo/bar/:id/:name (DemoPath4Req) returns (DemoResp)
// 示例路由 /foo/bar/baz-qux
@handler demoPath6
get /foo/bar/baz-qux (DemoReq) returns (DemoResp)
// 示例路由 /foo/bar_baz/123(goctl 1.5.1 支持)
@handler demoPath7
get /foo/bar_baz/123 (DemoReq) returns (DemoResp)
}路由分组
随着业务扩展,服务接口也会变多,生成代码文件也会变多,我们需要将生成的代码文件按照维度聚合,便于开发和维护
没有分组的情况,生成的代码handler和logic下的目录都是混在一起的,不方便管理和阅读
API语言中,我们可以通过在@server语句块中使用group关键字分组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@server (
prefix: /v1
group: user
)
service user-api {
//...
}
@server (
prefix: /v1
group: role
)
service user-api {
//...
}
@server (
prefix: /v1
group: class
)
service user-api {
//...
}这样可以将不同的业务逻辑分到不同的目录下
路由前缀
假设我们有一个用户服务,需要通过路由来区分不同版本,可以通过api语言声明路由前缀
1
2
https://example.com/v1/users
https://example.com/v2/users可以通过在@server语句块中使用prefix关键字声明路由前缀:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@server (
prefix: /v1
)
service user-api {
@handler usersv1
get /users returns ([]UserV1)
}
@server (
prefix: /v2
)
service user-api {
@handler usersv2
get /users returns ([]UserV2)
}生成的代码会通过rest.WithPrfix来声明路由前缀
中间件声明
API语言中,在@server语句块中使用middleware关键字声明中间件的,多个中间件以英文逗号分割,这样生成的代码会单独开一个middleware文件夹
1
2
3
4
@server(
// 通过 middileware 关键字声明中间件,多个中间件以英文逗号分割,如 UserAgentMiddleware,LogMiddleware
middleware: UserAgentMiddleware
)中间件代码由goctl自动生成,是一个结构体,存在一个Handle方法,接收一个http.HandlerFunc参数并返回一个http.HandlerFunc参数,用于对请求处理并传递给下一个中间件或handler
例:我们在中间件中将header的User-Agent信息存到context中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type UserAgentMiddleware struct {
}
func NewUserAgentMiddleware() *UserAgentMiddleware {
return &UserAgentMiddleware{}
}
func (m *UserAgentMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
val := r.Header.Get("User-Agent")
reqCtx := r.Context()
ctx := context.WithValue(reqCtx, "User-Agent", val)
newReq := r.WithContext(ctx)
next(w, newReq)
}
}context的传递:
- 在一个新中间件中间件中,使用
r.Context()获取传入请求的context - 使用
context.WithValue()新建带值的ctx - 使用
r.WithContext()得到新的request,传到下游 - 下游代码可以通过
r.Context()获取值
JWT鉴权
JWT是一种开放标准,用于在网络应用间传递声明式信息,是一种基于JSON的轻量级身份验证和授权机制,用于在客户端和服务端之间安全传输信息
在API文件中,在@server语句块中使用jwt:Auth开启jwt认证,它仅对下面对应的路由有效,使用Auth作为jwt的值,经过goctl代码生成后会转成对应的jwt配置:
1
2
3
4
5
6
7
8
9
10
11
12
package config
import "github.com/zeromicro/go-zero/rest"
type Config struct {
rest.RestConf
// Auth 配置 JWT 认证需要的密钥和过期时间。
Auth struct {
AccessSecret string
AccessExpire int64
}
}route.go中生成了通过rest.WithJwt来声明jwt认证
注意:对于jwt token的生成和refresh token仍需开发者自行实现
jwt通常也可以携带一些自定义信息,比如server端生成jwt
key时添加了custom-key值,go-zero解析后会将所有载体放到context中,获取载体信息示例如下:
1
2
3
4
5
func (l *UserInfoLogic) UserInfo(req *types.UserInfoReq) (resp *types.UserInfoResp, err error) {
// 获取 jwt 载体信息
value := l.ctx.Value("custom-key")
return
}请求签名
gozero中内置了签名功能,可以通过api语言开启,并生成签名代码
在@server代码块中使用signature关键字开启签名功能
1
2
3
4
5
6
7
@server (
signature: true // 通过 signature 关键字开启签名功能
)
service sign-api {
@handler SignDemo
post /sign/demo (SignDemoReq) returns (SignDemoResp)
}生成的路由代码会自动携带rest.WithSignature
API导入
随着业务量增大,API文件也会越来越大,写在同一个api文件中api文件也会变得巨大,不易阅读和维护,所以可以通过api import引入其他api文件
假设HTTP服务响应统一为如下json格式:
1
2
3
4
5
{
"code": 0,
"msg": "success",
"data": {}
}前两个是固定的,data可变,所以我们可以将前两个字段抽象出来,定义一个公共结构体,在其他api文件中引入该结构体,例如抽象在bash.api中:
1
2
3
4
5
6
syntax = "v1"
type Base {
Code int `json:"code"`
Msg string `json:"msg"`
}这样其他api文件可以通过import "base.api"引入
由于api描述语言中没有package的概念,所以引入时需要用相对路径
SSE路由
SSE是服务器推送事件,一种服务器向浏览器单向推送实时数据的轻量协议
工作原理
- 客户端发起一个普通HTTP请求,且header中告诉服务器自己支持SSE
- 服务器保持该连接不关闭,并持续推送文本消息(以
text/event-stream格式) - 每条消息都是一个事件,浏览器通过js的
EventSource对象自动接收,不用反复轮询
SSE支持
gozero中可以通过在@server语句块中开启sse:true注解来支持SSE
指定开启后,goctl会带有rest.WithSSE()选项的路由,该选项会自动:
- 设置适当的SSE头部
- 移除写入超时以允许长时间运行的连接
- 启用连接保持活动
注意:
- SSE路由通常应该使用GET方法
- 连接将保持打开,知道客户端断开连接或处理程序完成
- SSE可与JWT认证和中间件良好配合使用