Go 与 OpenAPI 生态系统
绪论
在用 Go 语言开发 Production 后端服务器时,几乎所有开发者最先遇到的难题之一如下:
API 文档化,如何进行?
对此稍作了解,便会意识到编写符合 OpenAPI 规范的文档是有益的,并自然而然地寻找与 OpenAPI 联动的库。然而,即使做出这样的决定,仍然存在下一个问题:
OpenAPI 相关库有很多,应该用哪个?
本文是为正在经历这种情况的 Go 入门者编写的简要库介绍文章。本文档编写于 2024 年末,由于语言生态系统始终处于动态变化中,建议在参考的同时,始终关注最新动态。
面向 OpenAPI 的库的策略
正如您可能已经知道的那样,OpenAPI 是用于明确定义和文档化 REST API 的规范。它以 YAML 或 JSON 格式定义 API 的端点、请求和响应格式,从而不仅帮助开发者,还能自动化前端和后端代码的生成,减少无意义的重复,并大大减少细微的人为错误。
为了将 OpenAPI 自然地整合到项目中,Go 生态系统中的库主要采取以下三种策略:
1. 将 Go 注释组合为 OpenAPI 规范文档
在按照 OpenAPI 开发 API 时,一个棘手的问题是,实际文档和实现该文档的代码位于不同的文件中,并且位置完全不同,因此,代码更新后忘记更新文档,或者文档更新后忘记更新代码的情况时有发生。
举一个简单的例子:
- 在
./internal/server/user.go
文件中修改了 API 的逻辑。 - 实际文档位于
./openapi3.yaml
中,并且可能会不小心忘记对其进行修改。 - 如果未意识到此更改问题而提交 Pull Request 并接受同事的审查,
- 审查人员也可能看不到
./openapi3.yaml
的更改,从而可能发生 API 规范未变但实际 API 实现已更改的不幸情况。
以 Go 注释的形式编写 API 文档可以在一定程度上解决这个问题。由于代码和文档集中在一处,因此可以在修改代码的同时更新注释。存在一些工具可以基于这些注释自动生成 OpenAPI 规范文档。
Swag 是一个典型的项目。Swag 解析 Go 代码中的注释,并生成 OpenAPI 2 格式的文档。使用方法很简单:在处理程序函数上方按照每个库定义的格式编写注释即可。
1// @Summary 创建用户
2// @Description 创建新用户。
3// @Tags Users
4// @Accept json
5// @Produce json
6// @Param user body models.User true "用户信息"
7// @Success 200 {object} models.User
8// @Failure 400 {object} models.ErrorResponse
9// @Router /users [post]
10func CreateUser(c *gin.Context) {
11 // ...
12}
编写这些注释后,Swag CLI 会解析这些注释并生成 OpenAPI 2 文档。通常,此操作在 CI 过程中执行,生成的 OpenAPI 规范文档将部署到 Git 存储库、最终构建结果、单独的外部 API 文档管理系统,以便与其他项目协作时使用。
优点:
- 由于注释与代码在一起,因此 实际代码和文档的形态差异的可能性会降低。
- 无需单独的工具或复杂的设置,仅通过注释即可简单自由地进行文档化。
- 由于注释不会影响实际的 API 逻辑,因此 添加不适合公开为文档的临时功能非常方便。
缺点:
- 随着注释行数的增加,单个代码文件的可读性可能会降低。
- 以注释的形式可能难以表达所有 API 规范。
- 由于文档不强制代码,因此 无法保证 OpenAPI 文档与实际逻辑一致。
2. 从 OpenAPI 规范文档生成 Go 代码
也可以将 Single source of Truth (SSOT) 放在文档侧,而不是 Go 代码中。即先定义 OpenAPI 规范,然后基于定义的内容生成 Go 代码。由于 API 规范会直接生成代码,因此在开发文化上可以强制先进行 API 设计,并且在开发顺序上,定义 API 规范是最先开始的,因此可以及早防止在开发完成后才意识到遗漏部分,然后修改 API 规范并修改整个代码的不幸情况。
oapi-codegen 和 OpenAPI Generator 是采用此方法的典型项目。使用方法很简单:
- 编写符合 OpenAPI 规范的 yaml 或 json 文档。
- 运行 CLI。
- 将生成相应的 Go stub 代码。
- 现在,只需直接实现各个 API 的详细逻辑,以便该 stub 可以使用。
以下是 oapi-codegen 生成的代码示例:
1// StrictServerInterface 表示所有服务器处理程序。
2type StrictServerInterface interface {
3 // ...
4 // 返回所有宠物
5 // (GET /pets)
6 FindPets(ctx context.Context, request FindPetsRequestObject) (FindPetsResponseObject, error)
7 // ...
8}
以上接口作为媒介,oapi-codegen 生成的代码执行查询参数、标头、主体解析和验证等逻辑,并调用在接口中声明的适当方法。用户只需实现上述接口的实现,即可完成 API 实现所需的工作。
优点:
- 由于先有规范,后进行开发,因此在多个团队协作的情况下,并行进行工作会更加有利。
- 自动生成原本需要重复性劳动的部分的代码,因此提高工作效率,并且仍然有利于调试。
- 更容易保证文档和代码的形态始终一致。
缺点:
- 如果对 OpenAPI 规范本身不熟悉,则 初始学习曲线会稍陡峭。
- 由于处理 API 的代码的形态是由项目自动生成的,因此在需要自定义时可能难以应对。
作者的评论: 截至 2024 年 10 月,OpenAPI Generator 生成的 Go 代码不仅强制执行 API 逻辑,还强制执行整个项目形态,并且项目结构僵化,因此生成的代码不适合添加实际生产环境所需的各种功能。强烈建议采用此方法的人员使用 oapi-codegen。作者使用 oapi-codege + echo + StrictServerInterface。
3. 从 Go 代码生成 OpenAPI 规范文档
当数十人或数百人针对同一服务器进行开发时,不可避免地会出现的问题是各个 API 的统一性可能会被破坏。一个直观的例子是,如果将超过 100 个 API 端点的规范声明在一个 OpenAPI yaml 文件中,则该文件将成为一个超过 1 万行的庞然大物,并且在声明新的 API 端点时,不可避免地会出现重复声明相同模型、遗漏某些字段、产生不符合约定的路径命名等问题,从而开始破坏整个 API 的统一性。
为了解决这些问题,可以单独指定管理 OpenAPI yaml 的 Owner,或者开发 Linter 以便在 CI 过程中自动捕获,但是可以通过使用 Go 语言定义 Domain-specific language (DSL) 来强制所有 API 具有一致的统一性。
Kubernetes 是使用这种技术的典型项目(无需单独的库即可自行构建),也可以使用 go-restful、goa 等项目。以下是 goa
的使用示例:
1var _ = Service("user", func() {
2 Method("create", func() {
3 Payload(UserPayload)
4 Result(User)
5 HTTP(func() {
6 POST("/users")
7 Response(StatusOK)
8 })
9 })
10})
如上所述,编写可编译的 Go 代码,可以同时完成 POST /users
API 的实现和文档定义,从而获得优势。
优点:
- 由于所有内容都来自代码,因此很容易保持整个项目的 API 一致性。
- 通过利用 Go 的强类型系统,可以获得比利用 OpenAPI3 的所有功能时更准确且无争议的规范。
缺点:
- 必须熟悉每个框架定义的 DSL,并且可能难以应用于现有代码。
- 由于必须强制遵循框架提出的规则,因此自由度和灵活性可能会降低。
总结
每种方法都有其优缺点,根据项目的需求和团队的偏好选择合适的方法非常重要。最重要的一点始终不是使用哪种方法更好,而是对当前所处情况进行价值判断,找到最合适的解决方案,并提高开发效率,以便早点下班,享受令人满意的生活工作平衡。
本文虽然是基于 2024 年 10 月编写的,但由于 Go 和 OpenAPI 生态系统在不断发展,请考虑阅读本文的时间间隔,并持续关注各个库和项目的最新动态及其变化的优缺点。
祝您 Go 生活愉快~ 😘