Go 与 OpenAPI 生态系统

引言
在使用 Go 语言开发生产环境后端服务器时,几乎所有开发者最先遇到的难题之一便是如下问题。
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 Repository、最终构建产物或独立的外部 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 represents all server handlers.
2type StrictServerInterface interface {
3 // ...
4 // Returns all pets
5 // (GET /pets)
6 FindPets(ctx context.Context, request FindPetsRequestObject) (FindPetsResponseObject, error)
7 // ...
8}
以上述 interface 为媒介,oapi-codegen 生成的代码负责执行 query parameters、header、body 的解析及 Validation 等逻辑,并调用 interface 中声明的相应 method。使用者只需实现该 interface 的具体实现体,即完成了 API 实现所需的全部工作。
优点:
- 规范先行、随后进行开发,因此在多团队协作的情况下,便于并行推进工作。
- 原本属于重复性的手工劳动的部分代码会自动生成,因此工作效率得以提升,同时调试依然十分便利。
- 易于保证文档与代码的形态始终一致。
缺点:
- 若对 OpenAPI 规范本身不熟悉,初期学习曲线会有一定的陡峭。
- 由于处理 API 的代码结构由项目自动生成,因此在需要定制化时,可能难以应对。
作者注。截至 2024 年 10 月,OpenAPI Generator 所生成的 Go 代码不仅强制规定 API 逻辑,还规定了整个项目的结构,其结构僵化,不适合用于添加生产环境所需的各类功能。采用该方式的读者,强烈建议使用 oapi-codegen。作者本人使用的是 oapi-codegen + echo + StrictServerInterface。
3. 由 Go 代码生成 OpenAPI 规范文档
当数十、数百人同时对同一服务器进行开发时,必然会出现的问题是:各个 API 之间的一致性可能被打破。直观的例子是,若将 100 多个 API Endpoint 的规范全部声明在一个 OpenAPI yaml 文件中,该文件必将膨胀为超过一万行的庞然大物;在新增 API Endpoint 时,难免会重复声明相同的模型,或遗漏某些字段,或出现不符合约定的 Path 命名,整体 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 生活愉快~ 😘