mcpField
G
go-openapi

go-openapi

github.com/eduardo-sl开发8378 次安装2 周前更新
代码仓库

关于 go-openapi

go-openapi 是一项指导 Go 项目采用 OpenAPI 契约驱动开发的技能,适合维护 API 的开发者与审阅者。它覆盖生成器选择、代码生成、请求验证、兼容性检查和契约测试,并坚持规范作为唯一事实来源。

主要功能

  1. 01从规范生成类型、路由与客户端
  2. 02将生成流程接入构建与 CI
  3. 03用中间件验证请求约束
  4. 04检查破坏性 API 变更
  5. 05以生成客户端进行契约测试

适用场景

  1. 01为现有 Go 服务引入 OpenAPI 生成
  2. 02扩展由 OpenAPI 驱动的 API
  3. 03审查实现、规范与客户端的一致性
  4. 04在 CI 中检测不兼容变更

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/eduardo-sl/go-agent-skills --skill go-openapi

安装后在 Claude Code 中输入/go-openapi确认已加载。

详细介绍

Go OpenAPI

规范是唯一可信来源。类型、路由和客户端均由它生成,而不是在旁边手动编写,因为两份手动维护的契约副本会在一个冲刺周期内出现分歧。

运行模式

  • 采用 — 项目有手写处理器但没有规范,或者虽有规范却无人据此生成代码。引入代码生成,无需重写。
  • 扩展 — 已有生成流水线。修改规范,重新生成并实现。
  • 审查 — 检查处理器、规范和已发布的客户端是否一致,并确认更改不会造成静默破坏。

1. 只选择一次代码生成器

工具适用场景
oapi-codegen默认选择。生成类型、服务端接口和客户端,兼容 net/http、chi、echo、gin
ogen需要完全生成且严格验证的服务端,并且能接受它的设计取舍
swaggo/swag仅适用于不打算迁移的旧版代码优先项目——注解会生成规范,因此在代码存在之前无法审查规范

不要混用。项目同时使用注解和签入仓库的规范,就又有了两个唯一可信来源。

2. 将代码生成接入构建流程

将生成器固定为模块工具(Go 1.24+),确保每位开发者和每次 CI 运行都使用相同版本:

go get -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
# oapi-codegen.yaml
package: api
output: internal/api/openapi.gen.go
generate:
  models: true
  std-http-server: true   # Go 1.22+ ServeMux; use chi-server / echo-server if that is the router
  strict-server: true     # typed request/response structs instead of raw http.ResponseWriter
  embedded-spec: true     # lets the service serve its own spec
output-options:
  skip-prune: false
//go:generate go tool oapi-codegen -config oapi-codegen.yaml ../../api/openapi.yaml

提交生成的文件。审查者需要看到契约更改的差异,而且构建不能依赖已安装生成器。

如果生成文件已过时,CI 必须失败:

go generate ./... && git diff --exit-code

3. 实现生成的接口

严格模式会提供类型化的请求和响应,因此编译器可以强制遵守契约。

// Generated: type StrictServerInterface interface { GetUser(ctx, GetUserRequestObject) (GetUserResponseObject, error) }

type Server struct{ users UserStore }

var _ api.StrictServerInterface = (*Server)(nil) // compile-time compliance

func (s *Server) GetUser(ctx context.Context, req api.GetUserRequestObject) (api.GetUserResponseObject, error) {
    u, err := s.users.Find(ctx, req.Id)
    if errors.Is(err, ErrNotFound) {
        return api.GetUser404JSONResponse{Title: "user not found", Status: 404}, nil
    }
    if err != nil {
        return nil, fmt.Errorf("find user %s: %w", req.Id, err) // 500 via the error handler
    }
    return api.GetUser200JSONResponse{Id: u.ID, Email: u.Email}, nil
}

规则:

  • 添加断言 var _ api.StrictServerInterface = (*Server)(nil)。这样,在规范中添加端点会变成编译错误,而不是在预发布环境中返回 404。
  • 为每个已记录的状态码返回类型化响应。仅在未记录的失败路径中返回 Go error。
  • 永远不要编辑 *.gen.go。每项更改都从 YAML 开始。

4. 根据规范验证请求

生成的类型会检查结构,但不会检查约束。查询参数中的 minLength、pattern、enum 和 required 只有在添加验证中间件后才会生效。

spec, err := api.GetSwagger()
if err != nil {
    return fmt.Errorf("load spec: %w", err)
}
spec.Servers = nil // otherwise the server URL must match exactly

mux := http.NewServeMux()
handler := nethttpmiddleware.OapiRequestValidator(spec)(mux)

这会在请求边界处拒绝格式错误的输入,并在任何处理器运行前返回 400。领域验证应保留在领域层——中间件负责强制遵守契约,而不是业务规则。

5. 错误:RFC 9457 问题详情

定义一个错误架构,并在每个失败响应中引用它。

components:
  schemas:
    Problem:
      type: object
      required: [type, title, status]
      properties:
        type:   { type: string, format: uri, default: "about:blank" }
        title:  { type: string }
        status: { type: integer }
        detail: { type: string }
        instance: { type: string }

将其作为 application/problem+json 提供。永远不要返回纯字符串,也不要将内部错误消息放入 detail——记录包装后的错误,并返回稳定且安全的标题。

6. 版本控制与破坏性更改

通过机械方式检测破坏性更改;审查者可能会漏掉。

go install github.com/oasdiff/oasdiff@latest
oasdiff breaking api/openapi.yaml.base api/openapi.yaml --fail-on ERR

在 CI 中将其与主分支上的规范进行比较。实际中,破坏性更改包括:删除端点或字段、收窄类型、添加必需的请求字段,或添加客户端必须理解的必需响应字段、更改状态码,以及从响应中移除枚举值。

新增内容属于安全更改。只有在无法避免破坏性更改时,才对路径进行版本控制(/v2/...),并在客户端迁移完成前继续提供旧版本。

7. 契约测试

只有在实现与契约不一致时会导致失败,规范才算是真正的契约。

func TestGetUser_MatchesSpec(t *testing.T) {
    spec, err := api.GetSwagger()
    require.NoError(t, err)
    spec.Servers = nil

    srv := httptest.NewServer(newTestHandler(t, spec))
    t.Cleanup(srv.Close)

    // Generated client — if the spec changed, this stops compiling
    c, err := api.NewClientWithResponses(srv.URL)
    require.NoError(t, err)

    resp, err := c.GetUserWithResponse(t.Context(), "u-1")
    require.NoError(t, err)
    require.Equal(t, http.StatusOK, resp.StatusCode())
    require.Equal(t, "u-1", resp.JSON200.Id)
}

在测试中使用生成的客户端,而不是手动编写 http.NewRequest。这样,契约漂移会导致编译失败。

8. 保持规范便于审查

vacuum lint -d api/openapi.yaml     # or: spectral lint, redocly lint
  • 每个 API 一个文件,存放在 api/ 下,签入仓库并像代码一样审查。
  • 每个操作都要有 operationId——它会成为 Go 方法名。
  • 每个架构都要有 description;它会成为生成类型的 godoc。
  • 使用 $ref 将大型规范拆分到 components/,不要通过生成片段来拆分。

验证清单

  1. 确保只有一个事实来源:纳入版本控制的规范文件,且不附带注解生成器
  2. 通过 go.mod 的 tool 指令固定生成器版本
  3. 提交生成的文件,并确保 CI 在运行 go generate 和 git diff --exit-code 后失败
  4. 存在 var _ api.StrictServerInterface = (*Server)(nil)
  5. 不得手动编辑 *.gen.go
  6. 安装请求验证中间件,并通过 400 测试覆盖
  7. 错误使用统一的 Problem 架构,并以 application/problem+json 提供
  8. 在 CI 中针对基础规范运行 oasdiff breaking
  9. 至少有一个测试使用生成的客户端调用真实处理器
  10. 规范通过 lint 检查,且每个操作都有 operationId

评论

支持 Markdown