Skip to content

Go Web Frameworks in 2026: What I Actually Reach For

Go Web Frameworks in 2026: What I Actually Reach For

Confession: I once spent a full Saturday switching a side project from chi to Echo, then to Gin, then back to chi. It taught me one real thing about Go web frameworks, and it wasn’t anything about Go web frameworks. It was that I should pick one and ship the feature instead of yak-shaving the router for the third time.

So this is the post I wish I’d had that Saturday. Four frameworks I keep an eye on, when I reach for each, and which ones I now skip without guilt. No “ultimate guide” energy, no benchmark wars. Just what I actually use in 2026 to ship Go HTTP services I’m not embarrassed to hand to a teammate.

If you’re impatient: I default to the standard library plus chi for almost everything new. Gin is still my pick when I want batteries included and have to move fast. Echo is a fine middle ground I rarely choose anymore. Fiber I respect but don’t ship. Read on for the why.

The shortlist I actually consider

There are roughly forty Go HTTP frameworks if you count GitHub stars. I consider four:

  • The standard library net/http (with the Go 1.22+ routing improvements)
  • chi, as a thin layer over net/http for ergonomic routing and middleware
  • Gin, when I want a batteries-included framework and don’t care about going off the well-trodden path
  • Echo, as Gin’s quieter cousin

Then I keep Fiber on the radar without using it, and I avoid the rest. That includes Beego, Buffalo, Iris, Revel, and the long tail of half-maintained projects. Not because they’re bad. They’re just not where the Go community is putting time, and that matters more than benchmark numbers when you’re going to be reading their source on a Tuesday at 11pm.

Standard library plus chi: my default for new services

When I’m starting a new HTTP service in 2026, I almost always begin with net/http plus chi. The Go 1.22 routing improvements (method-aware patterns, path wildcards) closed the most painful gap in net/http, but chi still gives me a few things I value enough to keep it as a small dependency: route groups, sub-routers, and a middleware chain that doesn’t make me sad.

A typical setup looks like this:

package main

import (
    "net/http"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
)

func main() {
    r := chi.NewRouter()
    r.Use(middleware.RequestID)
    r.Use(middleware.RealIP)
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)

    r.Route("/v1", func(r chi.Router) {
        r.Get("/health", healthHandler)
        r.Route("/users", func(r chi.Router) {
            r.Get("/", listUsers)
            r.Post("/", createUser)
            r.Get("/{id}", getUser)
        })
    })

    http.ListenAndServe(":8080", r)
}

What I like: the r.Route and r.Group calls let me apply auth middleware to a subtree without writing my own composer. Handler signatures stay func(w http.ResponseWriter, r *http.Request), which means any third-party middleware you find on the internet just plugs in.

Compare that to the older standard-library approach before 1.22, since Go itself has changed since then. Here’s roughly what I used to write:

mux := http.NewServeMux()
mux.HandleFunc("/v1/users", func(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        listUsers(w, r)
    case http.MethodPost:
        createUser(w, r)
    default:
        w.WriteHeader(http.StatusMethodNotAllowed)
    }
})
mux.HandleFunc("/v1/users/", func(w http.ResponseWriter, r *http.Request) {
    id := strings.TrimPrefix(r.URL.Path, "/v1/users/")
    if id == "" {
        http.NotFound(w, r)
        return
    }
    getUser(w, r, id)
})

That worked, but I wrote a path-extraction helper four times across four projects before I learned my lesson and added chi. The Go 1.22 update lets you write mux.HandleFunc("GET /v1/users/{id}", getUser) and pull r.PathValue("id"), which is a real improvement. For tiny services I’ll skip chi now. For anything with more than five routes and a middleware chain, chi still costs less code over time.

Gin: where it still wins for me

Gin is the framework I reach for when I’m building an internal tool quickly and I want JSON binding, validation, and a c.JSON(200, gin.H{...}) shortcut without thinking about it. It’s the closest thing Go has to Express in terms of “I have an idea, let me get it on the air today.”

package main

import "github.com/gin-gonic/gin"

type CreateUser struct {
    Email string `json:"email" binding:"required,email"`
    Name  string `json:"name" binding:"required,min=2"`
}

func main() {
    r := gin.Default()

    r.POST("/users", func(c *gin.Context) {
        var in CreateUser
        if err := c.ShouldBindJSON(&in); err != nil {
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        c.JSON(201, gin.H{"id": "u_123", "email": in.Email})
    })

    r.Run(":8080")
}

What I still like about Gin in 2026 is mostly the validator. Struct-tag binding is built in, so I don’t reach for go-playground/validator separately and wire it up myself. The gin.Context is also a known shape that the rest of my team already understands. And the middleware ecosystem is enormous, so I rarely have to write a CORS or auth middleware from scratch.

Where Gin loses me: the gin.Context is its own thing, not http.Request. When I need to plug into httptest, write something that takes plain http.Handler, or share middleware with a non-Gin service, I end up writing adapters. That cost is small for one service and annoying when I have five.

Echo: when I reach for it

Echo is Gin’s quieter cousin. Similar feel, slightly cleaner middleware API, tighter standard library. I used it heavily in 2022 and 2023, then drifted away because the two frameworks are close enough that picking the more popular one (Gin) was the easier call when working with juniors who’d Google for examples.

Where Echo still beats Gin for me: the validator integration feels less hacked-on, and the typed error handling (c.NoContent, c.String, echo.NewHTTPError) is a cleaner API. If I were starting a brand new team and had to pick one of the two, I might pick Echo. With my actual team, who already knows Gin, I haven’t found a strong reason to switch. The upgrade story is also a real consideration; both have shipped breaking changes I had to chase.

Fiber: I keep wanting to love it

Fiber is built on fasthttp instead of net/http. The pitch is performance. Fiber benchmarks well, and the API borrows from Express in a way that feels familiar if you came from Node.

I keep wanting to love it. I haven’t shipped it.

The reason is simple. fasthttp isn’t compatible with the standard library’s http.Handler interface, which means a huge chunk of the Go ecosystem (any middleware, observability library, or test helper that takes an http.Handler) doesn’t work without an adapter. For most services I write, the throughput difference doesn’t matter. The interop cost does.

If I were building something where the throughput honestly matters at p99 and the team is small enough to own every piece end to end, I’d revisit Fiber. So far that’s been zero of my projects.

What I’d skip

A few frameworks I’ve explicitly stopped considering, with reasons:

  • Beego: too much “we are a full MVC framework like Rails.” That’s not what Go is for, and the magic ends up costing more than it saves.
  • Buffalo: similar story. Asset pipeline included. I want a router, not a build system.
  • Iris: the licensing controversy and community vibe scared me off years ago and I haven’t found a reason to return.
  • Revel: feels like it’s frozen in 2017.

None of these are unusable. I just don’t want to be the engineer on a team explaining why we picked the unusual one when chi or Gin would do.

How I actually decide

A small mental flowchart, written out so it’s not insufferable:

  • Is this a five-route service or smaller? Standard library, Go 1.22+ routing.
  • Is this a longer-lived API with middleware chains and route groups? chi.
  • Do I want JSON binding, validation, and convenience helpers, and do I value moving fast over interop? Gin.
  • Am I starting fresh with a team that has no preference and I want a slightly cleaner API than Gin? Echo.
  • Am I building something where every microsecond matters at p99 and I own the whole stack? Consider Fiber, but plan for the interop tax.

If my answer to all of those is “I’m not sure”, I write it with net/http and chi. That’s the choice I’ve regretted least.

For more on how I think about Go service code in general, I covered the concurrency patterns I actually ship in an earlier post. The framework choice matters less than getting goroutines and graceful shutdown right. And if you’re trying to figure out whether Go is the right language for your next service at all, my comparison of Rust and Go digs into that question.

One thing to do this week

Pick a small idea you’ve been meaning to build. A webhook receiver, or a tiny dashboard backend. Write it twice: once with net/http and the Go 1.22 routing, once with chi. Not Gin. The point is to feel where the standard library’s recent improvements close the gap and where they don’t. You’ll come out with a much better instinct for when reaching for a framework actually saves you time.

Most of the framework debates online are a proxy for “what does my team already know.” If you write the same service twice in a weekend, you’ll have your own answer faster than any benchmark thread will give you.

If you’re hiring a Go-leaning engineer for a small team and want to talk about how I think about service architecture, I keep notes about the kind of work I take on over here.