gitbook/Go 语言项目开发实战/docs/391142.md
2022-09-03 22:05:03 +08:00

476 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 17 | API 文档:如何生成 Swagger API 文档
你好,我是孔令飞。
作为一名开发者我们通常讨厌编写文档因为这是一件重复和缺乏乐趣的事情。但是在开发过程中又有一些文档是我们必须要编写的比如API文档。
一个企业级的Go后端项目通常也会有个配套的前端。为了加快研发进度通常是后端和前端并行开发这就需要后端开发者在开发后端代码之前先设计好API接口提供给前端。所以在设计阶段我们就需要生成API接口文档。
一个好的API文档可以减少用户上手的复杂度也意味着更容易留住用户。好的API文档也可以减少沟通成本帮助开发者更好地理解API的调用方式从而节省时间提高开发效率。这时候我们一定希望有一个工具能够帮我们自动生成API文档解放我们的双手。Swagger就是这么一个工具可以帮助我们**生成易于共享且具有足够描述性的API文档**。
接下来我们就来看下如何使用Swagger生成API文档。
## Swagger介绍
Swagger是一套围绕OpenAPI规范构建的开源工具可以设计、构建、编写和使用REST API。Swagger包含很多工具其中主要的Swagger工具包括
* **Swagger编辑器**基于浏览器的编辑器可以在其中编写OpenAPI规范并实时预览API文档。[https://editor.swagger.io](https://editor.swagger.io/) 就是一个Swagger编辑器你可以尝试在其中编辑和预览API文档。
* **Swagger UI**将OpenAPI 规范呈现为交互式API文档并可以在浏览器中尝试API调用。
* **Swagger Codegen**根据OpenAPI规范生成服务器存根和客户端代码库目前已涵盖了40多种语言。
### Swagger和OpenAPI的区别
我们在谈到Swagger时也经常会谈到OpenAPI。那么二者有什么区别呢
OpenAPI是一个API规范它的前身叫Swagger规范通过定义一种用来描述API格式或API定义的语言来规范RESTful服务开发过程目前最新的OpenAPI规范是[OpenAPI 3.0](https://swagger.io/docs/specification)也就是Swagger 2.0规范)。
OpenAPI规范规定了一个API必须包含的基本信息这些信息包括
* 对API的描述介绍API可以实现的功能。
* 每个API上可用的路径/users和操作GET /usersPOST /users
* 每个API的输入/返回的参数。
* 验证方法。
* 联系信息、许可证、使用条款和其他信息。
所以你可以简单地这么理解OpenAPI是一个API规范Swagger则是实现规范的工具。
另外要编写Swagger文档首先要会使用Swagger文档编写语法因为语法比较多这里就不多介绍了你可以参考Swagger官方提供的[OpenAPI Specification](https://swagger.io/specification/)来学习。
## 用go-swagger来生成Swagger API文档
在Go项目开发中我们可以通过下面两种方法来生成Swagger API文档
第一如果你熟悉Swagger语法的话可以直接编写JSON/YAML格式的Swagger文档。建议选择YAML格式因为它比JSON格式更简洁直观。
第二通过工具生成Swagger文档目前可以通过[swag](https://github.com/swaggo/swag)和[go-swagger](https://github.com/go-swagger/go-swagger)两个工具来生成。
对比这两种方法直接编写Swagger文档不比编写Markdown格式的API文档工作量小我觉得不符合程序员“偷懒”的习惯。所以本专栏我们就使用go-swagger工具基于代码注释来自动生成Swagger文档。为什么选go-swagger呢有这么几个原因
* go-swagger比swag功能更强大go-swagger提供了更灵活、更多的功能来描述我们的API。
* 使我们的代码更易读如果使用swag我们每一个API都需要有一个冗长的注释有时候代码注释比代码还要长但是通过go-swagger我们可以将代码和注释分开编写一方面可以使我们的代码保持简洁清晰易读另一方面我们可以在另外一个包中统一管理这些Swagger API文档定义。
* 更好的社区支持go-swagger目前有非常多的Github star数出现Bug的概率很小并且处在一个频繁更新的活跃状态。
你已经知道了go-swagger是一个功能强大的、高性能的、可以根据代码注释生成Swagger API文档的工具。除此之外go-swagger还有很多其他特性
* 根据Swagger定义文件生成服务端代码。
* 根据Swagger定义文件生成客户端代码。
* 校验Swagger定义文件是否正确。
* 启动一个HTTP服务器使我们可以通过浏览器访问API文档。
* 根据Swagger文档定义的参数生成Go model结构体定义。
可以看到使用go-swagger生成Swagger文档可以帮助我们减少编写文档的时间提高开发效率并能保证文档的及时性和准确性。
这里需要注意如果我们要对外提供API的Go SDK可以考虑使用go-swagger来生成客户端代码。但是我觉得go-swagger生成的服务端代码不够优雅所以建议你自行编写服务端代码。
目前有很多知名公司和组织的项目都使用了go-swagger例如 Moby、CoreOS、Kubernetes、Cilium等。
### 安装Swagger工具
go-swagger通过swagger命令行工具来完成其功能swagger安装方法如下
```
$ go get -u github.com/go-swagger/go-swagger/cmd/swagger
$ swagger version
dev
```
### swagger命令行工具介绍
swagger命令格式为`swagger [OPTIONS] <command>`。可以通过`swagger -h`查看swagger使用帮助。swagger提供的子命令及功能见下表
![](https://static001.geekbang.org/resource/image/yy/78/yy3428aa968c7029cb4f6b11f2596678.png?wh=1176x1180)
## 如何使用swagger命令生成Swagger文档
go-swagger通过解析源码中的注释来生成Swagger文档go-swagger的详细注释语法可参考[官方文档](https://goswagger.io)。常用的有如下几类注释语法:
![](https://static001.geekbang.org/resource/image/94/b3/947262c5175f6f518ff677063af293b3.png?wh=1087x1134)
### 解析注释生成Swagger文档
swagger generate命令会找到main函数然后遍历所有源码文件解析源码中与Swagger相关的注释然后自动生成swagger.json/swagger.yaml文件。
这一过程的示例代码为[gopractise-demo/swagger](https://github.com/marmotedu/gopractise-demo/tree/main/swagger)。目录下有一个main.go文件定义了如下API接口
```
package main
import (
"fmt"
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/marmotedu/gopractise-demo/swagger/api"
// This line is necessary for go-swagger to find your docs!
_ "github.com/marmotedu/gopractise-demo/swagger/docs"
)
var users []*api.User
func main() {
r := gin.Default()
r.POST("/users", Create)
r.GET("/users/:name", Get)
log.Fatal(r.Run(":5555"))
}
// Create create a user in memory.
func Create(c *gin.Context) {
var user api.User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error(), "code": 10001})
return
}
for _, u := range users {
if u.Name == user.Name {
c.JSON(http.StatusBadRequest, gin.H{"message": fmt.Sprintf("user %s already exist", user.Name), "code": 10001})
return
}
}
users = append(users, &user)
c.JSON(http.StatusOK, user)
}
// Get return the detail information for a user.
func Get(c *gin.Context) {
username := c.Param("name")
for _, u := range users {
if u.Name == username {
c.JSON(http.StatusOK, u)
return
}
}
c.JSON(http.StatusBadRequest, gin.H{"message": fmt.Sprintf("user %s not exist", username), "code": 10002})
}
```
main包中引入的**User struct**位于gopractise-demo/swagger/api目录下的[user.go](https://github.com/marmotedu/gopractise-demo/blob/main/swagger/api/user.go)文件:
```
// Package api defines the user model.
package api
// User represents body of User request and response.
type User struct {
// User's name.
// Required: true
Name string `json:"name"`
// User's nickname.
// Required: true
Nickname string `json:"nickname"`
// User's address.
Address string `json:"address"`
// User's email.
Email string `json:"email"`
}
```
`// Required: true`说明字段是必须的生成Swagger文档时也会在文档中声明该字段是必须字段。
为了使代码保持简洁我们在另外一个Go包中编写带go-swagger注释的API文档。假设该Go包名字为docs在开始编写Go API注释之前需要在main.go文件中导入docs包
```
_ "github.com/marmotedu/gopractise-demo/swagger/docs"
```
通过导入docs包可以使go-swagger在递归解析main包的依赖包时找到docs包并解析包中的注释。
在gopractise-demo/swagger目录下创建docs文件夹
```
$ mkdir docs
$ cd docs
```
在docs目录下创建一个doc.go文件在该文件中提供API接口的基本信息
```
// Package docs awesome.
//
// Documentation of our awesome API.
//
// Schemes: http, https
// BasePath: /
// Version: 0.1.0
// Host: some-url.com
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Security:
// - basic
//
// SecurityDefinitions:
// basic:
// type: basic
//
// swagger:meta
package docs
```
**Package docs**后面的字符串 `awesome` 代表我们的HTTP服务名。`Documentation of our awesome API`是我们API的描述。其他都是go-swagger可识别的注释代表一定的意义。最后以`swagger:meta`注释结束。
编写完doc.go文件后进入gopractise-demo/swagger目录执行如下命令生成Swagger API文档并启动HTTP服务在浏览器查看Swagger
```
$ swagger generate spec -o swagger.yaml
$ swagger serve --no-open -F=swagger --port 36666 swagger.yaml
2020/10/20 23:16:47 serving docs at http://localhost:36666/docs
```
* \-o指定要输出的文件名。swagger会根据文件名后缀.yaml或者.json决定生成的文件格式为YAML或JSON。
* no-open因为是在Linux服务器下执行命令没有安装浏览器所以使no-open禁止调用浏览器打开URL。
* \-F指定文档的风格可选swagger和redoc。我选用了redoc因为觉得redoc格式更加易读和清晰。
* port指定启动的HTTP服务监听端口。
打开浏览器,访问[http://localhost:36666/docs](http://url) ,如下图所示:
![](https://static001.geekbang.org/resource/image/9a/2c/9a9fb7a31d418d8e4dc13b19cefa832c.png?wh=2524x699)
如果我们想要JSON格式的Swagger文档可执行如下命令将生成的swagger.yaml转换为swagger.json
```
$ swagger generate spec -i ./swagger.yaml -o ./swagger.json
```
接下来我们就可以编写API接口的定义文件位于[gopractise-demo/swagger/docs/user.go](https://github.com/marmotedu/gopractise-demo/blob/main/swagger/docs/user.go)文件中):
```
package docs
import (
"github.com/marmotedu/gopractise-demo/swagger/api"
)
// swagger:route POST /users user createUserRequest
// Create a user in memory.
// responses:
// 200: createUserResponse
// default: errResponse
// swagger:route GET /users/{name} user getUserRequest
// Get a user from memory.
// responses:
// 200: getUserResponse
// default: errResponse
// swagger:parameters createUserRequest
type userParamsWrapper struct {
// This text will appear as description of your request body.
// in:body
Body api.User
}
// This text will appear as description of your request url path.
// swagger:parameters getUserRequest
type getUserParamsWrapper struct {
// in:path
Name string `json:"name"`
}
// This text will appear as description of your response body.
// swagger:response createUserResponse
type createUserResponseWrapper struct {
// in:body
Body api.User
}
// This text will appear as description of your response body.
// swagger:response getUserResponse
type getUserResponseWrapper struct {
// in:body
Body api.User
}
// This text will appear as description of your error response body.
// swagger:response errResponse
type errResponseWrapper struct {
// Error code.
Code int `json:"code"`
// Error message.
Message string `json:"message"`
}
```
user.go文件说明
* swagger:route`swagger:route`代表API接口描述的开始后面的字符串格式为`HTTP方法 URL Tag ID`。可以填写多个tag相同tag的API接口在Swagger文档中会被分为一组。ID是一个标识符`swagger:parameters`是具有相同ID的`swagger:route`的请求参数。`swagger:route`下面的一行是该API接口的描述需要以英文点号为结尾。`responses:`定义了API接口的返回参数例如当HTTP状态码是200时返回createUserResponsecreateUserResponse会跟`swagger:response`进行匹配,匹配成功的`swagger:response`就是该API接口返回200状态码时的返回。
* swagger:response`swagger:response`定义了API接口的返回例如getUserResponseWrapper关于名字我们可以根据需要自由命名并不会带来任何不同。getUserResponseWrapper中有一个Body字段其注释为`// in:body`说明该参数是在HTTP Body中返回。`swagger:response`之上的注释会被解析为返回参数的描述。api.User自动被go-swagger解析为`Example Value`和`Model`。我们不用再去编写重复的返回字段只需要引用已有的Go结构体即可这也是通过工具生成Swagger文档的魅力所在。
* swagger:parameters`swagger:parameters`定义了API接口的请求参数例如userParamsWrapper。userParamsWrapper之上的注释会被解析为请求参数的描述`// in:body`代表该参数是位于HTTP Body中。同样userParamsWrapper结构体名我们也可以随意命名不会带来任何不同。`swagger:parameters`之后的createUserRequest会跟`swagger:route`的ID进行匹配匹配成功则说明是该ID所在API接口的请求参数。
进入gopractise-demo/swagger目录执行如下命令生成Swagger API文档并启动HTTP服务在浏览器查看Swagger
```
$ swagger generate spec -o swagger.yaml
$ swagger serve --no-open -F=swagger --port 36666 swagger.yaml
2020/10/20 23:28:30 serving docs at http://localhost:36666/docs
```
打开浏览器,访问 [http://localhost:36666/docs](http://localhost:36666/docs) ,如下图所示:
![](https://static001.geekbang.org/resource/image/e6/e0/e6d6d138fb890ef219d71671d146d5e0.png?wh=2450x1213)
上面我们生成了swagger风格的UI界面我们也可以使用redoc风格的UI界面如下图所示
![](https://static001.geekbang.org/resource/image/dd/48/dd568a44290283861ba5c37f28307d48.png?wh=2527x1426)
### go-swagger其他常用功能介绍
上面我介绍了swagger最常用的generate、serve命令关于swagger其他有用的命令这里也简单介绍一下。
1. 对比Swagger文档
```
$ swagger diff -d change.log swagger.new.yaml swagger.old.yaml
$ cat change.log
BREAKING CHANGES:
=================
/users:post Request - Body.Body.nickname.address.email.name.Body : User - Deleted property
compatibility test FAILED: 1 breaking changes detected
```
2. 生成服务端代码
我们也可以先定义Swagger接口文档再用swagger命令基于Swagger接口文档生成服务端代码。假设我们的应用名为go-user进入gopractise-demo/swagger目录创建go-user目录并生成服务端代码
```
$ mkdir go-user
$ cd go-user
$ swagger generate server -f ../swagger.yaml -A go-user
```
上述命令会在当前目录生成cmd、restapi、models文件夹可执行如下命令查看server组件启动方式
```
$ go run cmd/go-user-server/main.go -h
```
3. 生成客户端代码
在go-user目录下执行如下命令
```
$ swagger generate client -f ../swagger.yaml -A go-user
```
上述命令会在当前目录生成client包含了API接口的调用函数也就是API接口的Go SDK。
4. 验证Swagger文档是否合法
```
$ swagger validate swagger.yaml
2020/10/21 09:53:18
The swagger spec at "swagger.yaml" is valid against swagger specification 2.0
```
5. 合并Swagger文档
```
$ swagger mixin swagger_part1.yaml swagger_part2.yaml
```
## IAM Swagger文档
IAM的Swagger文档定义在[iam/api/swagger/docs](https://github.com/marmotedu/iam/tree/v1.0.0/api/swagger/docs)目录下遵循go-swagger规范进行定义。
[iam/api/swagger/docs/doc.go](https://github.com/marmotedu/iam/blob/v1.0.0/api/swagger/docs/doc.go)文件定义了更多Swagger文档的基本信息比如开源协议、联系方式、安全认证等。
更详细的定义你可以直接查看iam/api/swagger/docs目录下的Go源码文件。
为了便于生成文档和启动HTTP服务查看Swagger文档该操作被放在Makefile中执行位于[iam/scripts/make-rules/swagger.mk](https://github.com/marmotedu/iam/blob/v1.0.0/scripts/make-rules/swagger.mk)文件中):
```
.PHONY: swagger.run
swagger.run: tools.verify.swagger
@echo "===========> Generating swagger API docs"
@swagger generate spec --scan-models -w $(ROOT_DIR)/cmd/genswaggertypedocs -o $(ROOT_DIR)/api/swagger/swagger.yaml
.PHONY: swagger.serve
swagger.serve: tools.verify.swagger
@swagger serve -F=redoc --no-open --port 36666 $(ROOT_DIR)/api/swagger/swagger.yaml
```
Makefile文件说明
* tools.verify.swagger检查Linux系统是否安装了go-swagger的命令行工具swagger如果没有安装则运行go get安装。
* swagger.run运行 `swagger generate spec` 命令生成Swagger文档swagger.yaml运行前会检查swagger是否安装。 `--scan-models` 指定生成的文档中包含带有swagger:model 注释的Go Models。`-w` 指定swagger命令运行的目录。
* swagger.serve运行 `swagger serve` 命令打开Swagger文档swagger.yaml运行前会检查swagger是否安装。
在iam源码根目录下执行如下命令即可生成并启动HTTP服务查看Swagger文档
```
$ make swagger
$ make serve-swagger
2020/10/21 06:45:03 serving docs at http://localhost:36666/docs
```
打开浏览器,打开`http://x.x.x.x:36666/docs`查看Swagger文档x.x.x.x是服务器的IP地址如下图所示
![](https://static001.geekbang.org/resource/image/6a/3b/6ac3529ed98aa94573862da99434683b.png?wh=2524x1440)
IAM的Swagger文档还可以通过在iam源码根目录下执行`go generate ./...`命令生成为此我们需要在iam/cmd/genswaggertypedocs/swagger\_type\_docs.go文件中添加`//go:generate`注释。如下图所示:
![](https://static001.geekbang.org/resource/image/cc/d7/cc03b896e5403cc55d7e11fe2078d9d7.png?wh=1657x397)
## 总结
在做Go服务开发时我们要向前端或用户提供API文档手动编写API文档工作量大也难以维护。所以现在很多项目都是自动生成Swagger格式的API文档。提到Swagger很多开发者不清楚其和OpenAPI的区别所以我也给你总结了OpenAPI是一个API规范Swagger则是实现规范的工具。
在Go中用得最多的是利用go-swagger来生成Swagger格式的API文档。go-swagger包含了很多语法我们可以访问[Swagger 2.0](https://goswagger.io)进行学习。学习完Swagger 2.0的语法之后就可以编写swagger注释了之后可以通过
```
$ swagger generate spec -o swagger.yaml
```
来生成swagger文档 swagger.yaml。通过
```
$ swagger serve --no-open -F=swagger --port 36666 swagger.yaml
```
来提供一个前端界面供我们访问swagger文档。
为了方便管理,我们可以将 `swagger generate spec``swagger serve` 命令加入到Makefile文件中通过Makefile来生成Swagger文档并提供给前端界面。
## 课后练习
1. 尝试将你当前项目的一个API接口用go-swagger生成swagger格式的API文档如果中间遇到问题欢迎在留言区与我讨论。
2. 思考下为什么IAM项目的swagger定义文档会放在iam/api/swagger/docs目录下这样做有什么好处
欢迎你在留言区与我交流讨论,我们下一讲见。