Go HTTP Frameworks: Routers vs Full Frameworks
Go's standard library already ships a capable HTTP server.
Search across all documentation pages
Go's standard library already ships a capable HTTP server.
Third-party libraries split into routers that compose with net/http and full frameworks that add request context wrappers, binding, and batteries-included middleware.
Knowing which camp a library belongs to tells you how portable your handlers are and how much framework API leaks into business logic.
http.Handler; full frameworks (Gin, Echo) replace the handler signature with a framework-specific context type.http.Handler, middleware chain, route matching, context wrapper, binding/validation, stdlib compatibility.ServeMux (Go 1.22+) with light wrappers.net/http servers and middleware, JSON serialization, handler testing with httptest, and adapter patterns for migration.Every Go HTTP server ultimately calls func(w http.ResponseWriter, r *http.Request).
The standard library defines that contract in http.Handler.
Libraries that respect it are stdlib-compatible: your route functions are plain handlers or http.HandlerFunc, and middleware is typically func(http.Handler) http.Handler.
Routers add a route tree and parameter extraction without changing the handler signature.
chi registers patterns like /users/{id} and exposes chi.URLParam(r, "id").
gorilla/mux adds regex constraints, host matching, and method-scoped subrouters while still ending in http.Handler.
Full frameworks introduce a context wrapper - *gin.Context or echo.Context - that embeds or mirrors http.Request and http.ResponseWriter plus helpers (BindJSON, JSON, Param, Query).
Handlers become func(c *gin.Context) or func(c echo.Context) error.
That wrapper is ergonomic for JSON APIs but is not http.Handler, so you need adapters to mix framework routes with stdlib middleware or mount on foreign routers.
Client request
|
v
net/http Server
|
+-- Router (chi/mux): middleware(http.Handler) -> route handler(w,r)
|
+-- Framework (Gin/Echo): global middleware -> framework context -> handler(c)
Middleware runs as nested handlers.
On chi, r.Use(logging) wraps every matched route; outer middleware sees the request first inbound and last outbound.
Gin and Echo register middleware against the engine with framework-specific Next() semantics, but the idea is the same: cross-cutting concerns before the route handler.
Route matching differs in expressiveness and performance.
chi uses a radix tree tuned for common REST paths.
gorilla/mux compiles richer patterns (regex, headers, hosts) at registration time.
Gin and Echo embed routers (Gin uses httprouter-derived matching; Echo has its own tree) optimized for API-style paths.
None of this replaces the Go scheduler or GC; differences show up in allocation per request and how much reflection binding costs.
// Router style: stdlib handler throughout
r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
// ...
})
// Framework style: context wrapper
func getUser(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"id": id})
}Binding and rendering are where full frameworks pull ahead for JSON services.
Gin and Echo decode JSON/query/path into structs with tags and run validation hooks.
Routers leave encoding to encoding/json, third-party validators, or your own DTO layer - more boilerplate, clearer boundaries.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
stdlib ServeMux (1.22+) | Zero deps, stable patterns | Fewer grouping and param helpers | Small services, embedded admin APIs |
| chi / mux | Portable handlers, explicit control | Manual JSON/validation wiring | Libraries, gateways, teams standardizing on http.Handler |
| Gin | Fast JSON CRUD, large ecosystem | gin.Context lock-in, global mode footguns | High-throughput REST APIs |
| Echo | Balanced middleware, WebSocket helpers | Smaller community than Gin | APIs needing WS and structured middleware |
Testing stays simplest with http.Handler: httptest.NewRecorder and httptest.NewRequest exercise handlers without booting a framework engine.
Framework handlers need router/engine setup or test helpers that construct a context.
Observability should live in middleware either way.
OpenTelemetry HTTP instrumentation often expects http.Handler; adapters exist for Gin and Echo but add a translation layer.
Migration between routers is cheap if handlers are http.HandlerFunc and path params are read through a thin helper.
Moving off Gin/Echo means rewriting handlers or maintaining adapter shims per route.
net/http plus chi or mux is production-grade; frameworks mainly reduce repetitive JSON and routing ceremony.net/http; gains come from routing data structures and reduced allocations in hot paths, not magic outside the HTTP stack.http.Handler at the edge.ServeMux cover most new router needs; mux remains valid for regex/host rules.context.Context" - Request-scoped cancellation and deadlines still flow through r.Context(); framework wrappers should forward that, not replace it.A router matches URLs and chains http.Handler middleware.
A full framework changes the handler signature and bundles JSON binding, validation, and response helpers on a custom context type.
Not cleanly on one route tree.
Mount one as a sub-http.Handler on the other (r.Mount("/gin", ginEngine)) and keep path prefixes explicit.
Often yes for small services.
Reach for chi when you want route groups, middleware stacks, or cleaner param access without a framework context.
Binding tags, grouped routes, and built-in JSON rendering reduce boilerplate for CRUD endpoints at the cost of gin.Context coupling.
When you need regex path constraints, host-based routing, or header/query matchers declared declaratively on the route.
They prefer native middleware, but many apps wrap stdlib middleware with adapter functions at the engine edge.
Keep business logic in plain functions/services; let the framework context stop at the handler boundary so core code stays router-agnostic.
Echo documents first-class WebSocket helpers; chi and mux work with golang.org/x/net/websocket or third-party packages with more wiring.
Not directly - scaling is about statelessness, DB pools, and deployment.
Framework overhead is usually noise compared to I/O and serialization.
Default to stdlib or chi until JSON binding volume justifies Gin/Echo; avoid choosing a framework for routing alone.
Benchmark your real handlers with httptest or load tools; micro-benchmarks of hello-world routes rarely predict API workloads dominated by DB and JSON size.
Decode with json.Decoder, validate with go-playground/validator or hand-rolled checks in a DTO package - explicit, but under your control.
http.Handler chaining modelStack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 19, 2026