关于 go-openapi
go-openapi 是一项指导 Go 项目采用 OpenAPI 契约驱动开发的技能,适合维护 API 的开发者与审阅者。它覆盖生成器选择、代码生成、请求验证、兼容性检查和契约测试,并坚持规范作为唯一事实来源。
主要功能
- 01从规范生成类型、路由与客户端
- 02将生成流程接入构建与 CI
- 03用中间件验证请求约束
- 04检查破坏性 API 变更
- 05以生成客户端进行契约测试
适用场景
- 01为现有 Go 服务引入 OpenAPI 生成
- 02扩展由 OpenAPI 驱动的 API
- 03审查实现、规范与客户端的一致性
- 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/,不要通过生成片段来拆分。
验证清单
- 确保只有一个事实来源:纳入版本控制的规范文件,且不附带注解生成器
- 通过
go.mod的 tool 指令固定生成器版本 - 提交生成的文件,并确保 CI 在运行
go generate和git diff --exit-code后失败 - 存在
var _ api.StrictServerInterface = (*Server)(nil) - 不得手动编辑
*.gen.go - 安装请求验证中间件,并通过 400 测试覆盖
- 错误使用统一的
Problem架构,并以application/problem+json提供 - 在 CI 中针对基础规范运行
oasdiff breaking - 至少有一个测试使用生成的客户端调用真实处理器
- 规范通过 lint 检查,且每个操作都有
operationId