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
Contexttypes 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
adaptorshim as the only (lossy, allocating) bridge. - The contract that matters most is
func(http.Handler) http.Handlermiddleware interop — it is how the observability ecosystem (otelhttp, promhttp) ships. - Cross-cutting libraries converge anyway: go-playground/validator for validation and
log/slogfor 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
- Go web frameworks — the landscape — parent overview of the frameworks compared here.
- net/http deep dive — the contracts themselves, in depth.
- Gin deep dive — the "wraps" column in detail.
- Echo deep dive — the other wrapper, with shipped adapters.
- Fiber deep dive — the "replaced" column and its costs.
- REST in Go — where these interop choices bite in API services.
- Frameworks × Jakarta EE — who implements what — the JVM's version of the same question, against external specs.
- Frameworks × tower/hyper — who builds on the contracts — Rust's version, against community-crate contracts.
- Structured logging & MDC — the
log/slogconvergence, in observability terms. - Correlation IDs & context propagation —
context.Contextas the propagation contract the frameworks honor or shim. - OpenTelemetry —
otelhttpis thefunc(http.Handler) http.Handlerinterop payoff in practice.