Contents

Go语言之使用 swaggo 一键生成 API 文档

本文主要记录了如何在 Go 中使用 swaggo 根据注释自动生成 API 文档,以及如何使用条件编译来降低二进制文件大小。

之前也用过其他的API文档工具,但是最大的问题还是文档和代码是分离的。总是出现文档和代码不同步的情况。

于是最终采用的 swaggo,根据注释自动生成 API 文档,注释即文档。

1. 概述

关于Swaggo

或许你使用过 Swagger, 而 swaggo 就是代替了你手动编写 yaml 的部分。只要通过一个命令

就可以将注释转换成文档,这让我们可以更加专注于代码。

接入流程

接入流程主要分为以下几个步骤:

  • 0)main 文件中添加注释-配置Server,服务信息
  • 1)controller 中添加注释-配置接口,接口信息
  • 2)swag init 生成 docs 目录
  • 3)配置 handler 访问
  • 4)访问测试

2. 演示

以 gin 框架为例,演示一下具体操作流程。

完整 Demo 见 Github

配置服务信息

main.go 中引入以下两个包:

import "github.com/swaggo/gin-swagger" // gin-swagger middleware
import "github.com/swaggo/files" // swagger embed files

并在 main 方法上添加以下注释描述 server:

具体可以添加哪些注释参考 通用API信息

// 添加注释以描述 server 信息
// @title           Swagger Example API
// @version         1.0
// @description     This is a sample server celler server.
// @termsOfService  http://swagger.io/terms/

// @contact.name   API Support
// @contact.url    http://www.swagger.io/support
// @contact.email  support@swagger.io

// @license.name  Apache 2.0
// @license.url   http://www.apache.org/licenses/LICENSE-2.0.html

// @host      localhost:8080
// @BasePath  /api/v1

// @securityDefinitions.basic  BasicAuth
func main() {
    r := gin.Default()
    c := controller.NewController()
    v1 := r.Group("/api/v1")
    {
        accounts := v1.Group("/accounts")
        {
            accounts.GET(":id", c.ShowAccount)
        }
    //...
    }
    r.Run(":8080")
}
//...

配置接口文档

在每个接口的 controller 上添加注释以描述接口,就像下面这样:

具体可以添加哪些注释参考 API操作

// ShowAccount godoc
// @Summary      Show an account
// @Description  get string by ID
// @Tags         accounts
// @Accept       json
// @Produce      json
// @Param        id   path      int  true  "Account ID"
// @Success      200  {object}  model.Account
// @Failure      400  {object}  httputil.HTTPError
// @Failure      404  {object}  httputil.HTTPError
// @Failure      500  {object}  httputil.HTTPError
// @Router       /accounts/{id} [get]
func (c *Controller) ShowAccount(ctx *gin.Context) {
  id := ctx.Param("id")
  aid, err := strconv.Atoi(id)
  if err != nil {
    httputil.NewError(ctx, http.StatusBadRequest, err)
    return
  }
  account, err := model.AccountOne(aid)
  if err != nil {
    httputil.NewError(ctx, http.StatusNotFound, err)
    return
  }
  ctx.JSON(http.StatusOK, account)
}

swag init

需要先安装 swag,使用如下命令下载swag:

$ go get -u github.com/swaggo/swag/cmd/swag

# 1.16 及以上版本
$ go install github.com/swaggo/swag/cmd/swag@latest

在包含main.go文件的项目根目录运行swag init。这将会解析注释并生成需要的文件(docs文件夹和docs/docs.go)。

swag init

注:swag init 默认会找当前目录下的 main.go 文件,如果不叫 main.go 也可以手动指定文件位置。

# -o 指定输出目录。
swag init -g cmd/api/api.go -o cmd/api/docs

需要注意的是:swag init 的时候需要在项目根目录下执行,否则无法检测到所有文件中的注释。

比如在 /xxx 目录下执行 swag init 就只能检测到 xxx 目录下的,如果还有和 xxx 目录同级或者更上层的目录中的代码都检测不到。

init 之后会生成一个 docs 文件夹,这里面就是接口描述文件,生成后还需要将其导入到 main.go 中。

在 main.go 中导入刚才生成的 docs 包

在 main.go 中导入刚才生成的 docs 包

在 main.go 中导入刚才生成的 docs 包

重要的事情说三遍

加上之前导入的 gin-swag 相关的两个包,一共新导入了三个包。

_ "xx/cmd/api/docs" // main 文件中导入 docs 包

ginSwagger "github.com/swaggo/gin-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"

配置文档handler

最后则是在 router 中增加 swagger 的 handler 了。

在 main.go 或其他地方增加一个 handler.

engine := gin.New()
engine.GET("swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

访问测试

项目运行起来后访问ip:port/swagger/index.html 即可看到 API 文档。

如果注释有更新,需要重新生成 docs 并重启服务才会生效。

3. 注释语法

具体语法见 官方文档,这里主要列出几个特殊的点。

Controller 注释支持很多字段,这里主要记录常用的。

  • tags:给 API 按照 tag 分组,便于管理。

  • accept、produce:API 接收和响应的 MMIE 类型。

  • param:接口请求参数,重要

    • Syntax:param name,param type,data type,is mandatory?,comment attribute(optional)
  • response、success、failure:API 响应内容,如果成功失败返回值不一样也可以通过 success、failure 分别描述。

    • Syntax:return code,{param type},data type,comment
  • header:响应头

    • Syntax: return code,{param type},data type,comment
  • router:接口路由

    • Syntax:path,[httpMethod]

主要详细记录一下 param 和 response 该怎么写。

param

语法:param name,param type,data type,is mandatory?,comment attribute(optional)

参数类型

  • query
  • path
  • header
  • body
  • formData

数据类型

  • string (string)
  • integer (int, uint, uint32, uint64)
  • number (float32)
  • boolean (bool)
  • user defined struct

示例

// @Param        Authorization  header  string                     true  "JWT"
// @Param        req            body    listModel                  true  "相关信息"
// @Param        amount         query    string  				    true  "订单金额(元)"
type listModel struct {
	AnswerId string `form:"answerId"`
	Page     int    `form:"page"`
}

以上示例就用到了 3 种参数:

  • header 中的 Authorization
  • body 中的 listModel
  • queryString 中的 amount

由于是可以用结构体的,所以一般都建议使用结构体,这样比较简洁。

resp

语法:return code,{param type},data type,comment

返回值类型和数据类型和 param 都是一致的。

示例:

// @Success      200  {object}  respmodel.WxPayMp
// @Failure      400  {object}  srv.Result

由于成功失败返回结构不同,所以分别处理。如果相同就直接用 response 即可。

不过大部分项目中应该会定义一个通用返回结构,比如这样的:

type Result struct {
	Code int         `json:"code"`
	Data interface{} `json:"data"`
	Msg  string      `json:"msg"`
}

不同的接口,只需要替换里面的 Data 字段即可。

对于这种情况,如果每次都指定返回值是 Result 结构,就无法看到具体的响应了。但是肯定也不可能为每个接口重新生成一个对应的返回结构。

好在 swaggo 提供了 响应对象中的模型组合,可以自行组合结构体,以处理这种通用返回结果的情况。

对于固定返回结构来说这个就很方便,可以为每个接口指定对应的 data 字段内容。

具体如下:

// JSONResult的data字段类型将被proto.Order类型替换
// @success 200 {object} jsonresult.JSONResult{data=proto.Order} "desc"
type JSONResult struct {
Code    int          `json:"code" `
Message string       `json:"message"`
Data    interface{}  `json:"data"`
}

type Order struct { //in `proto` package
...
}

还支持对象数组和原始类型作为嵌套响应

// @success 200 {object} jsonresult.JSONResult{data=[]proto.Order} "desc"
// @success 200 {object} jsonresult.JSONResult{data=string} "desc"
// @success 200 {object} jsonresult.JSONResult{data=[]string} "desc"

替换多个字段的类型。如果某字段不存在,将添加该字段。

// @success 200 {object} jsonresult.JSONResult{data1=string,data2=[]string,data3=proto.Order,data4=[]proto.Order} "desc"

对于上面的情况就可以这样处理了:

// @Success      200  {object}  srv.Result{data=respmodel.WxPayMp}
// @Failure      400  {object}  srv.Result

对于不同的接口,只需要替换其中的 data 字段即可。

4. FAQ

无法解析外部依赖

就是在项目引入了另外一个独立项目的数据结构体,在接口上配置

// @Success 200 {object} ginfw.BaseHttpResponse

发现 swag init 无法正常生成想要的 swagger yaml 文件。 swag init 命令执行错误 cannot find type definition

解决方案

增加--parseDependency --parseInternal 两个参数同时在main.go 中导入我们依赖的包。

swag init –parseInternal depends on –parseDependency

注:因为依赖的是外部包的内部结构,导致无法扫描到,同理,直接引入 go 内置的包也无法直接解析,也是需要这样处理一下。

无法引入内部依赖

有时候一个项目里依赖也无法解析:

// @Success      200  {object}  srv.Result{data=respmodel.AnswerItem}

swag init 时报错:

cannot find type definition: respmodel.AnswerItem

尝试了--parseInternal depends on --parseDependency也没有效果,最后发现:

这个是因为同一个包里面定义了几个重名的结构体导致的,改名即可解决问题。

比如这里是在 user 的 respmodel 中定义了 AnswerItem,然后在 admin 的 respmodel 中也定义了 AnswerItem,二者结构不同只是名字相同。

因为几个项目是在一个仓库里,导致初始化时不知道用哪个了。

构建速度

在添加了--parseDependency 后生成速度明显减低,此时可以添加--parseDepth 参数指定数据结构深度来加快生成速度。

相关参数含义如下:

参数名 含义解释
parseInternal 解析内部依赖包,默认值: false
parseDependency 解析外部依赖包,默认值: false
parseDepth 解析依赖包深度,默认值:100

注:parseDepth 这个参数非常有用,如果你知道需要解析的数据结构深度,建议使用这个参数,swag 命令执行时间会大大缩短。

5. 优化

swag fmt

swaggo 提供了 swag fmt 工具,可以针对Swag的注释自动格式化,就像go fmt,让注释看起来更统一。

示例:

swag fmt

排除目录(不扫描)示例:

swag fmt -d ./ --exclude ./internal

指定 main.go 文件示例:

swag fmt -g cmd/api/api.go

条件编译

swaggo 是直接 build 到二进制里的,会极大增加二进制文件的大小,一般在生产环境不需要将 docs 编译进去。

可以利用 go 提供的条件编译来实现是否编译文档。

main.go声明swagHandler,并在该参数不为空时才加入路由:

package main

//...

var swagHandler gin.HandlerFunc

func main(){
    // ...
    
    	if swagHandler != nil {
			r.GET("/swagger/*any", swagHandler)
        }
    
    //...
}

同时,我们将该参数在另外加了build tag的包中初始化。

条件编译只需要在对应文件首行加入 go:build xxx 即可,这样只有编译时指定 xxx tag 才会把该文件编译进去。

//go:build doc

package main

import (
    ginSwagger "github.com/swaggo/gin-swagger"
    "github.com/swaggo/gin-swagger/swaggerFiles"
    _ "i-go/gin/swagger/docs"
)

func init() {
    swagHandler = ginSwagger.WrapHandler(swaggerFiles.Handler)
}

之后我们就可以使用go build -tags "doc"来打包带文档的包,直接go build来打包不带文档的包。

注:go 1.16 之前旧版条件编译语法为 //+build 1.16之后增加了新版语法 go:build

完整 Demo 见 Github