---
id: e10df2108115b462a5ac2a289080a901
author: iwanhae
title: Go 与 OpenAPI 生态系统
description: 介绍在使用 Go 语言进行基于 OpenAPI 的 API 开发时可以采用的库和策略。
language: zh
date: 2024-10-18T06:07:43.092628696Z
path: /blog/posts/go-and-the-openapi-ecosystem-zd5a8e472
category: api
---

### 引言

在使用 Go 语言开发生产环境后端服务器时，几乎所有开发者最先遇到的难题之一便是如下问题。

> 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](https://github.com/swaggo/swag)。Swag 解析 Go 代码中的注释，并生成 OpenAPI 2 格式的文档。其使用方法十分简单：只需在处理函数上方，按照各库规定的格式编写注释即可。

```go
// @Summary 유저 생성
// @Description 새로운 유저를 생성합니다.
// @Tags Users
// @Accept json
// @Produce json
// @Param user body models.User true "유저 정보"
// @Success 200 {object} models.User
// @Failure 400 {object} models.ErrorResponse
// @Router /users [post]
func CreateUser(c *gin.Context) {
    // ...
}
```

如此编写注释后，名为 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](https://github.com/oapi-codegen/oapi-codegen) 与 [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator/tree/master/samples/server/petstore/go-echo-server)。其使用方法同样简单：

1. 按照 OpenAPI 规范编写 yaml 或 json 文档；
2. 运行 CLI；
3. 即会生成对应的 Go stub 代码；
4. 此后只需直接实现各个 API 的具体逻辑，使该 stub 得以运作即可。

以下是 oapi-codegen 所生成代码的示例：

```go
// StrictServerInterface represents all server handlers.
type StrictServerInterface interface {
	// ...
	// Returns all pets
	// (GET /pets)
	FindPets(ctx context.Context, request FindPetsRequestObject) (FindPetsResponseObject, error)
	// ...
}
```

以上述 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](https://github.com/emicklei/go-restful)、[goa](https://goa.design/) 等项目。以下是 `goa` 的使用示例：

```go
var _ = Service("user", func() {
    Method("create", func() {
        Payload(UserPayload)
        Result(User)
        HTTP(func() {
            POST("/users")
            Response(StatusOK)
        })
    })
})
```

如上所示，编写可编译的 Go 代码后，`POST /users` API 的实现与文档定义便可同时完成，这是其显著优势。

**优点：**

- 一切均由代码生成，因此**便于在整个项目范围内保持 API 的一致性**。
- 借助 Go 的强类型系统，相较于充分利用 OpenAPI3 全部功能的情形，能够获得**更加精确且无争议的规范**。

**缺点：**

- 需要**掌握各框架所定义的 DSL**，且**较难应用于既有代码**。
- 由于必须强制遵循框架所提出的规则，**自由度与灵活性可能下降。**

**结语**

各种方法各有优劣，根据项目需求与团队偏好选择合适的方法至关重要。最重要的并非哪种方式更好，而是对自身所处的情境进行价值判断，明确何种方案最为适合，并提升开发效率，从而享受**准时下班**与**令人满意的工作与生活平衡**。

本文以 2024 年 10 月为基准撰写，但 Go 与 OpenAPI 生态仍在持续发展。请考虑阅读本文时的时间间隔，持续跟进各库与项目的最新动态及其变化后的优缺点。

祝您 Go 生活愉快~ 😘
