rgoussu@goussu: ~/library/go/web-frameworks
~/library/go/web-frameworks cat stdlib-correspondence.md

Frameworks × the stdlib contracts — who honors what

# Contract-by-contract table of how chi, Gin, Echo, and Fiber honor, wrap, or replace Go's stdlib interfaces, and why compatibility pays.

Conceptsaved 2026-08-09updated 2026-08-11 #go#web#frameworks#http#interoperability#middleware

Overview

Java's framework question is "which Jakarta EE / MicroProfile specs does this stack implement?" (the correspondence table). Go has no external spec body — the stdlib interfaces are the contracts: http.Handler is the servlet spec, context.Context the propagation spec, encoding/json the default codec. A framework's compatibility posture is therefore a per-contract question: does it honor the stdlib interface (everything composes), wrap it (composes at the edges), or replace it (composes only through shims)? This note tabulates that for chi, Gin, Echo, and Fiber.

Key points

  • chi is pure stdlib: every row is a ✔ — it adds routing ergonomics without touching a single contract, which is its entire value proposition.
  • Gin and Echo wrap: own Context types inside, ServeHTTP-compatible engines outside. Stdlib middleware needs adapters (Echo ships them; Gin mostly leaves it to you).
  • Fiber replaces: fasthttp underneath means ✘ on nearly every contract, with the adaptor shim as the only (lossy, allocating) bridge.
  • The contract that matters most is func(http.Handler) http.Handler middleware interop — it is how the observability ecosystem (otelhttp, promhttp) ships.
  • Cross-cutting libraries converge anyway: go-playground/validator for validation and log/slog for logging appear across all four stacks, softening the divergence above the HTTP layer.
  • The 1.22 ServeMux raised the bar for wrapping: with method + wildcard routing in the stdlib, a framework's own router must now justify itself on ergonomics alone — contract fidelity is the tiebreaker.

Details

Contract-by-contract

✔ honored natively · ≈ wrapped/adapted · ✘ replaced

Contract chi Gin Echo Fiber
http.Handler / http.HandlerFunc ✔ handlers are stdlib handlers ≈ own gin.HandlerFunc; gin.Engine implements ServeHTTP ≈ own echo.HandlerFunc; *echo.Echo implements ServeHTTP fiber.Handler over fasthttp; adaptor shim only
context.Context propagation r.Context() untouched gin.Context implements it, but keeps a parallel Set/Get bag beside Request.Context() ≈ stdlib context via c.Request().Context(); own value store beside it ✘ fasthttp's RequestCtx; c.UserContext() bolts a context.Context on
net/http middleware (func(http.Handler) http.Handler) ✔ native chain ≈ hand-written adapters required echo.WrapMiddleware provided ✘ only via adaptor, per-request conversion cost
encoding/json ✔ bring your own encoding ≈ default renderer (swappable build tags for sonic/go-json) ≈ default binder/renderer, swappable JSONSerializer ≈ default, swappable JSONEncoder/JSONDecoder (goccy et al.)
html/template ✔ use it directly LoadHTMLGlob wraps it ≈ pluggable Renderer interface, stdlib templates typical ✘ own Views engines (incl. an html/template adapter)
http.Client / transports (outbound) ✔ n/a — nothing imposed ✔ nothing imposed ✔ nothing imposed ≈ ecosystem leans to fasthttp's client; stdlib client still usable
Testing via net/http/httptest ✔ recorder + request, plain ✔ engine is a Handler; gin.CreateTestContext for unit scope e.ServeHTTP(rec, req); e.NewContext for unit scope ✘ no recorder path; app.Test(req) converts an *http.Request internally

Cross-cutting choices

Concern The convergent answer Per-framework notes
Validation go-playground/validator everywhere Gin bundles it behind binding: tags; Echo and chi wire it via a hook/manual call; Fiber docs recommend the same library
Logging log/slog since it landed in the stdlib Middleware adapters per framework (chi middleware, gin-contrib, Echo's RequestLogger, Fiber logger); zap/zerolog remain common behind slog handlers
Metrics & tracing promhttp + otelhttp/OpenTelemetry Native http.Handler wrapping on chi/stdlib; contrib middleware (otelgin, otelecho) re-implement it per framework; Fiber needs otelfiber + adaptor for promhttp

The payoff

Staying handler-compatible is compounding interest: every func(http.Handler) http.Handler ever written — otelhttp.NewHandler, promhttp.Handler, auth proxies, rate limiters — works on chi/stdlib services unchanged, on day one, with no per-framework contrib package to wait for. Wrapped frameworks (Gin, Echo) get there via maintained adapters and duplicate contrib middleware; Fiber's replaced stack gets each integration only when someone ports it. The stdlib contracts are Go's answer to the JVM's spec portability — honoring them is what keeps a service's middleware options open for the next decade.

Related