一、基本介绍

v2.0版本开始,框架的Server组件提供了规范化的路由注册方式,更加适合团队规范化的使用场景,实现了以下特性:

  • 规范化API按照结构化编程设计
  • 规范化API接口方法参数风格定义
  • 更加简化的路由注册与维护
  • 统一接口返回数据格式设计
  • 保障代码与接口文档同步维护
  • 自动的API参数对象化接收与校验
  • 自动生成基于标准OpenAPIv3协议的接口文档
  • 自动生成SwaggerUI页面

二、简单示例

1、配置文件

这里使用YAML配置文件:config.yaml

server:
  address:     ":8199"
  openapiPath: "/api.json"
  swaggerPath: "/swagger"

2、示例代码

我们从一个简单的Hello例子开始:

package main

import (
	"context"
	"fmt"

	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

type HelloReq struct {
	g.Meta `path:"/hello" method:"get"`
	Name   string `v:"required" dc:"Your name"`
}
type HelloRes struct {
	Reply string `dc:"Reply content"`
}

type Hello struct{}

func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) {
	g.Log().Debugf(ctx, `receive say: %+v`, req)
	res = &HelloRes{
		Reply: fmt.Sprintf(`Hi %s`, req.Name),
	}
	return
}

func main() {
	s := g.Server()
	s.Use(ghttp.MiddlewareHandlerResponse)
	s.Group("/", func(group *ghttp.RouterGroup) {
		group.Bind(
            new(Hello),
        )
	})
	s.Run()
}

拷贝这段代码,我们运行起来试试,终端输出信息如下:

2021-11-19 23:31:35.277 25580: http server started listening on [:8199] 

  SERVER  | DOMAIN  | ADDRESS | METHOD |   ROUTE    |                          HANDLER                          |    MIDDLEWARE      
----------|---------|---------|--------|------------|-----------------------------------------------------------|--------------------
  default | default | :8199   | ALL    | /*         | github.com/gogf/gf/v2/net/ghttp.MiddlewareHandlerResponse | GLOBAL MIDDLEWARE  
----------|---------|---------|--------|------------|-----------------------------------------------------------|--------------------
  default | default | :8199   | ALL    | /api.json  | github.com/gogf/gf/v2/net/ghttp.(*Server).openapiSpec-fm  |                    
----------|---------|---------|--------|------------|-----------------------------------------------------------|--------------------
  default | default | :8199   | GET    | /hello     | main.(*Hello).Say                                         |                    
----------|---------|---------|--------|------------|-----------------------------------------------------------|--------------------
  default | default | :8199   | ALL    | /swagger/* | github.com/gogf/gf/v2/net/ghttp.(*Server).swaggerUI-fm    | HOOK_BEFORE_SERVE  
----------|---------|---------|--------|------------|-----------------------------------------------------------|--------------------

可以看到,除了我们的业务路由之外,Server自动帮我们注册了两个路由:/api.json/swagger/*。前者是自动生成的基于标准的OpenAPIv3协议的接口文档,后者是自动生成SwaggerUI页面,方便开发者查看和调试。这两个功能默认是关闭的,开发者可以通过前面示例中的openapiPathswaggerPath两个配置项开启。

3、接口文档

接口文档通过OpenAPIv3协议生成,一般来说需要结合相应的UI工具查看,地址:http://127.0.0.1:8199/api.json

由于OpenAPIv3协议是规范的接口定义协议,因此开发者根据协议内容可以做很多事,例如:自定义UI展示、Client代码生成、协议转换等等。

4、SwaggerUI

我们来看看这个SwaggerUI页面:http://127.0.0.1:8199/swagger/

这里只有一个我们的路由地址以及对应的输入输出结构体。当然,这只是个简单的示例,你可以在真实的项目中通过一些配置使得页面更加丰富多彩。

我们接着在这个页面上做下接口测试吧:

嗯,接口返回了一个固定数据格式的Json内容,但是能看到其中的data为我们需要的返回结果。

5、返回中间件

等等,似乎漏掉了什么东西?是的,我们这里使用了一个Server组件提供的中间件,它是拿来做什么的呢?我们开看看它的方法定义:

是的,它在我们没有提供自定义的返回数据格式处理中间件时,使用了一个默认的中间件处理我们的请求,并返回了一个默认的数据格式。

三、如何使用

1、路由方法定义

从上面的例子中,我们可以看到,路由方法定义使用固定的格式:

func Handler(ctx context.Context, req *Request) (res *Response, err error)

其中输入参数和输出参数都是两个,并且都是必须的一个都不能少。简单介绍下:

参数说明注意事项
ctx context.Context上下文Server组件会自动从请求中获取并传递给接口方法
req *Request请求对象

就算没有接收参数也要定义,因为请求结构体中不仅仅包含请求参数的定义,也包含了接口的请求定义。

res *Response返回对象

就算没有返回参数也要定义,因为返回结构体中不仅仅包含返回参数的定义,也可以包含接口返回定义。

err error错误对象Server通过该参数判断接口执行成功或失败。

2、请求/返回结构体

在规范化路由注册中,非常重要的是请求/返回结构体的定义,在该结构体不仅仅包含了输入参数的定义,也包含了接口的定义,特别是路由地址、请求方法、接口描述等信息。为保证命名规范化,输入数据结构以XxxReq方式命名,输出数据结构以XxxRes方式命名。即便输入或者输出参数为空,也需要定义相应的数据结构,这样的目的一个是便于后续扩展,另一个是便于接口信息的管理。关于结构体中涉及到OpenAPIv3协议的标签介绍请查看后续章节。

请求参数自动转换到请求数据结构,字段映射转换不区分大小写,也会自动忽略特殊字符。

3、数据校验

请求结构体在进入API接口执行前将会被自动执行校验,如果其中一条规则校验失败,那么将终止后续规则的校验。校验功能使用的是框架统一的校验组件,具体请参考:数据校验

4、数据返回

接口的数据返回处理需要设置统一的后置中间件,当然也可以使用Server默认提供的数据返回中间件。开发者自定义中间件时可以参考Server默认提供的中间件。注意其中的一个重要的方法:

// GetHandlerResponse retrieves and returns the handler response object and its error. 
// return type handlerResponse struct {
//		Object interface{}
//		Error  error
//	}
func (r *Request) GetHandlerResponse() interface{} 

通过后置中间件执行时通过请求对象的GetHandlerResponse方法获取当前业务执行的结果,并根据需要做相应处理。

5、路由注册

我们推荐使用对象化的方式来管理所有路由方法,并通过分组路由的Bind方法执行统一注册。

需要注意的是,在规范化路由方式下,路由地址以及请求方式将由请求结构体在g.Meta中定义,通过分组路由可以定义分组下的所有路由前缀。

四、OpenAPIv3协议

Server组件自动生成的接口文档使用的是最新的OpenAPIv3协议。更多介绍请参考章节:接口文档

五、Ctx中的Request对象

我们可以通过RequestFromCtx/g.RequestFromCtx方法从ctx中获取Request对象。

方法定义:

func RequestFromCtx(ctx context.Context) *Request

使用示例:

func (c *cHello) Hello(ctx context.Context, req *apiv1.HelloReq) (res *apiv1.HelloRes, err error) {
	g.RequestFromCtx(ctx).Response.Writeln("Hello World!")
	return
}

六、常见问题

1、在规范路由下,同一接口如何支持多种HTTP Method提交方式

一个接口应当只做一件事情,HTTP Method是有意义的,一个接口支持多种HTTP Method方式是接口设计不合理,在规范路由下不支持。建议重新审视接口设计。

如果确实需要注册类似于ALL这种请求处理路由,在标准的OpenAPI协议里面不支持,可以不使用规范路由,而是使用普通的路由注册方式,将方法定义为func(r *ghttp.Request)即可。不同的路由注册方式可以结合使用,虽然不是很推荐。

2、在使用默认提供的Response结构体下,如何让Data字段只返回数组而无需指定名称的键值对

使用类型别名即可。

源码地址:https://github.com/gogf/gf/tree/master/example/httpserver/response_with_json_array

结果示例:




Content Menu

  • No labels

50 Comments

  1. 请教一下,现在可以做到在swagger接口页面全局设置header头的鉴权字段么


    1. 我试了是支持的。 goai里面的结构体对应的就是openApi3官网的yaml结构. 

      openapi.Components = goai.Components{
      SecuritySchemes: goai.SecuritySchemes{
      "ApiKeyAuth": goai.SecuritySchemeRef{
      Ref: "", // 暂时还不知道该值是干什么用的
      Value: &goai.SecurityScheme{
      Type: "apiKey",
      In: "header",
      Name: "token",
      },
      },
      },
      }
      1. beautiful mtgnorton 

        openapi.Components = goai.Components{
        		SecuritySchemes: goai.SecuritySchemes{
        			"ApiKeyAuth": goai.SecuritySchemeRef{
        				Ref: "", // 暂时还不知道该值是干什么用的
        				Value: &goai.SecurityScheme{
        					Type: "apiKey",
        					In:   "header",
        					Name: "token",
        				},
        			},
        		},
        	}

        代码加上了,swagger页面上出现了Authorize按钮,设置key之后,使用try it out 发出的请求并没有在header带上token。 怎么回事呢?

        1. 经过研究发现,openapi.Components配置SecuritySchema之后只是让swagger定义了一个校验的方式,并没有让每一个API使用这个校验,必须在每一个path上配置上这个校验,才能真正生效。但是发现gf并没有支持这个功能,经过修改gf的代码,实现了该效果:swagger_ui.png (1217×599) (smartbear.co) 类似于这个图上的 那个 Authorize按钮和每个API后边的那个锁图标,实际体验可以看 Swagger UI 这个demoapiKey方式。

          我实现的方式为:

          1. 增加如上的SecuritySchemes
          2. 修改我本地的E:\go-workspace\pkg\mod\github.com\gogf\gf\v2@v2.0.0-rc3\protocol\goai\goai_path.go文件中的91行,把option的初始化改为:

            var (
            		path                 = Path{}
            		inputMetaMap         = gmeta.Data(inputObject.Interface())
            		outputMetaMap        = gmeta.Data(outputObject.Interface())
            		isInputStructEmpty   = oai.doesStructHasNoFields(inputObject.Interface())
            		inputStructTypeName  = oai.golangTypeToSchemaName(inputObject.Type())
            		outputStructTypeName = oai.golangTypeToSchemaName(outputObject.Type())
            		operation            = Operation{
            			Responses: map[string]ResponseRef{},
            			Security: &SecurityRequirements{SecurityRequirement{"APIKeyAuth": []string{}}},   // 这一行是新增
            		}
            	)

          此时再看swaggerUI页面就可以看到那个 Authorize按钮和每个API后边的那个锁图标。使用try it out 按钮也可以实现在header上自动加上想要的token

          郭强 海亮 gf是否有更好的方式支持,而不是我这样修改gf的源码?

          1. 这个只是OpenAPI接口定义的展示而已,真正要实现功能还是得依靠中间件。

            关于全局设置OpenAPI属性,可以Server.GetOpenAPI后修改goai.OpenAPI对象对应属性即可,不用修改源码。

            focus-single项目示例代码有例子:https://github.com/gogf/focus-single/blob/786c16e7516dd68c11352cc4fce3152353c20c60/internal/cmd/cmd.go#L110

            1. 首先,全局设置OpenAPI属性的确可以使用Server.GetOpenAPI后修改goai.OpenAPI,但是我这里的需求是每一个api的path对应的swagger定义,不是全局的,所以这里提供的方法是无效的。
              另外,无论是中间件也好,hook也好,都是request--》response 的流程过程中进行拦截处理,而我这里的诉求是在server启动之后,生成openAPI的定义时候触发。所以,中间件不行。
              目前只能改gf的源码。请仔细看我的问题描述  

  2. 强哥,自动生成的 swagger 页面中,【Responses】栏只列出了 200 状态。

    [假装有截图]

    是否有办法能自定义每个路由的 400、401、403、500 错误呢?

    或者为所有路由配置统一的 400、401、403、500 错误。

    1. 这个只有自己获取ServerOpenApi对象后自己去扩展了,无法在Req对象中通过结构体标签绑定,因为那样太复杂了,可维护性太低。

  3. http://gf.l11.cn/   规范化路由代码生成

    1. 你这有点意思哦。

    2. 你这个是真的棒...点赞

    3. https://gitee.com/Guwengo/gf_code_auto

      基于后端 结构体  自动生成CURD代码的版本来了

      基于GF自己的模板引擎制作

      更方便

      更便捷

  4. gf 工具里面应该集成这个功能,模板能定义最好,起码把该有的文件给创建了.哈哈

  5. swagger ui在使用gf-user-demo时,没有try it out按钮哦

  6. 使用v2.0.1版本及以上版本会发现 swaggerUI被修改了为 redoc了。redoc缺少了 Try it console。能不能提供一个方式,可以选择swaggerUI或者redoc 么?

    1. 可以自己开个静态页面展示自己的SwaggerUI,然后仅使用ServerOpenAPI地址。

  7. 灵活性太受限了

         比如一个接口,可能有多个路由,但入参结构体相同,应如何处理呢?


    1. 一接口多路由:参考常见问题。

      请求参数复用:将公用的结构体进行嵌套复用。

      1. 指这个吗  “ 1、在规范路由下,同一接口如何支持多种HTTP Method提交方式 ”

        我的意思是,/api/hello, /welcome/sayhello, 两个路由地址,可能指向同一个 handler (如 Tom.SayHello())

        1. 规范路由要求会比较严格一些,"规范"二字由此而来。你描述的case已经不太规范了,但你可以通过两种方式来实现:

          1、适当规范你的路由设计,使用不同的路由前缀来区分不同的业务模块。这样你定义的输入输出结构体不用重新定义,只需要在路由注册的时候使用分组路由,绑定不同的路由前缀。

          2、定义不同的Req结构体,内部嵌套相同的结构体来实现。


  8. 如果希望自定义response 该如何处理,比如 code 为 string 类型,

    不同的分组路由,使用不一样的response。   历史遗留系统,返回样式五花八门

  9. 请教一下,新版本的路由注册,我有点迷,普通的用法我会

    比如我发送 GET 和 PUT 请求,是 user/:id ,那么 g.Meta 里面的 path 标签应该如何写呢?

    如果我打算把参数放在路径里面,不通过 struct 里面的其它属性来定义,我该如何在控制器中获取到这个 :id 呢?

    我用了 

    g.RequestFromCtx(ctx).Get("id")

    好像获取不到

    1. 无论参数是request body还是url上的,都需要在结构体里定义。

      g.Meta      `path:"/user/{uuid}" method:"put" `
      1. 确实取到值了,大哥你好聪明啊,我读完文档,没找到相关用法,谢谢!

      2. 我又遇到了一个新的疑问

        type RoleUpdateReq struct {
        	g.Meta `path:"/backend/role" tags:"三体" method:"put" summary:"故事角色"`
        	Id     uint `json:"id" v:"required#请选择需要修改的角色"`
        	RoleBaseReq
        }

        这里不能是 Id 否则获取到的就是个莫名其妙的数值,我传的 Id 是2,但是获取到的是下面的 13

        {
            Id:            13,
            Avatar:        "222",
            Name:          "string",
            Nickname:      "string",
            SurvivalStage: "string",
            Nationality:   "string",
            Achievement:   "string",
            Gender:        0,
            Content:       "string",
            IfShow:        0,
        }

        如果把 Id 改成 RoleId ,就可以正确获取到值,这是为什么呢?

        写成下面就是正常的,这是规定吗?

        type RoleUpdateReq struct {
        	g.Meta `path:"/backend/role" tags:"三体" method:"put" summary:"故事角色"`
        	RoleId     uint `json:"role_id" v:"required#请选择需要修改的角色"`
        	RoleBaseReq
        }
        1. 问题已解决,补充一下这个答案。

          一位朋友提示会不会是中间件赋值的影响

          我在断点调试的过程中发现了 ID 被覆盖了

          原因是 JWT 中 IdentityKey 我用了 id 这个作为 key

          如果替换成一个其它的字段,就不会影响前端业务字段了

          func init() {
          	auth = jwt.New(&jwt.GfJWTMiddleware{
          		Realm:           "threeboby",
          		Key:             []byte("mingyuejishiyou"),
          		Timeout:         time.Minute * 300,
          		MaxRefresh:      time.Minute * 5,
          		IdentityKey:     "auth_unique_key",
          		TokenLookup:     "header: Authorization, query: token, cookie: jwt",
          		TokenHeadName:   "Bearer",
          		TimeFunc:        time.Now,
          		Authenticator:   Authenticator,
          		Unauthorized:    Unauthorized,
          		PayloadFunc:     PayloadFunc,
          		IdentityHandler: IdentityHandler,
          	})
          }
  10. 可能我的理解级别不够,分组路由不挺好吗,为什么非要卷出一个规范路由,这所谓的规范,反正让我背这一串字符串,挺难,接口上百个,感觉这字符串难维护,比如:`path:"/user/{uuid}" method:"put" `

    1. 不同场景,选择适合的就好。

  11. 对于这种规范路由的方式,如果返回的格式不同,是不是就不能用了?比如想下面两种格式。如果能用的话,统一返回值处理那里应该怎么做?

    {
    	"code": 200,
    	"message": "success"
    	"data":{
    		"id":1
    	}
    }
    
    {
    	"code": 200,
    	"message": "success"
    	"data":[
    		{"id":1},
    		{"id":2},
    		]
    	"total":2
    }
    1. 重写MiddlewareHandlerResponse 根据业务需要去处理返回内容.

    2. 另外 建议将业务内容包到data内部保持风格一致.


      {
          "code": 200,
          "message": "success",
          "data": {
              "total": 2,
              "list": [
                  {
                      "id": 1
                  },
                  {
                      "id": 2
                  }
              ]
          }
      }
  12. 请教一下,请求结构体中的参数从 http header 获取参数的情况,使用 in:"header" 是否可以直接获取值。

    我本地测试发现不行,不知是框架暂时不支持还是我使用姿势不正确,望可以解答。谢谢。

    type GetUserInfoReq struct {
       g.Meta        `tags:"用户" sm:"获取用户信息"`
       Authorization string `json:"Authorization" dc:"用户token" in:"header"`
    }
    1. 还没实现这个标签 先用手动获取赋值处理吧.

      1. 感谢。现在是直接由ctx,从header里取的。

        token := g.RequestFromCtx(ctx).GetHeader("Authorization")
        1. auth在控制器处理的话复用效率比较低,一般在前置中间件中处理,而中间件取header数据也是从request对象取,其实并不冲突.

  13. 两个问题

    1.  如下,如果一个路由需要两种请求方式怎么解,我要在conteoller里创建两个 结构体以及函数吗
    2. 我在声明中注入了我自己的tag 比如 auth 我怎么取值
    type RoleUpdateReq struct {
    	g.Meta `path:"/backend/role" tags:"三体" method:"get/post" auth:"true"`
    }

    1. ctrl & logic 层可以拿到RoleUpdateReq,通过反射就能拿 tags/methmod...


      1. 我想是现在中间层,在请求处理前进行统一处理,也就是在controller层前一层处理掉没有登录的请求,并且返回统一的响应值,按照你的说话,可能我需要在进去在处理,那每一个函数都要有自己的鉴权处理,我觉得可能有一些麻烦

  14. 原谅我吐槽一下,这个api结构体的字段tag都有哪些,从截图里我看到了dc是字段描述,好歹文档提一嘴吧

      1. 好嘞,多谢

  15. 我想出一个前台接口文档,一个后台接口文档,同一个项目,应该怎么弄

      1. 可以使用两个Server不同的监听端口。
      2. 可以使用两个Server相同的监听端口但不同的URI。这个算高级用法,可以看下Server中有个SetListener方法。
  16. 最新版本,我是想设置ctx的变量的g.RequestFromCtx(ctx).SetCtxVar(consts.CtxKnownErr, "true") 
    但是g.RequestFromCtx(ctx)会报错
    interface conversion: interface {} is *ghttp.Request, not *ghttp.Request (types from different packages)

    1. 我发现是默认引用了

      "github.com/gogf/gf/frame/g"

      换成最新版就可以了

        "github.com/gogf/gf/v2/frame/g"