红魔咖啡馆

头发越掉越多,头发越掉越少

0%

【go-zero】API DSL

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-datax-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认证和中间件良好配合使用