第 11 章 · OpenAPI 与 Swagger 文档
本章目标:使用 swaggo/swag 从注释生成 OpenAPI 3 文档;为 api-go-demo 商品与认证 API 添加完整注解;暴露 /swagger/index.html 交互调试;DTO 标注 slug、is_published、price 分;对照 fastapi-web 自动 /docs 与 ch16 答辩材料。
学时建议:3~4 小时(含 1.5 小时跟练)
前置:完成 gin-web ch10;Handler 与 DTO 已实现。
11.1 场景说明:可交付的 API 文档
api-go-demo 答辩与 ch16 毕业项目 需在线文档。FastAPI 自带 Swagger UI;Gin 需 swag 从 Go 注释生成 OpenAPI。
| 能力 | FastAPI svc-demo | Gin api-go-demo |
|---|---|---|
| 交互文档 | /docs | /swagger/index.html |
| Schema | Pydantic 自动 | 注释 + struct tag |
| 导出 | /openapi.json | docs/swagger.json |
| Bearer | OAuth2 方案 | @securityDefinitions.apikey |
域名 api.example.com;文档 description 写明:价格单位为分(int64)。
11.2 安装与工具链
go install github.com/swaggo/swag/cmd/swag@latest
go get github.com/swaggo/gin-swagger@v1.6.0
go get github.com/swaggo/files@v1.0.1
验证:
swag -v
# Swag Version: v1.16.x
Makefile 目标(推荐):
.PHONY: swagger
swagger:
swag init -g cmd/server/main.go -o docs --parseDependency --parseInternal
CI 可选:提交前 make swagger 并检查 git diff docs/ 为空。
11.3 General API 注释(main.go)
// cmd/server/main.go
// @title api-go-demo API
// @version 1.0
// @description 虚构商城 API。商品标识为 slug;上架字段 is_published;价格单位为分(int64,6800=68.00元)。
// @termsOfService https://example.com/terms
// @contact.name API Support
// @contact.url https://example.com/support
// @contact.email support@example.com
// @license.name MIT
// @license.url https://opensource.org/licenses/MIT
// @host 127.0.0.1:8080
// @BasePath /api/v1
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description 输入: Bearer {access_token}
package main
import (
"example.com/api-go-demo/docs"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
// ...
)
func main() {
// docs 包由 swag init 生成,需 blank import 触发 init
_ = docs.SwaggerInfo
// ...
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
生产环境可关闭 Swagger 路由:
if cfg.Env != "prod" {
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
11.4 DTO 与响应模型
swag 解析导出结构体与 json / example tag:
// internal/dto/product.go
package dto
type ProductResp struct {
Slug string `json:"slug" example:"go-handbook"`
Name string `json:"name" example:"Go 语言手册"`
Price int64 `json:"price" example:"6800"` // 分
Stock int `json:"stock" example:"50"`
IsPublished bool `json:"is_published" example:"true"`
Description string `json:"description,omitempty" example:"入门到进阶"`
}
type CreateProductReq struct {
Slug string `json:"slug" binding:"required,min=2,max=64" example:"go-handbook"`
Name string `json:"name" binding:"required" example:"Go 语言手册"`
Price int64 `json:"price" binding:"required,min=1" example:"6800"`
Stock int `json:"stock" binding:"min=0" example:"50"`
IsPublished bool `json:"is_published" example:"false"`
Description string `json:"description" example:""`
}
type UpdateProductReq struct {
Name *string `json:"name" example:"Go 手册第二版"`
Price *int64 `json:"price" example:"7200"`
Stock *int `json:"stock" example:"100"`
IsPublished *bool `json:"is_published" example:"true"`
Description *string `json:"description"`
}
type PageData struct {
Items []ProductResp `json:"items"`
Total int64 `json:"total" example:"42"`
Page int `json:"page" example:"1"`
Size int `json:"size" example:"20"`
}
Envelope(ch07):
// internal/response/envelope.go
type Envelope struct {
Code string `json:"code" example:"OK"`
Message string `json:"message" example:"success"`
Data interface{} `json:"data"`
}
11.5 Handler 注解完整示例
商品
// ListProducts 商品列表(仅已发布)
// @Summary 分页列表已发布商品
// @Description 仅返回 is_published=true 的商品;price 单位为分
// @Tags products
// @Produce json
// @Param page query int false "页码" default(1) minimum(1)
// @Param page_size query int false "每页条数" default(20) minimum(1) maximum(100)
// @Success 200 {object} response.Envelope{data=dto.PageData}
// @Failure 500 {object} response.Envelope
// @Router /products [get]
func (h *ProductHandler) ListProducts(c *gin.Context) { /* ... */ }
// GetProduct 按 slug 获取详情
// @Summary 商品详情
// @Description 按 slug 查询;未发布或不存在返回 404
// @Tags products
// @Produce json
// @Param slug path string true "商品 slug" example(go-handbook)
// @Success 200 {object} response.Envelope{data=dto.ProductResp}
// @Failure 404 {object} response.Envelope
// @Router /products/{slug} [get]
func (h *ProductHandler) GetProduct(c *gin.Context) { /* ... */ }
// CreateProduct 创建商品
// @Summary 创建商品(admin)
// @Description 需要 admin 角色;slug 唯一;price 为分
// @Tags products
// @Accept json
// @Produce json
// @Param body body dto.CreateProductReq true "创建参数"
// @Security BearerAuth
// @Success 201 {object} response.Envelope{data=dto.ProductResp}
// @Failure 400 {object} response.Envelope
// @Failure 401 {object} response.Envelope
// @Failure 403 {object} response.Envelope
// @Failure 409 {object} response.Envelope
// @Router /products [post]
func (h *ProductHandler) CreateProduct(c *gin.Context) { /* ... */ }
// UpdateProduct 部分更新
// @Summary 更新商品(admin)
// @Tags products
// @Accept json
// @Produce json
// @Param slug path string true "slug"
// @Param body body dto.UpdateProductReq true "更新字段"
// @Security BearerAuth
// @Success 200 {object} response.Envelope{data=dto.ProductResp}
// @Failure 404 {object} response.Envelope
// @Router /products/{slug} [patch]
func (h *ProductHandler) UpdateProduct(c *gin.Context) { /* ... */ }