REF / WRITING · SOFTWARE

Go HTTP Services in 2026: net/http vs Gin, Echo, Chi, Fiber

Go 1.22 supercharged net/http. The framework-vs-stdlib calculus has changed - when each Go HTTP framework still earns its keep.

DomainSoftware
Formatessay
Published7 Jan 2025
Tagsgo · golang · net-http

For years, the standard advice for Go HTTP services was "use Chi" or "use Gin". anything to escape net/http's missing features. The standard library couldn't do path parameters, methods routing was awkward, middleware composition was painful. Frameworks closed those gaps.

Go 1.22 changed the calculus. The standard library now does method-aware routing and path parameters natively. The case for a framework is genuinely weaker than it was. But "weaker" is not "gone". and the choice still matters for production services.

What Go 1.22 Actually Added

Before 1.22:

// Pre-1.22 - works but ugly
mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodGet {
        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
        return
    }
    id := strings.TrimPrefix(r.URL.Path, "/users/")
    // ...
})

After 1.22:

// Post-1.22 - first-class
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // ...
})

mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
    // ...
})

This is what every Go web framework was built to provide. With net/http in 1.22+, you don't need a framework for routing and method handling. For 80% of services, that's the entire reason most teams reached for one.

When the Stdlib Is Now Enough

Three scenarios where I now reach for plain net/http in 2026:

  1. Internal services with simple routing. A REST API with 20 endpoints, JSON in/out, no exotic content negotiation. The stdlib does the job in 200 lines.
  2. Services that prioritise minimal dependencies. Compliance-sensitive deployments, embedded use, or just teams that value the audit-friendly "we depend on the standard library, full stop."
  3. Services where you want to understand every request lifecycle. No middleware magic, no framework surprises in the trace.

A complete production-shaped stdlib service:

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /health", health)
    mux.HandleFunc("GET /users/{id}", getUser)
    mux.HandleFunc("POST /users", createUser)

    handler := chain(mux, logging, recovery, requestID)

    srv := &http.Server{
        Addr:              ":8080",
        Handler:           handler,
        ReadHeaderTimeout: 5 * time.Second,
        IdleTimeout:       120 * time.Second,
    }

    log.Fatal(srv.ListenAndServe())
}

type middleware func(http.Handler) http.Handler

func chain(h http.Handler, mws ...middleware) http.Handler {
    for i := len(mws) - 1; i >= 0; i-- {
        h = mws[i](h)
    }
    return h
}

func logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        slog.Info("request", "method", r.Method, "path", r.URL.Path, "duration", time.Since(start))
    })
}

Routing, method matching, middleware composition, structured logging. all in the standard library. No framework required.

When Each Framework Still Earns Its Keep

Chi: the framework I'd still pick when the stdlib isn't quite enough.

Chi's value over the stdlib in 2026:

  • Cleaner subroute composition (r.Route("/api", func(r chi.Router) { ... }))
  • Built-in middleware library (rate limiting, request ID, real IP, recoverer, throttle)
  • Mountable subrouters for plugin-style architectures

If you have a service with 100+ endpoints organised into multiple subroutes, Chi's routing API is more pleasant to read than nested stdlib mux composition. For a service with 20 endpoints, the stdlib is fine.

Gin: performance and a richer rendering layer.

Gin's value:

  • Fast (its routing is optimised; for high-RPS services it matters)
  • Built-in JSON binding with validation tags
  • Render helpers (c.JSON(), c.XML(), c.YAML())
  • Larger middleware ecosystem

The cost: it's an opinionated framework with its own context type (*gin.Context), which means your handlers don't compose with stdlib http.Handler patterns. Once you're in Gin, your code is in Gin.

For a high-RPS public API where the routing layer needs to be measurably fast and you don't mind the framework lock-in, Gin is a defensible choice.

Echo: middle-ground between Chi and Gin.

Echo gives you Gin's ergonomics with a slightly cleaner middleware story. Its biggest practical advantage is the per-handler error return signature, which encourages cleaner error handling than Gin's "set status and return" pattern.

e.GET("/users/:id", func(c echo.Context) error {
    id := c.Param("id")
    user, err := userRepo.Get(c.Request().Context(), id)
    if err != nil {
        return err  // central error handler picks it up
    }
    return c.JSON(http.StatusOK, user)
})

If you're choosing between Gin and Echo and don't have a strong reason for either, Echo's error handling is the cleaner default.

Fiber: a different category.

Fiber is built on fasthttp instead of net/http. It's the fastest Go HTTP framework on benchmarks, often by 2-3×. The cost is significant: fasthttp is incompatible with net/http's ecosystem. Standard middleware doesn't work. Standard observability instrumentation doesn't work. You're in a parallel universe.

For most services, the throughput advantage doesn't matter. the bottleneck is your database or external API, not the HTTP layer. Fiber is the right choice only when the HTTP layer is genuinely your bottleneck and you've measured it.

The Decision in Practice

For a new Go service in 2026:

  • Default to net/http for services with up to ~50 endpoints, simple routing, and standard requirements.
  • Reach for Chi when you want subrouter composition, more built-in middleware, or a slightly nicer routing API while staying close to the stdlib.
  • Reach for Echo when you want cleaner error handling and rendering helpers without going to a fully opinionated framework.
  • Reach for Gin for high-RPS public APIs where performance and a richer ecosystem matter.
  • Reach for Fiber only when measured throughput is the bottleneck and you've accepted the tradeoffs.

The Go 1.22 routing changes shifted the default from "always pick a framework" to "start with the standard library and add a framework when you have a measured reason." For most services, that reason never arrives.

What Doesn't Change

Whatever HTTP layer you pick, the production discipline is the same:

  • Always set ReadHeaderTimeout (defends against Slowloris attacks)
  • Always set IdleTimeout (prevents connection accumulation)
  • Always wrap your handlers with a recovery middleware (a panic in one handler shouldn't take down the process)
  • Always add a structured logger with request ID propagation
  • Always integrate with OpenTelemetry for traces

These are the things that make a Go HTTP service production-grade. The choice of framework is at most a 10% factor; the discipline is the other 90%.