GoSuda

Go 与 OpenAPI 生态系统

By iwanhae
views ...

绪论

在用 Go 语言开发 Production 后端服务器时,几乎所有开发者最先遇到的难题之一如下:

API 文档化,如何进行?

对此稍作了解,便会意识到编写符合 OpenAPI 规范的文档是有益的,并自然而然地寻找与 OpenAPI 联动的库。然而,即使做出这样的决定,仍然存在下一个问题:

OpenAPI 相关库有很多,应该用哪个?

本文是为正在经历这种情况的 Go 入门者编写的简要库介绍文章。本文档编写于 2024 年末,由于语言生态系统始终处于动态变化中,建议在参考的同时,始终关注最新动态。

面向 OpenAPI 的库的策略

正如您可能已经知道的那样,OpenAPI 是用于明确定义和文档化 REST API 的规范。它以 YAML 或 JSON 格式定义 API 的端点、请求和响应格式,从而不仅帮助开发者,还能自动化前端和后端代码的生成,减少无意义的重复,并大大减少细微的人为错误。

为了将 OpenAPI 自然地整合到项目中,Go 生态系统中的库主要采取以下三种策略:

1. 将 Go 注释组合为 OpenAPI 规范文档

在按照 OpenAPI 开发 API 时,一个棘手的问题是,实际文档和实现该文档的代码位于不同的文件中,并且位置完全不同,因此,代码更新后忘记更新文档,或者文档更新后忘记更新代码的情况时有发生。

举一个简单的例子:

  1. ./internal/server/user.go 文件中修改了 API 的逻辑。
  2. 实际文档位于 ./openapi3.yaml 中,并且可能会不小心忘记对其进行修改。
  3. 如果未意识到此更改问题而提交 Pull Request 并接受同事的审查,
  4. 审查人员也可能看不到 ./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-codegenOpenAPI Generator 是采用此方法的典型项目。使用方法很简单:

  1. 编写符合 OpenAPI 规范的 yaml 或 json 文档。
  2. 运行 CLI。
  3. 将生成相应的 Go stub 代码。
  4. 现在,只需直接实现各个 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-restfulgoa 等项目。以下是 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 生活愉快~ 😘