Skip to content

Go Web 与 RPC 工具选型:先分清它们替你承担哪一层 ​

Gin、Echo、chi、Fiber 是处理 HTTP 请求的框架或路由器;validator 检查字段;grpc-go 和 connect-go 处理 RPC;Buf 管理 Protobuf 契约;swag 与 oapi-codegen 在代码和接口文档之间生成文件。把这十个工具放进同一张性能榜,无法回答一个项目该怎么组合它们。

选型先看已经拥有的代码、客户端和运维能力,再看工具能减少哪些重复工作、会引入哪些语义变化。本篇逐个说明常用入口与限制,并实际验证 chi、validator、Connect 三种协议兼容和 OpenAPI 模型生成。版本全部明确列出,固定版本是复现和查证基线,不表示应在新生产项目里不经审查地沿用。

先按层选择,不必每层都选一个 ​

层工具主要决定
HTTP 路由与请求上下文Gin、Echo、chi、FiberHandler 模型、中间件兼容、绑定与错误处理
字段验证validator标签含义、自定义规则、错误格式
RPC 运行时grpc-go、connect-go客户端协议、流模式、HTTP 栈、生态能力
Protobuf 工程工具Buflint、依赖、生成、兼容性检查
从代码生成文档swag注释规范、生成结果、文档是否与实现一致
从契约生成 Gooapi-codegenOpenAPI 版本、生成目标、运行时校验

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    A[HTTP / OpenAPI 契约] --> B[oapi-codegen]
    B --> C[Go 类型、客户端或服务端接口]
    C --> D[net/http / 框架与业务]
    D --> E[validator 或契约校验器]
    F[Go 注释] --> G[swag]
    G --> H[Swagger 文档]
    I[Protobuf 契约] --> J[Buf 检查与生成编排]
    J --> K[grpc-go 或 connect-go 生成代码]
    K --> L[RPC 业务实现]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    A[HTTP / OpenAPI 契约] --> B[oapi-codegen]
    B --> C[Go 类型、客户端或服务端接口]
    C --> D[net/http / 框架与业务]
    D --> E[validator 或契约校验器]
    F[Go 注释] --> G[swag]
    G --> H[Swagger 文档]
    I[Protobuf 契约] --> J[Buf 检查与生成编排]
    J --> K[grpc-go 或 connect-go 生成代码]
    K --> L[RPC 业务实现]

图里的 validator 和契约校验器不是自动串联的:生成器产出 Go 类型以后,仍需应用主动调用验证逻辑。OpenAPI 和 Protobuf 也可以用于不同边界,不必把整个公司强行统一成一种协议。

四种 Web 框架:先看 Handler 是否仍是 net/http ​

工具与查证版本处理函数形状主要优势迁移时要检查
Gin v1.11.0func(*gin.Context)路由分组、绑定、渲染和成熟中间件习惯Abort/Next、自动错误响应、Context 生命周期
Echo v4.13.4func(echo.Context) error返回 error,集中处理响应错误绑定来源覆盖、错误处理器、Context 复用
chi v5.2.3标准 http.Handler与标准中间件、httptest 直接组合绑定校验需要自己配,路由上下文API不同
Fiber v2.52.9func(*fiber.Ctx) errorfasthttp 栈和类似 Express 的接口适配器、内存引用、取消与流式语义

标准库 ServeMux 本身也是基线。Go 1.22 起已支持方法和路径参数,小服务未必需要框架。若团队已有通用日志、鉴权、追踪等 func(http.Handler) http.Handler 组件,保留 net/http 模型通常能减少迁移工作;这是一条基于依赖关系的判断,不是吞吐排名。

Gin:方便的 Context,也带有自动行为 ​

常用入口是 gin.New 或 gin.Default、GET/POST、Group、Use。gin.Default 默认加日志和 Recovery;想自己定义边界时用 New。Gin Engine 实现 http.Handler,可以交给 http.Server,但内层中间件使用 Gin 自己的 Context、Next 和 Abort。

ShouldBindJSON 在绑定失败时把错误交还给你;BindJSON 走 MustBind 路径,会按错误先 Abort 并设置响应状态。在 v1.11.0 源码中,识别到 MaxBytesError 是 413,其他绑定错误通常为 400。若你的 API 要统一 Problem Details,用可控的 ShouldBind 路径更容易在提交响应前决定格式。Gin 固定 Context 源码

Abort 阻止后续处理链,不等于 Go 的 return。当前函数里写了 AbortWithStatus 后,如果下面还有业务代码,仍应明确 return。Context 来自复用机制,也不要在请求结束后把原 Context 留给后台工作;需要的字段先复制,后台任务另设生命周期。便利接口减少样板代码,但没有取消 HTTP 响应只能提交一次的约束。

Echo:让处理函数把错误交给统一出口 ​

Echo 用 echo.New、e.GET、e.Group、e.Use 建服务,Handler 返回 error,统一的 HTTPErrorHandler 可以把领域错误映射成状态码和错误体。c.JSON 等方法本身也返回 error,调用方通常直接返回它。

绑定入口 c.Bind 需要留意来源。v4.13.4 的默认 Binder 先绑定路径参数;对 GET、DELETE、HEAD 再绑定 Query;最后绑定 Body,后面的来源可能覆盖前面的值。Header 绑定另有入口。如果目标对象 ID 来自路径,不要又让 Body 悄悄覆盖同一字段;可以分 DTO,或选择 BindPathParams、BindQueryParams、BindBody 单一来源。Echo 固定 bind.go

Echo 的绑定不等于完整业务校验。可以挂接自己的 Validator 实现,并把字段错误转成对外契约。它同样复用 Context,不应把它跨请求留存;下游调用用 c.Request().Context(),不要因为框架也有一个“Context”就忽略标准取消链。Echo v4.13.4

chi:保留标准 Handler 的组合方式 ​

chi 的常用入口是 chi.NewRouter、Use、Route、Mount、Get 和 chi.URLParam。处理函数仍接收标准 ResponseWriter 与 *Request,所以已有 net/http 中间件可以直接使用。它负责路由和组合,不替你选择 JSON 验证库或统一错误模型。chi v5.2.3

本次的小服务从路径读三位数字 ID:

go
r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
    in := struct {
        ID string `validate:"required,number,len=3"`
    }{ID: chi.URLParam(r, "id")}
    if err := validate.Struct(in); err != nil {
        http.Error(w, "id must contain 3 digits", 422)
        return
    }
    w.WriteHeader(204)
})

测试断言 /users/123 为 204,abc、12、-12、+12、1.2 为 422,不存在的路由为 404;标准中间件给这些路径都加了相同 Header。这个实验验证兼容与校验路径,不是路由吞吐基准。

Fiber:换了 HTTP 栈,就要重新验收边界 ​

这里固定讨论 Fiber v2.52.9,不把 v3 API 混进来。Fiber 构建在 fasthttp 上,fiber.New、app.Get、app.Use 和 *fiber.Ctx 组成它的接口。它不天然等于标准 http.Handler;即便用适配器连接 net/http 组件,也要重新测 Header、取消、升级、SSE、代理和性能,不要假设接口转换保留了所有语义。Fiber v2.52.9

它对减少分配的追求还影响返回值生命周期。固定源码明确说明,Body、BodyRaw、Get 等返回的内容通常只在当前 Handler 内有效,需要长期保留时复制,或评估 Immutable 配置的成本。把请求体切片直接放进异步队列,可能在后续请求复用缓冲后读到其他内容。Fiber v2.52.9 ctx.go

这个差异比一张“hello world 每秒多少次”的图更值得先看。如果项目严重依赖标准 HTTP 生态,适配与验证成本可能大于路由阶段节省的时间;如果团队已经熟悉 fasthttp 并测过真实负载,则可以在明确边界下选它。本文没有运行这三种框架的性能对比,也没有宣称任何一个绝对更快。

validator:标签是程序,不能凭英文猜语义 ​

validator 用 validator.New 建实例,用 Struct 或 Var 校验。常见标签有 required、min/max、len、oneof,以及集合元素验证的 dive;跨字段限制和领域规则可以用自定义校验。注册规则应在服务启动时完成,再复用实例,不要在处理请求时并发修改全局校验器。validator v10.27.0

一个很具体的误用是把 numeric,len=3 当成“三个数字字符”。实际跑一遍:

go
for _, s := range []string{"123", "-12", "+12", "1.2", "abc"} {
    fmt.Printf("%s numeric=%v number=%v\n", s,
        v.Var(s, "required,numeric,len=3") == nil,
        v.Var(s, "required,number,len=3") == nil)
}

输出:

text
123 numeric=true number=true
-12 numeric=true number=false
+12 numeric=true number=false
1.2 numeric=true number=false
abc numeric=false number=false

原因在固定版本的正则定义里:numeric 允许正负号和小数,number 对字符串使用 [0-9]+。三位数字字符串在本例应选 required,number,len=3。这同时保留 001,如果先转整数再校验,就失去原始字符长度。v10.27.0 regexes.go

required 对数值字段会把零值视为缺失,业务若允许 0,要用指针或独立存在标记表达“有值”。WithRequiredStructEnabled 明确启用该版本的结构体 required 行为,避免依赖未来默认变化。错误处理还需区分 ValidationErrors 和调用方式错误 InvalidValidationError,不要直接把内部 Go 字段名和原始值输出给客户端。

验证格式也不验证权限或事实。“邮箱格式合法”不证明邮箱属于此用户;“订单 ID 合法”不证明订单存在或允许修改。validator 应放在这些业务检查之前,而不是取代它们。

grpc-go 与 connect-go:同用 Protobuf,服务接入方式不同 ​

grpc-go:完整的 gRPC 运行时 ​

grpc-go 的常用入口是 grpc.NewServer、生成的 RegisterXServer,以及客户端 grpc.NewClient 和 NewXClient。它围绕 gRPC 协议提供四种调用模式、Metadata、状态、拦截器、名称解析、负载均衡等能力。已有 gRPC 服务发现、观测和多语言客户端时,这些生态约定是重要的选择依据。grpc-go v1.75.1

使用时仍要为每次调用设置合适的 Context,区分 Unary 与 Stream 拦截器,并长期复用 ClientConn。也要锁定 protobuf 生成器、gRPC 生成器和运行时的兼容范围;升级生成代码后,旧运行时可能不再满足它要求的 API。

它不是普通 REST 路由器。浏览器调用、网关、HTTP 错误映射和流式代理需要单独设计。上一类项目的问题是协议与运维整合,而不是在 Gin 路由表中多注册一个路径即可解决。

connect-go:把类型化 RPC 接到标准 HTTP Handler ​

connect-go 的 Handler 基于 net/http,可以放进 ServeMux,并支持 Connect、gRPC 和 gRPC-Web 协议。常见入口由 protoc-gen-connect-go 生成,也可以直接使用 connect.NewUnaryHandler 和 connect.NewClient。选择它通常是希望保持 Protobuf 契约,同时更直接地利用标准 HTTP 的部署与中间件方式。connect-go v1.18.1

本次直接创建一个一元 Handler,用标准 protobuf StringValue 作为消息,核心是:

go
h := connect.NewUnaryHandler("/demo.v1.Greeter/Hello",
    func(ctx context.Context, r *connect.Request[wrapperspb.StringValue]) (
        *connect.Response[wrapperspb.StringValue], error) {
        if r.Msg.Value == "" {
            return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("name required"))
        }
        return connect.NewResponse(wrapperspb.String("hello " + r.Msg.Value)), nil
    })

同一个 Handler 启动在本地 TLS/HTTP2 测试服务上,客户端分别使用默认模式、WithGRPC、WithGRPCWeb,实测:

text
Connect: hello Go; empty name=invalid_argument
gRPC: hello Go; empty name=invalid_argument
gRPC-Web: hello Go; empty name=invalid_argument

这验证三种协议的一元调用与错误映射,不代表浏览器拥有相同的双向流能力。浏览器和 HTTP 版本会约束客户端流/双向流,反向代理也可能缓冲;要按具体传输路径验收。Connect Go streaming

Connect 自己的拦截器与 HTTP 中间件分工不同:前者看到 RPC 消息和错误,后者看到 HTTP Header、方法与连接。不要因为标准中间件可包住 Handler,就假设普通 JSON 错误体符合每一种 RPC 协议。

Buf:先管好契约,再生成代码 ​

Buf 是 Protobuf 工程工具,不是运行在每次请求里的 RPC 服务端。buf lint 检查风格和规则;buf build 构建描述;buf breaking 把当前契约与指定基线比较;buf generate 按插件配置生成代码。生成器仍由对应语言/协议插件负责。Buf 官方文档

在多人维护的服务中,最有价值的是把“字段删了没保留编号”“名称变化会不会破坏客户端”变成 CI 中可重复检查。FILE 等破坏性检查类别比纯 wire 兼容更严格;应根据使用方依赖的代码与 JSON 形态选择。它检测的是规则覆盖的结构性变化,不知道某字段单位被你从秒改成毫秒。Buf breaking

文末附了 v2 格式的 buf.yaml 和完整示意 proto,展示 STANDARD lint 与 FILE breaking 配置。本次没有安装或执行 Buf,不把该配置写成已通过检查。实际接入时要固定 Buf CLI、插件版本、依赖锁文件以及基线 revision,先运行 build/lint,再与真实发布版本比较;不能把“与自己比较无差异”当兼容验证。

是否使用远程生成插件还涉及代码发送与构建可用性。离线或有严格出网要求的环境,优先规划本地插件与依赖缓存。工具能降低重复操作,但不会让供应链与版本管理自动消失。

swag 与 oapi-codegen:生成方向和规范版本都不同 ​

工具与本篇版本输入输出最容易误会的地方
swag v1.16.6Go 注释、类型Swagger 2.0 文档文档生成不会自动约束真实 Handler
oapi-codegen v2.4.1OpenAPI 3.0 契约Go 模型、客户端、服务端胶水等生成类型不等于完整请求校验

swag:适合从现有 Go 服务补文档 ​

swag 通过 @Summary、@Param、@Success、@Failure、@Router 等注释生成接口描述。它可用于已有 Gin、Echo 等服务,文档 UI 只是生成结果的展示层,不是框架的自动安全边界。swag v1.16.6

例如本次文档输入声明:

go
// @Summary Check a user
// @Param id path string true "User ID"
// @Success 204 "No Content"
// @Failure 404 {string} string "Not Found"
// @Router /users/{id} [get]

注释里的 404 不会自动让函数在找不到用户时返回 404;代码、注释、测试必须一起维护。生成产物应进入审查,CI 可重新生成并检查是否有未提交差异。只看 Swagger UI 能打开,不能证明每个状态和字段都正确。

本次实际生成文件的开头是 "swagger": "2.0",路径中含 /users/{id} 的 GET 及 204/404 响应,与上述注释一致。该固定版本输出 Swagger 2.0,不要把它直接当作 OpenAPI 3.0 或 3.1 输入交给另一工具。转换规范需要独立审查,尤其是请求体、安全方案、nullable 和 schema 语义。

oapi-codegen:先写契约,再让 Go 接口跟着变 ​

oapi-codegen 可以生成模型、客户端和不同 HTTP 框架的服务端接口/适配。已有明确外部契约、多语言客户端或设计评审流程时,从规范出发更容易控制变更。生成文件不应手工修改,业务逻辑写在自己的实现层。oapi-codegen v2.4.1

本次 OpenAPI 3.0.3 Schema 把 id 定义为三位数字字符串:

yaml
id:
  type: string
  pattern: '^[0-9]{3}$'

使用固定 v2.4.1 生成 models 后,实际 Go 类型只是:

go
type User struct {
    Id string `json:"id"`
}

测试故意赋值 User{Id: "bad-value"} 再 JSON 编码,得到:

text
generated model accepts: {"id":"bad-value"}

这证明了生成模型不会神奇地执行 pattern。还要添加契约验证中间件或显式业务校验,并测试反例;生成的 strict server 接口也不能被理解为自动检查所有 schema 约束。类型安全、结构解析、协议校验和业务校验是相邻但不同的层。项目 Request/Response validation 说明

这个版本明确以 OpenAPI 3.0 为基础,3.1 支持仍有上游限制,也不直接支持 Swagger 2.0。新版本是否改变,必须重新查看相应 tag 文档。正因为兼容范围不是由工具名字决定,本文没有拿另一篇的 3.1.1 契约直接喂给 v2.4.1。固定 README 的设计边界

真正开始选型时,拿一条有代表性的接口做验证 ​

先把业务层写成普通 Go 函数,例如接收 context.Context 和业务输入结构体,返回结果与领域错误。框架适配层只负责绑定、验证、调用和错误映射。这样换 Gin/chi 或加一个 RPC 入口时,不必同时重写核心业务。

验证项应观察什么
成功、验证失败、权限失败状态与错误体是否一致,链是否正确停止
客户端断开、下游超时取消是否传到需要停止的工作
SSE/大文件/升级Flush、缓冲、Body 上限、可选接口是否保留
真实 JSON 与中间件链CPU、分配、延迟分布,而非空 Handler 纳秒数
过载与慢客户端排队、拒绝策略、内存是否有界
升级生成器或框架生成差异、契约兼容、旧客户端回归

先排除不能满足协议和生态需求的方案,再比较真实负载。数据库耗时 30ms 时,路由节省几百纳秒通常不会改变用户体验;若路由真是热点,应通过 profile 和可复现基准确认。本文没有提供跨框架性能排行。

资料、版本与本次验证 ​

运行环境是 Go 1.27.2、Linux/amd64。实际 test/race/vet 与 go mod verify 通过,覆盖 chi 路由和标准中间件、validator 正负例、Connect/gRPC/gRPC-Web 一元调用,以及生成模型不执行 pattern 的反例。swag 和 oapi-codegen 均已实际生成,完整输入与生成结果放在下面。Gin/Echo/Fiber 在本文做源码与接口核对,未运行其端到端或性能实验;grpc-go 四种模式的独立实验见《RPC 与 gRPC》。

完整代码、配置和复现命令 ​

先保存下面文件,保持 contract 子目录。运行测试不要求重新生成模型,models.gen.go 已完整附上。首次下载依赖后,可离线测试:

sh
GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org go mod download
go mod verify
GOPROXY=off GOSUMDB=off go test -v -count=1 ./...
GOPROXY=off GOSUMDB=off go test -race -count=1 ./...
GOPROXY=off GOSUMDB=off go vet ./...

重新生成模型的实际命令,固定生成器版本:

sh
GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org \
  go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.4.1 \
  --config contract/oapi.yaml contract/openapi.yaml

也可以运行附带的 generate.sh,同时重新生成模型与 Swagger 文档:

sh
GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org sh generate.sh

生成命令需要访问模块元信息;本次尝试完全关闭代理时,go run 工具@版本 因缺少 deprecation 查询结果失败,随后用上述显式代理命令重新生成成功。测试本身在关闭代理后通过。脚本给固定版本 swag 生成的 JSON 补一个文件末尾换行,以便生成结果逐字节比较。Buf 文件是配置示意,本次未执行。使用时需要自行固定 CLI 版本并指定真实历史基线,不能从本文推断它已通过你项目的兼容检查。

go.mod ​

go.mod
module example.com/webapi/07_tools

go 1.27.2

require (
	github.com/go-chi/chi/v5 v5.2.3
	github.com/go-playground/validator/v10 v10.27.0
)

require (
	connectrpc.com/connect v1.18.1 // indirect
	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
	github.com/go-playground/locales v0.14.1 // indirect
	github.com/go-playground/universal-translator v0.18.1 // indirect
	github.com/leodido/go-urn v1.4.0 // indirect
	golang.org/x/crypto v0.33.0 // indirect
	golang.org/x/net v0.34.0 // indirect
	golang.org/x/sys v0.30.0 // indirect
	golang.org/x/text v0.22.0 // indirect
	google.golang.org/protobuf v1.36.6 // indirect
)

go.sum ​

text
connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=

tools_test.go ​

代码块 · 46 行 · 点击收起 / 展开
go
package toolsdemo

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/go-chi/chi/v5"
	"github.com/go-playground/validator/v10"
)

func app() http.Handler {
	validate := validator.New(validator.WithRequiredStructEnabled())
	r := chi.NewRouter()
	r.Use(func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("X-Standard-Middleware", "yes")
			next.ServeHTTP(w, r)
		})
	})
	r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
		in := struct {
			ID string `validate:"required,number,len=3"`
		}{ID: chi.URLParam(r, "id")}
		if err := validate.Struct(in); err != nil {
			http.Error(w, "id must contain 3 digits", 422)
			return
		}
		w.WriteHeader(204)
	})
	return r
}
func TestStandardHandlerAndValidation(t *testing.T) {
	h := app()
	for _, tc := range []struct {
		path string
		want int
	}{{"/users/123", 204}, {"/users/abc", 422}, {"/users/-12", 422}, {"/users/+12", 422}, {"/users/1.2", 422}, {"/users/12", 422}, {"/missing", 404}} {
		w := httptest.NewRecorder()
		h.ServeHTTP(w, httptest.NewRequest("GET", tc.path, nil))
		if w.Code != tc.want || w.Header().Get("X-Standard-Middleware") != "yes" {
			t.Fatalf("%s status=%d header=%v", tc.path, w.Code, w.Header())
		}
	}
	t.Log("chi is an http.Handler; standard middleware wraps routes; validator rejects invalid path values")
}

teaching_test.go ​

代码块 · 71 行 · 点击收起 / 展开
go
package toolsdemo

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"connectrpc.com/connect"
	"github.com/go-playground/validator/v10"
	"google.golang.org/protobuf/types/known/wrapperspb"
)

func Example_validatorNames() {
	v := validator.New(validator.WithRequiredStructEnabled())
	for _, s := range []string{"123", "-12", "+12", "1.2", "abc"} {
		fmt.Printf("%s numeric=%v number=%v\n", s, v.Var(s, "required,numeric,len=3") == nil, v.Var(s, "required,number,len=3") == nil)
	}
	// Output:
	// 123 numeric=true number=true
	// -12 numeric=true number=false
	// +12 numeric=true number=false
	// 1.2 numeric=true number=false
	// abc numeric=false number=false
}
func TestConnectAndGRPCOnHTTP(t *testing.T) {
	const path = "/demo.v1.Greeter/Hello"
	h := connect.NewUnaryHandler(path, func(ctx context.Context, r *connect.Request[wrapperspb.StringValue]) (*connect.Response[wrapperspb.StringValue], error) {
		if r.Msg.Value == "" {
			return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("name required"))
		}
		return connect.NewResponse(wrapperspb.String("hello " + r.Msg.Value)), nil
	})
	mux := http.NewServeMux()
	mux.Handle(path, h)
	server := httptest.NewUnstartedServer(mux)
	server.EnableHTTP2 = true
	server.StartTLS()
	defer server.Close()
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	for _, mode := range []struct {
		name    string
		options []connect.ClientOption
	}{{"Connect", nil}, {"gRPC", []connect.ClientOption{connect.WithGRPC()}}, {"gRPC-Web", []connect.ClientOption{connect.WithGRPCWeb()}}} {
		c := connect.NewClient[wrapperspb.StringValue, wrapperspb.StringValue](server.Client(), server.URL+path, mode.options...)
		result, err := c.CallUnary(ctx, connect.NewRequest(wrapperspb.String("Go")))
		if err != nil || result.Msg.Value != "hello Go" {
			t.Fatalf("%s %v %v", mode.name, result, err)
		}
		_, err = c.CallUnary(ctx, connect.NewRequest(wrapperspb.String("")))
		if connect.CodeOf(err) != connect.CodeInvalidArgument {
			t.Fatal(err)
		}
		t.Logf("%s: %s; empty name=%s", mode.name, result.Msg.Value, connect.CodeOf(err))
	}
}
func TestGeneratedModelDoesNotValidatePattern(t *testing.T) {
	u := User{Id: "bad-value"}
	b, err := json.Marshal(u)
	if err != nil {
		t.Fatal(err)
	}
	if string(b) != `{"id":"bad-value"}` {
		t.Fatal(string(b))
	}
	t.Log("generated model accepts:", string(b))
}

models.gen.go ​

go
// Package toolsdemo provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT.
package toolsdemo

// User defines model for User.
type User struct {
	Id string `json:"id"`
}

generate.sh ​

sh
#!/bin/sh
set -eu
# Run from this module's root. The caller selects GOPROXY/GOSUMDB.
go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.4.1 \
  --config contract/oapi.yaml contract/openapi.yaml
go run github.com/swaggo/swag/cmd/swag@v1.16.6 init \
  -g main.go -d contract/swag -o contract/swag/docs --outputTypes json
# The pinned swag version omits a final newline; normalize the checked-in artifact.
printf '\n' >> contract/swag/docs/swagger.json

contract/openapi.yaml ​

yaml
openapi: 3.0.3
info:
  title: User example
  version: 1.0.0
paths: {}
components:
  schemas:
    User:
      type: object
      required: [id]
      properties:
        id:
          type: string
          pattern: '^[0-9]{3}$'

contract/oapi.yaml ​

yaml
package: toolsdemo
generate:
  models: true
output: models.gen.go
output-options:
  skip-prune: true

contract/swag/main.go ​

go
package main

import "net/http"

// @title User API example
// @version 1.0
// @description Documentation generation fixture.
// @BasePath /v1
func main() {}

// GetUser is a documentation fixture, not a running API.
// @Summary Check a user
// @Param id path string true "User ID"
// @Success 204 "No Content"
// @Failure 404 {string} string "Not Found"
// @Router /users/{id} [get]
func GetUser(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) }

contract/swag/docs/swagger.json ​

代码块 · 37 行 · 点击收起 / 展开
json
{
    "swagger": "2.0",
    "info": {
        "description": "Documentation generation fixture.",
        "title": "User API example",
        "contact": {},
        "version": "1.0"
    },
    "basePath": "/v1",
    "paths": {
        "/users/{id}": {
            "get": {
                "summary": "Check a user",
                "parameters": [
                    {
                        "type": "string",
                        "description": "User ID",
                        "name": "id",
                        "in": "path",
                        "required": true
                    }
                ],
                "responses": {
                    "204": {
                        "description": "No Content"
                    },
                    "404": {
                        "description": "Not Found",
                        "schema": {
                            "type": "string"
                        }
                    }
                }
            }
        }
    }
}

buf.yaml ​

yaml
version: v2
modules:
  - path: proto
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE

proto/users/v1/users.proto ​

proto
syntax = "proto3";
package users.v1;
option go_package = "example.com/webapi/gen/users/v1;usersv1";
message User { string id = 1; }
message GetUserRequest { string id = 1; }
message GetUserResponse { User user = 1; }
service UserService {
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
}

保留各书来源与许可 · 示例运行条件以正文为准