go-zero api语法

标签:微服务首次发布:2023-12-15最近修改:2023-12-15

基本语法

  1. syntax:用于标记 api 语言的版本,不同的版本可能语法结构有所不同。示例:

    text
    syntax = "v1"
  2. info:是 api 语言的meta信息,其仅对当前 api 文件进行描述,暂不参与代码生成。示例:

    text
    info(   title : "图书管理系统"   desc: "api语法学习"   author: "哈哈哈"   email: "12345@qq.com"   date: "2023年12月10日"   version: "1.0")
  3. import:在当前 api 文件中引入其他 api 文件的语法块,其支持相对/绝对路径。但是被引入的 api 文件中不允许出现 service 语法块,否则会报错。示例(demo.api):

    text
    // foo.api文件和bar.api文件中不能出现service语法块import (  "foo.api"  "../relative/bar.api")type UserInfoReq {  Id int64 `path:"id"`}type UserInfoResp {  Base 	 // Base 为foo.api中的公共结构体  UserInfo // UserInfo 为bar.api中的公共结构体}service user {  @handler userInfo  get /user/info/:id (UserInfoReq) returns (UserInfoResp)}
  4. type:由 golang 的 type 演变而来,当然也保留着一些 golang type 的特性,并且保留了 golang 内置数据类型:boolintint8int16int32int64uintuint8uint16uint32uint64uintptrfloat32float64complex64complex128stringbyterune。除此之外,还支持枚举类型自定义类型类型别名数组(切片)哈希类型嵌套等。但是不支持 package 设计,如 time.Time。示例:

    api
    // 别名类型 [1]type Int inttype 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"`  })
  5. @server:对一个服务语句的 meta 信息进行描述,其对应特性包含但不限于:jwt 开关、中间件、路由分组、路由前缀、超时控制。

  6. @doc:对单个路由的 meta 信息描述,一般为 key-value 值,可以传递给 goctl 及其插件来进行扩展生成。

  7. @handler:对单个路由的 handler 信息控制,主要用于生成 golang http.HandleFunc 的实现转换方法。

  8. 路由语句:路由语句是对单此 HTTP 请求的具体描述,包括请求方法,请求路径,请求体,响应体信息。

    text
    // 没有请求体和响应体的写法get /ping// 只有请求体的写法get /foo (foo)// 只有响应体的写法post /foo returns (foo)// 有请求体和响应体的写法post /foo (foo) returns (bar)

统一 api 前缀

在 HTTP 服务开发中,路由前缀需求是非常常见的,比如通过路由来区分版本,或者通过路由来区分不同的服务,这些都是非常常见的需求。以下是路由前缀的典型场景:

  1. 版本控制: 如果 API 有多个版本,可以使用路由前缀来区分它们,例如 /v1/v2。这样,就可以在不同的路由处理器中实现不同版本的 API 逻辑,而不会造成混淆。

    text
    @server(	prefix: /v1)service user-api {   @handler Login   post /user/login (LoginRequest) returns (LoginResponse)}@server(   prefix: /v2)service user-api {   @handler Login   post /user/login (LoginRequest) returns (LoginResponse)}
  2. 服务分割: 当有不同的服务或功能模块时,路由前缀可以按模块组织路由,例如 /auth 用于认证相关的路由,/orders 用于订单处理路由。

    text
    @server(   prefix: /auth)service user-api {   @handler Register   post /register (RegisterRequest) returns (RegisterResponse)    @handler Login   post /login (LoginRequest) returns (LoginResponse)}@server(   prefix: /orders)service user-api {   @handler CreateOrders   post /create (CreateOrdersRequest) returns (CreateOrdersResponse)    @handler UpdateOrders   post /update (UpdateOrdersRequest) returns (UpdateOrdersResponse)}

使用路由前缀后,生成的路由代码(handler/routes.go)如下:

go
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {    server.AddRoutes(      []rest.Route{        {          Method:  http.MethodPost,          Path:    "/register",          Handler: RegisterHandler(serverCtx),        },        {          Method:  http.MethodPost,          Path:    "/login",          Handler: LoginHandler(serverCtx),        },      },      rest.WithPrefix("/auth"),    )        server.AddRoutes(      []rest.Route{        {          Method:  http.MethodPost,          Path:    "/create",          Handler: CreateOrdersHandler(serverCtx),        },        {          Method:  http.MethodPost,          Path:    "/update",          Handler: UpdateOrdersHandler(serverCtx),        },      },      rest.WithPrefix("/orders"),    )}

可以看到,其实声明的 prefix 其实在生成代码后通过 rest.WithPrefix 来声明了路由前缀,这样就可以通过路由前缀来区分不同的服务了。

路由分组

路由分组是通过在 api 文件中使用 @server 注解下的group来实现的。每个 @server 可以定义一个路由前缀,然后在该前缀下可以定义一个路由分组。这样,所有相关的路由都会被组织在一起,形成一个路由分组。并且每一个分组都有自己的文件夹。

text
@server (  prefix: /v1  group:  user)service user-api {  @handler UserLogin  post /user/login (UserLoginReq) returns (UserLoginResp)  @handler UserInfo  post /user/info (UserInfoReq) returns (UserInfoResp)}@server (  prefix: /v1  group:  admin)service user-api {  @handler AdminAdd  get /admin/add (AdminAddReq) returns (AdminAddResp)  @handler AdminUpdate  get /admin/update (AdminUpdateReq) returns (AdminUpdateResp)}

生成的路由文件(handler/routes.go)如下所示:

go
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {  server.AddRoutes(    []rest.Route{      {        Method:  http.MethodPost,        Path:    "/user/login",        Handler: user.LoginHandler(serverCtx),      },      {        Method:  http.MethodPost,        Path:    "/user/info",        Handler: user.InfoHandler(serverCtx),      },    },    rest.WithPrefix("/v1"),  )    server.AddRoutes(    []rest.Route{      {        Method:  http.MethodGet,        Path:    "/admin/add",        Handler: admin.AddHandler(serverCtx),      },      {        Method:  http.MethodGet,        Path:    "/admin/update",        Handler: admin.UpdateHandler(serverCtx),      },    },    rest.WithPrefix("/v1"),  )}

从路由文件可以看出分组并不会改变 URL 路由,仅仅只是为了把多个路由和逻辑代码进行分类而已。

参数获取

go-zero 通过在 tag 中来声明参数接收规则,但是不支持多 tag 来接收参数,即一个字段只能有一个 tag。目前 go-zero 支持的参数接收规则如下:

1. json

  • 说明:用于处理 JSON 格式的请求体和响应体,对于 POST 和 PUT 请求,用于接收 JSON 格式的请求体;对于响应,会将数据序列化为 json 格式并响应。

  • 示例:

    go
    type MyRequest struct {    Name string `json:"name"`}

2. path

  • 说明:用于从 URL 路径中提取路由参数。

  • 示例:

    go
    // URL: /user/:idtype MyRequest struct {    Id string `path:"id"`}

3. form

  • 说明:用于解析 POST 请求的Content-Typeapplication/x-www-form-urlencoded 或者 multipart/form-data 编码的表单参数。也可以用于解析 GET 请求的 URL 中的 querystring 参数。

  • 示例:

    go
    type LoginForm struct {    Username string `form:"username"`    Password string `form:"password"`}

4. header

  • 说明:用于将 HTTP 请求头映射到结构体字段。对于需要根据请求头做逻辑处理的场景非常有用(例如,读取认证信息、内容类型等)。

  • 示例:

    go
    type AuthRequest struct {    ContentLength int `header:"Content-Length"`}

参数校验规则

可以通过在 tag 中来声明参数接收规则,除此之外,还支持参数的校验,参数校验的规则仅对 请求体 有效,参数校验的规则写在 tag value 中,目前 go-zero 支持的参数校验规则如下:

接收规则 说明 示例
optional 当前字段是可选参数,允许为零值(zero value) json:"foo,optional"
options 当前参数仅可接收的枚举值 json:"gender,options=[foo,bar]"
default 当前参数默认值 json:"gender,default=male"
range 当前参数数值有效范围,仅对数值有效,写法规则详情见下 json:"age,range=[0:120]"

Range 表达式值规则:

  1. 左开右闭区间:(min:max],表示大于 min 小于等于 max,当 min 缺省时,min 代表数值 0,当 max 缺省时,max 代表无穷大,min 和 max 不能同时缺省
  2. 左闭右开区间:[min:max),表示大于等于 min 小于 max,当 max 缺省时,max 代表数值 0,当 min 缺省时,min 代表无穷大,min 和 max 不能同时缺省
  3. 闭区间:[min:max],表示大于等于 min 小于等于 max,当 min 缺省时,min 代表数值 0,当 max 缺省时,max 代表无穷大,min 和 max 不能同时缺省
  4. 开区间:(min:max),表示大于 min 小于 max,当 min 缺省时,min 代表数值 0,当 max 缺省时,max 代表无穷大,min 和 max 不能同时缺省

中间件

声明中间件

可以通过 api 语言来声明中间件, 在 api 语言中,使用 middleware 关键字来声明中间件,中间件的声明格式如下:

text
@server(    middleware: DemoMiddleware)

然后使用goctl api go --api user.api --dir .命令来自动生成声明中间件的代码。使用该命令后 internal 目录下会多出来一个 middleware 目录用来存放中间件。生成的中间件代码如下:

go
type DemoMiddleware struct {}func NewDemoMiddleware() *DemoMiddleware {    return &DemoMiddleware{}}func (m *DemoMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {    return func(w http.ResponseWriter, r *http.Request) {      // TODO generate middleware implement function, delete after code implementation          // Passthrough to next handler if need        next(w, r)    }}

中间件的代码是一个结构体,结构体中有一个 Handle 方法,这个方法是中间件的核心方法。该方法的介绍如下:

  • 参数:http.HandlerFunc
  • 返回值:http.HandlerFunc
  • 使用说明:对请求进行处理,比如鉴权,日志记录等等,然后将请求传递给下一个中间件或者 handler。

逻辑代码编写

由于只是一个 demo 练习,所以中间件的任务是设置 Cookie 并统计中间件运行所花费的时间,然后将该时间通过上下文传递到下一个路由处理函数中。中间件代码如下:

go
func (m *DemoMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {    return func(w http.ResponseWriter, r *http.Request) {      // 开始计算时间      nowTime := time.Now()          // 中间件的业务逻辑部分: 设置cookie返回给前端      fmt.Print("开始调用中间件")      http.SetCookie(w, &http.Cookie{          Name:  "session_id",          Value: "123456",      })          // 计算花费的时间,并将花费的时间传递给下一个路由(通过在上下文当中设置)      sub := time.Since(nowTime).Microseconds()      ctx := context.WithValue(r.Context(), "cost", sub)          // 创建一个新的请求,带有新的上下文      newReq := r.WithContext(ctx)      next.ServeHTTP(w, newReq)    }}

路由处理函数的逻辑代码如下:

go
func (l *UserLoginLogic) UserLogin(req *types.LoginRequest) (resp *types.LoginResponse, err error) {    // 接收上下文中的cost    value, _ := l.ctx.Value("cost").(int64)        // 接收请求参数    username := req.Username    password := req.Password    if username != "" && password != "" {        costStr := strconv.FormatInt(value, 10)        return &types.LoginResponse{            Code:    http.StatusOK,            Message: "登录成功," + "中间件花费的时间:[" + costStr + "]微秒",        }, err    } else {      return nil, err    }}

依赖注入

go-zero中,是通过ServiceContext来管理和注入依赖项。所以在中间件的逻辑代码编写完成后,还需要配置依赖。配置如下:

go
type ServiceContext struct {    Config         config.Config    DemoMiddleware rest.Middleware}func NewServiceContext(c config.Config) *ServiceContext {    return &ServiceContext{        Config:         c,        DemoMiddleware: middleware.NewDemoMiddleware().Handle,    }}

也就是在 ServiceContext 中添加 DemoMiddleware 依赖配置,这个 DemoMiddleware 变量名并不是随便取的,而是使用命令goctl api go --api user.api --dir .后,在自动生成路由文件(router.go)中[]rest.Middleware{serverCtx.DemoMiddleware}可以知道是哪一个配置项。

测试

运行后的测试结果如下:

image-20231213112635391

image-20231213112656925

jwt 验证

api 文件定义

text
service user-api {    @handler userLogin    post /login (LoginRequest) returns (LoginResponse)}@server (    jwt: Auth // 开启 jwt 认证)service user-api {    @handler userInfo    get /userInfo (UserInfoRequest) returns (UserInfoResponse)}

如上所示,通过在 @server 中来通过 jwt 关键字声明了开启 jwt 认证,且该 jwt 认证仅对其对应的路由有用,如上的 jwt 仅对 /userInfo 生效,对 /login 是不生效的。

jwt 配置编写

在 user-api.yaml 文件中添加如下内容:

text
Auth:  AccessSecret: secretKey  AccessExpire: 3600

同样,需要映射到结构体中。所以在 config.go 文件中添加如下配置:

go
type Config struct {    rest.RestConf        // JWT 认证需要配置密钥和过期时间    Auth struct {        AccessSecret string        AccessExpire int64    }}

使用 goctl 命令生成的路由文件中,在路由文件中可以看到如下内容:

go
server.AddRoutes(    []rest.Route{        {            Method:  http.MethodGet,            Path:    "/userInfo",            Handler: userInfoHandler(serverCtx),        },    },    rest.WithJwt(serverCtx.Config.Auth.AccessSecret),)

rest.WithJwt(serverCtx.Config.Auth.AccessSecret)表明/userInfo 路由需要进行 jwt 认证才能执行。

逻辑代码编写

  1. 登录功能的逻辑代码如下:

    go
    func (l *UserLoginLogic) getJwtToken(secretKey, username string, iat, seconds int64) (string, error) {   claims := make(jwt.MapClaims)   claims["exp"] = iat + seconds   claims["iat"] = iat   claims["username"] = username   token := jwt.New(jwt.SigningMethodHS256)   token.Claims = claims   return token.SignedString([]byte(secretKey))}func (l *UserLoginLogic) UserLogin(req *types.LoginRequest) (resp *types.LoginResponse, err error) {   // 接收上下文中的cost   value, _ := l.ctx.Value("cost").(int64)    // iat表示token的颁发时间   iat := time.Now().Unix()   // seconds表示token的有效期   seconds := l.svcCtx.Config.Auth.AccessExpire   secretKey := l.svcCtx.Config.Auth.AccessSecret    // 接收请求参数   username := req.Username   password := req.Password   if username != "" && password != "" {     token, err := l.getJwtToken(secretKey, username, iat, seconds)     if err != nil {       logx.Infof("生成Token错误:%v\n", err.Error())     }          costStr := strconv.FormatInt(value, 10)      return &types.LoginResponse{       Code:    http.StatusOK,       Message: "登录成功," + "中间件花费的时间:[" + costStr + "]微秒",       Token:   token,     }, err   } else {     return nil, err   }}

    getJwtToken 方法用来生成 token 字符串。其中AccessExpireAccessSecret是从依赖中的配置中去获取。

  2. 返回用户信息的逻辑代码如下:

    go
    func (l *UserInfoLogic) UserInfo(req *types.UserInfoRequest) (resp *types.UserInfoResponse, err error) {   id := req.Id   if id != "" {     username, _ := l.ctx.Value("username").(string)     return &types.UserInfoResponse{       Username: username,       Age:      18,       Other: map[string]interface{}{         "address": "江西",         "hobby":   []string{"篮球", "足球", "双色球"},       },     }, err   } else {     return nil, err   }}

测试

测试结果如下:

image-20231213185420268

image-20231213185621816