Swagger

Swag将Go的注释转换为Swagger2.0文档。我们为流行的 Go Web Framework 创建了各种插件,这样可以与现有Go项目快速集成(使用Swagger UI)。

官方文档

快速开始 #

  • 将注释添加到API源代码中,请参阅声明性注释格式。
  • 使用如下命令下载swag:
go install github.com/swaggo/swag/cmd/swag@latest

从源码开始构建的话,需要有Go环境(1.19及以上版本)。

或者从github的release页面下载预编译好的二进制文件。

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

确保导入了生成的docs/docs.go文件,这样特定的配置文件才会被初始化。如果通用API注释没有写在main.go中,可以使用-g标识符来告知swag。

swag init -g http/api.go
  • (可选) 使用fmt格式化 SWAG 注释。(请先升级到最新版本)
swag fmt

swag cli #

swag init -h
NAME:
   swag init - Create docs.go

USAGE:
   swag init [command options] [arguments...]

OPTIONS:
   --generalInfo value, -g value          API通用信息所在的go源文件路径,如果是相对路径则基于API解析目录 (默认: "main.go")
   --dir value, -d value                  API解析目录 (默认: "./")
   --exclude value                        解析扫描时排除的目录,多个目录可用逗号分隔(默认:空)
   --propertyStrategy value, -p value     结构体字段命名规则,三种:snakecase,camelcase,pascalcase (默认: "camelcase")
   --output value, -o value               文件(swagger.json, swagger.yaml and doc.go)输出目录 (默认: "./docs")
   --parseVendor                          是否解析vendor目录里的go源文件,默认不
   --parseDependency                      是否解析依赖目录中的go源文件,默认不
   --parseDependencyLevel, --pdl          对'--parseDependency'参数进行增强, 是否解析依赖目录中的go源文件, 0 不解析, 1 只解析对象模型, 2 只解析API, 3 对象模型和API都解析 (default: 0)
   --markdownFiles value, --md value      指定API的描述信息所使用的markdown文件所在的目录
   --generatedTime                        是否输出时间到输出文件docs.go的顶部,默认是
   --codeExampleFiles value, --cef value  解析包含用于 x-codeSamples 扩展的代码示例文件的文件夹,默认禁用
   --parseInternal                        解析 internal 包中的go文件,默认禁用
   --parseDepth value                     依赖解析深度 (默认: 100)
   --instanceName value                   设置文档实例名 (默认: "swagger")
swag fmt -h
NAME:
   swag fmt - format swag comments

USAGE:
   swag fmt [command options] [arguments...]

OPTIONS:
   --dir value, -d value          API解析目录 (默认: "./")
   --exclude value                解析扫描时排除的目录,多个目录可用逗号分隔(默认:空)
   --generalInfo value, -g value  API通用信息所在的go源文件路径,如果是相对路径则基于API解析目录 (默认: "main.go")
   --help, -h                     show help (default: false)

实操(gin) #

  • 使用swag init生成Swagger2.0文档后,导入如下代码包:
import "github.com/swaggo/gin-swagger" // gin-swagger middleware
import "github.com/swaggo/files" // swagger embed files
  • main.go源代码中添加通用的API注释:
// @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

// @externalDocs.description  OpenAPI
// @externalDocs.url          https://swagger.io/resources/open-api/
func main() {
    r := gin.Default()

    c := controller.NewController()

    v1 := r.Group("/api/v1")
    {
        accounts := v1.Group("/accounts")
        {
            accounts.GET(":id", c.ShowAccount)
            accounts.GET("", c.ListAccounts)
            accounts.POST("", c.AddAccount)
            accounts.DELETE(":id", c.DeleteAccount)
            accounts.PATCH(":id", c.UpdateAccount)
            accounts.POST(":id/images", c.UploadAccountImage)
        }
    //...
    }
    r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
    r.Run(":8080")
}
//...

注意:当路由文件不在main函数时,通用注释也要写在main函数上面,否则无法生效。

此外,可以动态设置一些通用的API信息。生成的代码包docs导出SwaggerInfo变量,使用该变量可以通过编码的方式设置标题、描述、版本、主机和基础路径。使用Gin的示例:

package main

import (
    "github.com/gin-gonic/gin"
    "github.com/swaggo/files"
    "github.com/swaggo/gin-swagger"

    "./docs" // docs is generated by Swag CLI, you have to import it.
)

// @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
func main() {

    // programatically set swagger info
    docs.SwaggerInfo.Title = "Swagger Example API"
    docs.SwaggerInfo.Description = "This is a sample server Petstore server."
    docs.SwaggerInfo.Version = "1.0"
    docs.SwaggerInfo.Host = "petstore.swagger.io"
    docs.SwaggerInfo.BasePath = "/v2"
    docs.SwaggerInfo.Schemes = []string{"http", "https"}

    r := gin.New()

    // use ginSwagger middleware to serve the API docs
    r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

    r.Run()
}
  • controller代码中添加API操作注释:
package controller

import (
    "fmt"
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
    "github.com/swaggo/swag/example/celler/httputil"
    "github.com/swaggo/swag/example/celler/model"
)

// 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)
}

// ListAccounts godoc
// @Summary      List accounts
// @Description  get accounts
// @Tags         accounts
// @Accept       json
// @Produce      json
// @Param        q    query     string  false  "name search by q"  Format(email)
// @Success      200  {array}   model.Account
// @Failure      400  {object}  httputil.HTTPError
// @Failure      404  {object}  httputil.HTTPError
// @Failure      500  {object}  httputil.HTTPError
// @Router       /accounts [get]
func (c *Controller) ListAccounts(ctx *gin.Context) {
  q := ctx.Request.URL.Query().Get("q")
  accounts, err := model.AccountsAll(q)
  if err != nil {
    httputil.NewError(ctx, http.StatusNotFound, err)
    return
  }
  ctx.JSON(http.StatusOK, accounts)
}
//...
swag init
  • 运行程序,然后在浏览器中访问 http://localhost:8080/swagger/index.html 。将看到Swagger 2.0 Api文档,如下所示:

    http://127.0.0.1:8998/swagger/index.html   //你定义的那个地址
    

swagger_index.html

踩坑 #

swag init --outputTypes json --parseDependency --parseInternal
参数 作用
--outputTypes json 指定生成 Swagger 文档为 JSON 格式(默认是 YAML)。
--parseDependency 解析并包含代码中的外部依赖项,确保依赖类型在文档中被正确识别。
--parseInternal 解析项目内部未导出的私有方法和类型,适用于需要生成更详细文档的场景。
swag init --parseDependency --parseInternal    //否则对于外部引人结构体参数无效
  • 读取文件
// VideoContent godoc
//
//@Description	获取视频内容(16进制)
//@Tags			video
//@Produce		json
//@Param			cid	query	int64												true	"cid"
//@Param			eid	query	int64												true	"eid"
//@Param			path	query	string											true	"视频路径"
//@Param			Range	header	string											true	"偏移"
//@Success      206   {file}   []bute   "分片视频内容"
//@Failure		500			{object}	response.Response
//@Router			/video/content [get]
func (v *VideoApi) VideoContent(c *gin.Context) {
	cidStr := c.Query("cid")
	eidStr := c.Query("eid")
	videoPath := c.Query("path")
	rangeStr := c.GetHeader("Range")
	if cidStr == "" || eidStr == "" || videoPath == "" || rangeStr == "" {
		response.ErrorParam(c, nil)
		return
	}
	_, err := strconv.Atoi(cidStr)
	if err != nil {
		response.ErrorParam(c, err)
		return
	}
	_, err = strconv.Atoi(eidStr)
	if err != nil {
		response.ErrorParam(c, err)
		return
	}
	shortRange, found := strings.CutPrefix(rangeStr, "bytes=")
	if !found {
		response.ErrorParam(c, rangeStr)
		return
	}
	rangeSlice := strings.Split(shortRange, "-")
	if len(rangeSlice) != 2 {
		response.ErrorParam(c, rangeStr)
		return
	}
	startIndex, serr := strconv.ParseInt(rangeSlice[0], 10, 64)
	endIndex, eerr := strconv.ParseInt(rangeSlice[1], 10, 64)
	if serr != nil || eerr != nil {
		response.ErrorParam(c, rangeStr)
		return
	}
	content, fileSize, err := service.GetVideoContent(startIndex, endIndex, videoPath)
	if err != nil {
		if !strings.Contains(err.Error(), "invalid range") {
			response.Fail(c, global.FileContentReadFail, err)
			return
		}
	}
	bytesCnt := int64(len(content))
	extraHeader := map[string]string{"Accept-Ranges": "bytes", "Content-Range": fmt.Sprintf("bytes %d-%d/%d", startIndex, bytesCnt+startIndex-1, fileSize)}
	c.DataFromReader(http.StatusPartialContent, bytesCnt, "application/octet-stream", bytes.NewReader(content), extraHeader)
}
// ObjectDetection godoc
//
//	@Description	物体识别
//	@Tags			detection
//	@Accept			json
//	@Produce		json
//	@Param			body	body		param.ObjectDetectionParam	true	"创建记录的参数"
//	@Success		200		{object}	response.Response
//	@Failure		500		{object}	response.Response
//	@Router			/object/detection [post]
func (d *DetectionApi) ObjectDetection(c *gin.Context, p *param.ObjectDetectionParam) {
	type detectionStatus struct {
		Status       int
		DetectionTid int32
	}
	responseData := new(detectionStatus)

	if p.DetectionTid == 0 && !p.Cancel {
		responseData.DetectionTid = service.GetFakeTaskId()
		p.DetectionTid = responseData.DetectionTid
	}

	status, err := service.AiEngine.GetAiStartStatus(service.CModelTypeObjectDetection)
	if err != nil {
		response.Fail(c, global.ErrorUnknownMsg, err.Error())
		return
	}

	err = service.AiEngine.ObjectDetection(p)
	if err != nil {
		response.Fail(c, global.ErrorUnknownMsg, err.Error())
		return
	}
	responseData.Status = status
	response.Success(c, global.CurdStatusOkMsg, responseData)
}
// GetObjectImage godoc
//
//	@Description	获取识别图
//	@Tags			detection
//	@Produce		json
//	@Param			cid			query		int64	true	"cid"
//	@Param			eid			query		int64	true	"eid"
//	@Param			keyid		query		int64	false	"时间区间记录主键id"
//	@Param			skwing		query		string	false	"时间偏移"
//	@Param			videopath	query		string	true	"视频路径"
//	@Success		200			{object}	response.Response{data=string}
//	@Failure		500			{object}	response.Response
//	@Router			/object/image [post]
func (d *DetectionApi) GetObjectImage(c *gin.Context, cid, eid, keyid int64, datatype, videoPath string, currentTime int64) {

	objectImagePath, err := service.Detection.GetObjectImage(cid, eid, keyid, datatype, videoPath, currentTime)
	if err != nil {
		common.Log.Errorf("get objectImage err:%v", err)
		response.Fail(c, global.ExitResordFailMsg, err.Error())
		return
	}
	response.Success(c, global.CurdStatusOkMsg, objectImagePath)
}
// GetEvidencePartition godoc
//
//	@Description	获取检材分区信息
//	@Tags			evidence
//	@Accept			json
//	@Produce		json
//	@Param			Cid				query		string	true	"记录id"
//	@Param			Eid				query		string	true	"检材id"
//	@Param			EvidencePath	query		string	true	"检材路径"
//	@Success		200				{object}	response.Response{data=[]vmodel.Partition}
//	@Failure		400				{object}	response.Response	"未知错误"
//	@Failure		500				{object}	response.Response
//	@Router			/evidence/partition [get]
  • router文件
package routers

import (
	......
	"github.com/gin-gonic/contrib/sessions"
	"github.com/gin-gonic/gin"
	swaggerFiles "github.com/swaggo/files"
	ginSwagger "github.com/swaggo/gin-swagger"
	"go.uber.org/zap"
)

func StartGin() *gin.Engine {
	var router *gin.Engine
	if !global.IsDebugMode() {
		router = ReleaseRouter()
	} else {
		router = gin.Default()
		//pprof包选择性关闭
		//pprof.Register(router)
		router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
	}

	////是否允许跨域
	if global.AllowCrossDomain {
		router.Use(cors.Next())
	}

	store := sessions.NewCookieStore([]byte("gesecret"))
	router.Use(sessions.Sessions("sid", store))
	{
		router.GET("/", func(c *gin.Context) { response.Success(c, vmodel.TaskStatusSuccess, "") })
	}

	fastdev.SetupRouters(router)

	checkLicenseAPI := router.Group("/")
	checkLicenseAPI.Use(CheckLicenes)
	{
		checkLicenseAPI.POST("record/create", validator.Create(global.WebPrefix+"CreateRecord")) //*
    ......
		}
  ......
}