Categorygithub.com/Chendemo12/fastapi
repositorypackage
0.2.11
Repository: https://github.com/chendemo12/fastapi.git
Documentation: pkg.go.dev

# Packages

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# README

FastApi-Golang (包装器)

  • 类似python-FastApiGolang实现;
  • 提供OpenApi文档的自动生成,提供SwaggerRedoc文档;
  • 通过一次代码编写同时生成文档和参数校验,无需编写swagger的代码注释;
  • 直接在路由函数中使用经过校验的请求参数,无需通过ShouldBind等方法;
  • 支持请求参数的自动校验:
    • 包括:QueryPathHeaderCookieFormFile 参数的自定校验(HeaderCookieFormFile正在支持中)
    • Body参数的自动校验,支持json/multipart格式,json 基于validator
  • 包装器,不限制底层的HTTP框架,支持ginfiber等框架,并可轻松的集成到任何框架中;

Usage 使用说明:

go get https://github.com/Chendemo12/fastapi

1. 快速实现

  1. 创建一个Wrapper对象:
import "github.com/Chendemo12/fastapi"

// 可选的 fastapi.Config 参数
app := fastapi.New(fastapi.Config{
    Version:     "v1.0.0",
    Description: "这是一段Http服务描述信息,会显示在openApi文档的顶部",
    Title:       "FastApi Example",
})

显示效果

显示效果
  1. 指定底层HTTP路由器,也称为Mux, 为兼容不同的Mux,还需要对Mux进行包装,其定义为MuxWrapper
import "github.com/Chendemo12/fastapi/middleware/fiberWrapper"

// 此处采用默认的内置Fiber实现, 必须在启动之前设置
mux := fiberWrapper.Default()
app.SetMux(mux)

// 或者自定义Fiber实现
fiberEngine := fiber.New(fiber.Config{
    Prefork:       false,                   // 多进程模式
    CaseSensitive: true,                    // 区分路由大小写
    StrictRouting: true,                    // 严格路由
    ServerHeader:  "FastApi",               // 服务器头
    AppName:       "fastapi.fiber",         // 设置为 Response.Header.Server 属性
    ColorScheme:   fiber.DefaultColors,     // 彩色输出
    JSONEncoder:   utils.JsonMarshal,       // json序列化器
    JSONDecoder:   utils.JsonUnmarshal,     // json解码器
})                                          // 创建fiber引擎
mux := fiberWrapper.NewWrapper(fiberEngine) // 创建fiber包装器
app.SetMux(mux)
  1. 创建路由:
  • 实现fastapi.GroupRouter接口,并创建方法以定义路由:
// 创建一个结构体实现fastapi.GroupRouter接口
type ExampleRouter struct {
	fastapi.BaseGroupRouter
}

func (r *ExampleRouter) Prefix() string { return "/api/example" }

func (r *ExampleRouter) GetAppTitle(c *fastapi.Context) (string, error) {
	return "FastApi Example", nil
}

type UpdateAppTitleReq struct {
	Title string `json:"title" validate:"required" description:"App标题"`
}

func (r *ExampleRouter) PatchUpdateAppTitle(c *fastapi.Context, form *UpdateAppTitleReq) (*UpdateAppTitleReq, error) {
	return form, nil
}

// 注册路由
app.IncludeRouter(&ExampleRouter{})

显示效果

显示效果
  1. 启动:
app.Run("0.0.0.0", "8090") // 阻塞运行
  1. 完整示例
package test

import (
	"github.com/Chendemo12/fastapi"
	"github.com/Chendemo12/fastapi/middleware/fiberWrapper"
	"testing"
)

// ExampleRouter 创建一个结构体实现fastapi.GroupRouter接口
type ExampleRouter struct {
	fastapi.BaseGroupRouter
}

func (r *ExampleRouter) Prefix() string { return "/api/example" }

func (r *ExampleRouter) GetAppTitle(c *fastapi.Context) (string, error) {
	return "FastApi Example", nil
}

type UpdateAppTitleReq struct {
	Title string `json:"title" validate:"required" description:"App标题"`
}

func (r *ExampleRouter) PatchUpdateAppTitle(c *fastapi.Context, form *UpdateAppTitleReq) (*UpdateAppTitleReq, error) {
	return form, nil
}

func TestExampleRouter(t *testing.T) {
	// 可选的 fastapi.Config 参数
	app := fastapi.New(fastapi.Config{
		Version:     "v1.0.0",
		Description: "这是一段Http服务描述信息,会显示在openApi文档的顶部",
		Title:       "FastApi Example",
	})

	// 此处采用默认的内置Fiber实现, 必须在启动之前设置
	mux := fiberWrapper.Default()
	app.SetMux(mux)

	// 注册路由
	app.IncludeRouter(&ExampleRouter{})

	app.Run("0.0.0.0", "8090") // 阻塞运行
}

显示效果

显示效果

2. 详细实现

2.1 路由方法定义

  • 路由定义的关键在于实现GroupRouter 接口:

    // GroupRouter 结构体路由组定义
    // 用法:首先实现此接口,然后通过调用 Wrapper.IncludeRoute 方法进行注册绑定
    type GroupRouter interface {
    	// Prefix 路由组前缀,无需考虑是否以/开头或结尾
    	// 如果为空则通过 PathSchema 方案进行格式化
    	Prefix() string
    	// Tags 标签,如果为空则设为结构体名称的大驼峰形式,去掉可能存在的http方法名
    	Tags() []string
    	// PathSchema 路由解析规则,对路由前缀和路由地址都有效
    	PathSchema() pathschema.RoutePathSchema
    	// Summary 允许对单个方法路由的文档摘要信息进行定义
    	// 方法名:摘要信息
    	Summary() map[string]string
    	// Description 方法名:描述信息
    	Description() map[string]string
    	// Path 允许对方法的路由进行重载, 方法名:相对路由
    	// 由于以函数名确定方法路由的方式暂无法支持路径参数, 因此可通过此方式来定义路径参数
    	// 但是此处定义的路由不应该包含查询参数
    	// 路径参数以:开头, 查询参数以?开头
    	Path() map[string]string
    
    	// InParamsName 允许对函数入参名称进行修改,仅适用于基本类型和time.Time类型的参数
    	// 由于go在编译后无法获得函数或方法的入参名称,只能获得入参的类型和偏移量,
    	// 因此在openapi的文档生成中,作为查询参数的函数入参无法正确显示出查询参数名称,取而代之的是手动分配的一个虚假参数名,此名称会影响api的调用和查询参数的解析
    	// 对于此情况,推荐使用结构体来定义查询参数,以获得更好的使用体验
    	// 此外,对于入参较少的情况,允许通过手动的方式来分配一个名称。
    	//
    	//
    	//	对于方法:ManyGet(c *Context, age int, name string, graduate bool, source float64)
    	//
    	//	在未手动指定名称的情况下, 查询参数解析为:
    	//		age int => int_2
    	//		name string => string_3
    	//		graduate bool => bool_4
    	//		source float64 => float64_5
    	//
    	//	通过一下方式来手动指定名称:
    	//		{
    	//			"ManyGet": {
    	//				2: "age",
    	//				3: "name",
    	//				4: "graduate",
    	//				5: "source",
    	//			},
    	//		}
    	InParamsName() map[string]map[int]string
    }
    
  • 对于路由组ExampleRouter来说:

    • TagsExampleRouter
    • 路由组前缀为手动定义的/api/example

2.1.1 满足路由定义的方法要求:

  • 方法需为指针接收器
  • 方法必须是导出方法
  • 方法名必须以HTTP操作名开头或结尾:Post, Patch, Get, Delete, Put
  • 返回值必须为2个参数:(XXX, error), 第二个参数必须是error类型,第一个参数为任意参数,但不建议是map类型,不能是nil
  • 第一个入参必须是fastapi.Context,
    • 对于Post, Patch, Put 至少有一个自定义参数作为请求体,如果不需要请求体参数则用fastapi.None代替
    • 对于Get, Delete只能有一个自定义结构体参数作为查询参数,cookies,header等参数

2.1.2 有关方法入参的解析规则:

  • 对于GetDelete
    • 有且只有一个结构体入参,被解释为查询/路径等参数
  • 对于Post, Patch, Put:
    • 最后一个入参被解释为请求体,其他入参被解释为查询/路径等参数

2.1.3 参数定义

  • 建议用结构体来定义所有参数:
  • 参数的校验和文档的生成遵循Validator的标签要求
  • 任何情况下json标签都会被解释为参数名,对于查询参数则优先采用query标签名
  • 任何模型都可以通过SchemaDesc() string方法来添加模型说明,作用等同于python.__doc__属性

示例1:

type IPModel struct {
	IP     string `json:"ip" description:"IPv4地址"`
	Detail struct {
		IPv4     string `json:"IPv4" description:"IPv4地址"`
		IPv4Full string `json:"IPv4_full" description:"带端口的IPv4地址"`
		Ipv6     string `json:"IPv6" description:"IPv6地址"`
	} `json:"detail" description:"详细信息"`
}

func (m IPModel) SchemaDesc() string { return "IP信息" }

type DomainRecord struct {
	IP struct {
		Record *IPModel `json:"record" validate:"required" description:"解析记录"`
	} `json:"ip" validate:"required"`
	Addresses []struct {
		Host string `json:"host"`
		Port string `json:"port"`
	} `json:"addresses" validate:"required,gte=1" description:"主机地址"`
	Timestamp int64 `json:"timestamp" description:"时间戳"`
}
  • 其文档如下:

    显示效果

    显示效果

2.1.4 路由url解析 RoutePathSchema

  • 方法开头或结尾中包含的http方法名会被忽略,对于方法中包含多个关键字的仅第一个会被采用:

    • PostDelet - > 路由为delte, 方法为Post
  • 允许通过PathSchema() pathschema.RoutePathSchema来自定义解析规则,支持以下规则:

    • LowerCaseDash:全小写-短横线
    • LowerCaseBackslash:全小写段路由
    • LowerCamelCase:将结构体名称按单词分割后转换为小驼峰的形式后作为相对路由
    • LowerCase:将方法名按单词分割后全部转换为小写字符再直接拼接起来
    • UnixDash/ Dash:将方法名按单词分割后用"-"相连接
    • Underline:将方法名按单词分割后用"_"相连接
    • Backslash:按单词分段,每一个单词都作为一个路由段
    • Original:原始不变,保持结构体方法名(不含HTTP方法名),只拼接成合法的路由
    • AddPrefix:用于在分段路由基础上添加一个前缀字符,作用于每一段路径,通常与其他方案组合使用
    • AddSuffix:用于在分段路由基础上添加一个后缀字符,作用于每一段路径,通常与其他方案组合使用
    • Composition:组合式路由格式化方案, 通过按顺序执行多个 RoutePathSchema 获得最终路由
  • 详细示例可见 pathschema_test.go

    func TestFormat(t *testing.T) {
    	type args struct {
    		schema       RoutePathSchema
    		prefix       string
    		relativePath string
    	}
    	tests := []struct {
    		name string
    		args args
    		want string
    	}{
    		{
    			name: "UnixDash",
    			args: args{
    				prefix:       "/api",
    				relativePath: "ReadUnixProcTree",
    				schema:       &UnixDash{},
    			},
    			want: "/api/Read-Unix-Proc-Tree",
    		},
    		{
    			name: "Underline",
    			args: args{
    				prefix:       "/api",
    				relativePath: "ReadUnixProcTree",
    				schema:       &Underline{},
    			},
    			want: "/api/Read_Unix_Proc_Tree",
    		},
    		{
    			name: "LowerCamelCase",
    			args: args{
    				prefix:       "/api",
    				relativePath: "ReadUnixProcTree",
    				schema:       &LowerCamelCase{},
    			},
    			want: "/api/readUnixProcTree",
    		},
    		{
    			name: "LowerCase",
    			args: args{
    				prefix:       "/api",
    				relativePath: "ReadUnixProcTree",
    				schema:       &LowerCase{},
    			},
    			want: "/api/readunixproctree",
    		},
    		{
    			name: "Backslash",
    			args: args{
    				prefix:       "/api",
    				relativePath: "ReadUnixProcTree",
    				schema:       &Backslash{},
    			},
    			want: "/api/Read/Unix/Proc/Tree",
    		},
    		{
    			name: "Original",
    			args: args{
    				prefix:       "/api",
    				relativePath: "ReadUnixProcTree",
    				schema:       &Original{},
    			},
    			want: "/api/ReadUnixProcTree",
    		},
    		{
    			name: "LowerCaseDash",
    			args: args{
    				prefix:       "/api",
    				relativePath: "ReadUnixProcTree",
    				schema:       Default(),
    			},
    			want: "/api/read-unix-proc-tree",
    		},
    		{
    			name: "LowerCaseUnderline",
    			args: args{
    				prefix:       "/api",
    				relativePath: "ReadUnixProcTree",
    				schema:       NewComposition(&LowerCase{}, &Underline{}),
    			},
    			want: "/api/read_unix_proc_tree",
    		},
    		{
    			name: "LowerCaseDashUnderline",
    			args: args{
    				prefix:       "/api",
    				relativePath: "ReadUnixProcTree",
    				schema:       NewComposition(&LowerCase{}, &UnixDash{}, &Underline{}),
    			},
    			want: "/api/read-_unix-_proc-_tree",
    		},
    		{
    			name: "LowerCaseDashUnderlineBackslash",
    			args: args{
    				prefix:       "/api",
    				relativePath: "ReadUnixProcTree",
    				schema:       NewComposition(NewComposition(&LowerCase{}, &UnixDash{}), &Underline{}, &Backslash{}),
    			},
    			want: "/api/read-_/unix-_/proc-_/tree",
    		},
    		{
    			name: "LowerCaseWithPreEqual",
    			args: args{
    				prefix:       "/api",
    				relativePath: "ReadUnixProcTree",
    				schema:       NewComposition(&LowerCase{}, &AddPrefix{Prefix: "="}, &Backslash{}),
    			},
    			want: "/api/=read/=unix/=proc/=tree",
    		},
    		{
    			name: "LowerCaseWithPostDash",
    			args: args{
    				prefix:       "/api",
    				relativePath: "ReadUnixProcTree",
    				schema:       NewComposition(&LowerCase{}, &AddSuffix{Suffix: "-"}, &Backslash{}),
    			},
    			want: "/api/read-/unix-/proc-/tree-",
    		},
    	}
    	for _, tt := range tests {
    		t.Run(tt.name, func(t *testing.T) {
    			if got := Format(tt.args.prefix, tt.args.relativePath, tt.args.schema); got != tt.want {
    				t.Errorf("Format() = %v, want %v", got, tt.want)
    			}
    		})
    	}
    }
    

示例2:

type DateTime struct {
	Name         *Name     `json:"name" query:"name"`
	Birthday     time.Time `json:"birthday" query:"birthday" description:"生日"` // 日期时间类型
	ImportantDay *struct {
		LoveDay          time.Time   `json:"love_day"`
		NameDay          time.Time   `json:"name_day"`
		ChildrenBirthday []time.Time `json:"children_birthday"`
	} `json:"important_day,omitempty" description:"纪念日"`
}

显示效果

显示效果

2.2 Config 配置项 app.go:Config

参数作用是否必选默认值
TitleAPP标题1.0.0
VersionAPP版本号FastAPI Application
DescriptionAPP描述FastAPI
ShutdownTimeout平滑关机,单位秒5
DisableSwagAutoCreate禁用OpenApi文档,但是不禁用参数校验false
StopImmediatelyWhenErrorOccurs是否在遇到错误字段时立刻停止校验, 对于有多个请求参数时,默认会检查每一个参数是否合法,并最终返回所有的错误参数信息,设为true以在遇到一个错误参数时停止后续的参数校验并直接返回错误信息。false
ContextAutomaticDerivationDisabled禁止为每一个请求创建单独的context.Context 。为每一个请求单独创建一个派生自Wrapper.Context()的ctx是十分昂贵的开销,但有时有时十分必要的,禁用后调用 Context.Context() 将会产生错误false

2.3 Wrapper 配置项 app.go:Wrapper

2.3.1 添加启动/关闭前同步事件 ,等同于 FastAPI.on_event(),

  • event_type: startup / shutdown
  • 同步执行,应避免阻塞;
  • startup 会在初始化完成后、listen之前依次调用;
  • shutdown 会在Context cancel 之后,mux shutdown之前依次调用;

2.3.2 设置设置路由错误信息格式化函数 SetRouteErrorFormatter

  • 由于路由方法handler的定义中,其返回值必须有2个参数,且最后一个参数必须为 error 接口,因此当handler返回错误时,如何合理的返回错误消息也十分的重要;

  • 默认情况下当handler返回错误时,Wrapper 会返回500错误码string类型的错误消息;

    错误消息

  • 允许通过 Wrapper.SetRouteErrorFormatter 方法来自定义错误消息:

    // 自定义错误格式
    type ErrorMessage struct {
    	Code      string `json:"code,omitempty" description:"错误码"`
    	Message   string `json:"message,omitempty" description:"错误信息"`
    	Timestamp int64  `json:"timestamp,omitempty"`
    }
    
    // 格式化路由函数错误消息
    func FormatErrorMessage(c *fastapi.Context, err error) (statusCode int, message any) {
    	return 400, &ErrorMessage{
    		Code:      "0x1234",
    		Message:   err.Error(),
    		Timestamp: time.Now().Unix(),
    	}
    }
    
    // 自定义错误格式
    app.SetRouteErrorFormatter(FormatErrorMessage, fastapi.RouteErrorOpt{
        StatusCode:   400,
        ResponseMode: &ErrorMessage{},
        Description:  "服务器内部发生错误,请稍后重试",
    })
    

    显示效果

    错误文档
  • 此时的接口返回值更新为:

    显示效果

    接口响应
  • RouteErrorFormatter的定义如下:

    // RouteErrorFormatter 路由函数返回错误时的处理函数,可用于格式化错误信息后返回给客户端
    //
    //	程序启动时会主动调用此方法用于生成openApi文档,所以此函数不应返回 map等类型,否则将无法生成openApi文档
    //
    //	当路由函数返回错误时,会调用此函数,返回值会作为响应码和响应内容, 返回值仅限于可以JSON序列化的消息体
    //	默认情况下,错误码为500,错误信息会作为字符串直接返回给客户端
    type RouteErrorFormatter func(c *Context, err error) (statusCode int, resp any)
    
  • 其中实际接口响应的状态码以RouteErrorFormatter的返回值为准,而非fastapi.RouteErrorOpt中的配置,fastapi.RouteErrorOpt的配置仅仅作用于文档显示。

2.3.3 使用依赖 DependenceHandle

// DependenceHandle 依赖函数 Depends/Hook
type DependenceHandle func(c *Context) error
  • 对于Wrapper而言,其本质是一个装饰器,是对具体的Mux的包装,因此其自身并没有中间件的概念,同时也是为了避免与Mux的中间件引起冲突,有关的WraperMux的核心交互定义如下:

    • Wraper会将使用者定义的每一个handler进行二次包装,并作为Muxhandler注册到路由器上,其包装后的定义如下:
    // Handler 路由函数,实现逻辑类似于装饰器
    //
    // 路由处理方法(装饰器实现),用于请求体校验和返回体序列化,同时注入全局服务依赖,
    // 此方法接收一个业务层面的路由钩子方法 RouteIface.Call
    //
    // 方法首先会查找路由元信息,如果找不到则直接跳过验证环节,由路由器返回404
    // 反之:
    //
    //  1. 申请一个 Context, 并初始化请求体、路由参数等
    //  2. 之后会校验并绑定路由参数(包含路径参数和查询参数)是否正确,如果错误则直接返回422错误,反之会继续序列化并绑定请求体(如果存在)序列化成功之后会校验请求参数的正确性,
    //  3. 校验通过后会调用 RouteIface.Call 并将返回值绑定在 Context 内的 Response 上
    //  4. 校验返回值,并返回422或将返回值写入到实际的 response
    func (f *Wrapper) Handler(ctx MuxContext) error {}
    
  • 建议将跨域访问Recover等方法注册为Mux的中件间;而将日志、认证等业务方法注册为Wraper的依赖,但是其2者并没有十分明显的区别,绝大部分情况都可以互相替换;

  • Wraper.Handler 的实现是一个顺序执行的过程,其作为一个整体,因此是无法在Mux的中间件中对其进行访问和拦截的,为此Wraper暴露出了一些锚点,用于控制Wraper.Handler的执行流:

    • Wrapper.UsePrevious: 添加一个校验前依赖函数,此依赖函数会在:请求参数校验前调用

    • Wrapper.UseAfter: 添加一个校验后依赖函数(也即路由前), 此依赖函数会在:请求参数校验后-路由函数调用前执行

    • Wrapper.UseBeforeWrite: 在数据写入响应流之前执行的钩子方法; 可用于日志记录, 所有请求无论何时终止都会执行此方法

    • Wrapper.UseDependsUseAfter的别名

    • Wrapper.UseUseAfter的别名

      // Use 添加一个依赖函数(锚点), 数据校验后依赖函数
      //
      // 由于 Wrapper 的核心实现类似于装饰器, 而非常规的中间件,因此无法通过 MuxWrapper 的中间件来影响到 Wrapper 的执行过程;
      // 因此 Wrapper 在关键环节均定义了相关的依赖函数,类似于hook,以此来控制执行流程;
      //
      //	与python-FastApi的Depends不同的地方在于:
      //		python-FastApi.Depends接收Request作为入参,并将其返回值作为路由函数Handler的入参;
      //		而此处的hook不返回值,而是通过 Context.Set 和 Context.Get 来进行上下文数据的传递,并通过返回 error 来终止后续的流程;
      //		同时,由于 Context.Set 和 Context.Get 是线程安全的,因此可以放心的在依赖函数中操作 Context;
      //	   	依赖函数的执行始终是顺序进行的,其执行顺序是固定的:
      //	   	始终由 UsePrevious -> (请求参数)Validate -> UseAfter -> (路由函数)RouteHandler -> (响应参数)Validate -> UseBeforeWrite -> exit;
      //
      // 此处的依赖函数有校验前依赖函数和校验后依赖函数,分别通过 Wrapper.UsePrevious 和 Wrapper.UseAfter 注册;
      // 当请求参数校验失败时不会执行 Wrapper.UseAfter 依赖函数, 请求参数会在 Wrapper.UsePrevious 执行完成之后被触发;
      // 如果依赖函数要终止后续的流程,应返回 error, 错误消息会作为消息体返回给客户端, 响应数据格式默认为500+string,可通过 Wrapper.SetRouteErrorFormatter 进行修改;
      func (f *Wrapper) Use(hooks ...DependenceHandle) *Wrapper {
      	return f.UseAfter(hooks...)
      }
      
    • 使用示例:

      func BeforeValidate(c *fastapi.Context) error {
      	c.Set("before-validate", time.Now())
      
      	return nil
      }
      
      func PrintRequestLog(c *fastapi.Context) {
      	fastapi.Info("请求耗时: ", time.Since(c.GetTime("before-validate")))
      	fastapi.Info("响应状态码: ", c.Response().StatusCode)
      }
      
      func returnErrorDeps(c *fastapi.Context) error {
      	return errors.New("deps return error")
      }
      
      app.UsePrevious(BeforeValidate)
      app.Use(returnErrorDeps)
      app.UseBeforeWrite(PrintRequestLog)
      

      显示效果

      显示效果
      ```bash
      2024/04/27 17:47:38 group_router_test.go:522: INFO	请求耗时:  10.372µs
      2024/04/27 17:47:38 group_router_test.go:523: INFO	响应状态码:  400
      2024-04-27 17:47:38    GET	/api/example/error    400
      2024/04/27 17:47:38 group_router_test.go:522: INFO	请求耗时:  11.855µs
      2024/04/27 17:47:38 group_router_test.go:523: INFO	响应状态码:  400
      2024-04-27 17:47:38    GET	/api/example/error    400
      2024/04/27 17:47:38 group_router_test.go:522: INFO	请求耗时:  6.739µs
      2024/04/27 17:47:38 group_router_test.go:523: INFO	响应状态码:  400
      2024-04-27 17:47:38    GET	/api/example/error    400
      ```
      

开发选项

TODO

查看在线文档

# 安装godoc
go install golang.org/x/tools/cmd/godoc@latest
godoc -http=:6060

# 或:pkgsite 推荐
go install golang.org/x/pkgsite/cmd/pkgsite@latest
cd fastapi-go/
pkgsite -http=:6060 -list=false
# 浏览器打开:http://127.0.0.1:6060/github.com/Chendemo12/fastapi

struct内存对齐

go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest

fieldalignment -fix ./... 

打包静态资源文件

# 安装工具
go get -u github.com/go-bindata/go-bindata/...
go install github.com/go-bindata/go-bindata/...

# 下载资源文件
#https://fastapi.tiangolo.com/img/favicon.png
#https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css
#https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js
#https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js

# 打包资源文件到openapi包
go-bindata -o openapi/css.go --pkg openapi internal/static/...