Backend · Go · October 2026

Which Go framework should you choose when none is the obvious choice?

You’re starting a backend in Go, and everyone recommends a different tool. In Java or Python, the question would be all but settled in advance. In Go, it’s still open. Here’s how to decide for your situation, and why the criterion that matters most isn’t the one people usually look at.

Written for
  • tech leads
  • devs

Ask a Java team what they start a new backend with, and the answer usually fits in two words: Spring Boot. In Python, it’ll be Django or FastAPI; in Ruby, Rails; in PHP, Laravel or Symfony; in C#, ASP.NET Core. Serious alternatives exist everywhere, but they define themselves against the big name.

Ask Go developers the same question, and you’ll get five answers. One of them will almost always be “you don’t need a framework”. The others will name Gin, the best known, Echo, Chi, Fiber, and more recently Huma or Encore. To understand this diversity, and to choose, we need to start at the beginning: what a backend does.

This article describes the ecosystem as it stands at the end of October 2026: Go 1.27, Chi v5, Gin 1.12, Echo v5, Fiber v3, Huma v2. The broad strokes will hold for a long time, because they rest on the standard library, which changes slowly. Frameworks, on the other hand, ship new major versions: check the one you adopt.
Part 1 · backend

What a backend does, and what a framework brings to it

A backend application has one main job: receive requests, process them, and respond. Those requests almost always arrive over HTTP, the protocol of the web: that’s the case for a REST API called by a browser or a mobile app.

Let’s take an example that will follow us through the whole article: Notebook, a small fictional service written in Go that manages notebooks. A client asks it for notebook no. 42. Let’s follow that request, from its arrival to the response:

The clienta browser, a mobile app: it sends a request, then waits for the response.
the requestGET /notebooks/42 the response200 OKand the notebook as JSON
The backendthe Notebook program, written in Go: the language is the frame for everything else
  1. 1HTTP serveraccepts the connection and decodes the HTTP requeststandard library
  2. 2Routerpicks the function that will respond, based on the method and the pathstandard library
  3. 3Middlewareprocessing shared by every request: logging, authentication…framework, optional
  4. 4Handlerthe function that responds: reads the request, calls the business logic, writes the responseyour code
  5. 5Business logicthe application’s rules, the databaseyour code
1 to 3: the plumbing, the same for every application4 and 5: what makes your application yours
your codeframeworkstandard librarylanguage, the frame
The request goes through the five steps, then the response heads back to the client. The colors show who provides each step in a Go backend; they’re the same in every diagram in the article.

Steps 1 to 3 look much the same from one application to the next, and nobody wants to rewrite them every time. That’s the framework’s job:

Definition

A framework is a set of ready-made tools that takes care of an application’s plumbing (here, steps 1 to 3) and imposes a way of organizing the rest. It doesn’t change the language: it’s code written in that language, which you add to your project.

And yet, in the diagram, only one block has the framework’s color, and it’s dashed. That’s because in Go, the standard library, shipped with the language, already handles most of the plumbing. Let’s compare with Java, where the framework does everything:

your codeframeworkstandard librarylanguage
Same layers, same colors: only the size changes. In Java, the framework handles all the plumbing, HTTP server included. In Go, the standard library already handles most of it: the framework is just a thin layer, and an optional one. The numbers refer to the steps of the journey.

=== RUN TestWhatABackendDoes --- PASS: TestWhatABackendDoes (part 1)

A backend is five steps, and a framework takes care of the plumbing. In Go, the standard library already does most of it. That’s no accident: it’s a philosophy.

Part 2 · Go’s philosophy and history
Part 2 · philosophy

Go’s philosophy and history

The absence of a big framework in Go is neither a delay nor an accident. It follows from choices made when the language was designed: a culture wary of magic, and a standard library trusted with the essentials. Two recent events reinforced them.

A culture wary of magic

Go was designed at Google for large codebases maintained by many people, with one obsession: code should read without surprises.

No magic

No annotations that generate behavior, no inheritance: you read the journey of a request from top to bottom.

Dependencies passed by hand

No injection container in the standard library: you pass what you need through constructors.

The Go 1 compatibility promise

A program written for Go 1.0 still compiles today: the standard library is the safest long-term bet.

A little copying is better than a little dependency.

Rob Pike, one of Go’s creators, in his “Go Proverbs”, 2015

Everything that makes Spring powerful, convention-based configuration, annotations, automatic injection, is precisely what this culture looks at with suspicion. A Go framework has to justify every line it adds.

Betting everything on the standard library

This culture has a direct consequence: rather than leaving the essentials to the ecosystem, Go ships them with the language. Elsewhere, the standard library can speak HTTP, but nobody trusts it with production: Python’s documentation says so in plain words about its http.server module, and in Java the server has historically come from Tomcat or Jetty. In Go, the standard library covers almost everything a backend uses day to day:

net/httpserver, client, router, static files
encoding/jsonreading and writing JSON
encoding/json/v2the new JSON, the default since Go 1.27
html/templateHTML templates, with context-aware escaping
crypto/tlsTLS, without OpenSSL
database/sqldatabase access, connection pooling
log/slogstructured logging (Go 1.21)
contextcancellation and deadlines, end to end
net/http/httptesthandler tests, without a network
net/http/pprofprofiling in production

net/http, its centerpiece

For a web backend, the centerpiece of this library is net/http, the package that handles steps 1 and 2 of the journey. Three reasons make it the center of gravity of the whole ecosystem:

Ships with Go

Nothing to install: it’s been there since Go 1.0, maintained by the language team and covered by its compatibility promise.

Production-ready

HTTP/1.1 and HTTP/2, timeouts, graceful shutdown, reverse proxy. And since Go 1.22, a real router.

A standard plug

Almost every Go framework is built on it, and a single interface connects the whole ecosystem: http.Handler.

This standard plug deserves a closer look, because everything that follows depends on it. It’s a one-method interface, called at step 4 to make your handler respond:

Standard library, package net/http
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

// A middleware is just a function
// that wraps one Handler in another.
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", "path", r.URL.Path, "duration", time.Since(start))
	})
}

Like a standardized power socket: anything with this shape plugs into everything else, no adapter needed.

The ecosystem’s middleware: CORS, rate limiting, compression, authentication.
Instrumentation: OpenTelemetry’s otelhttp wraps any handler.
Tests: httptest calls a handler without opening a port.
Safeguards: http.MaxBytesHandler, http.TimeoutHandler, and since Go 1.25 http.CrossOriginProtection against CSRF attacks.
Other protocols: a ConnectRPC service, a gRPC server through ServeHTTP, a reverse proxy: all of them are http.Handlers.

A framework that keeps this plug inherits all of that for free. A framework that replaces it has to rewrite these pieces or provide adapters.

Two events that changed the game

December 2022 Gorilla is archived

For lack of maintainers, the Gorilla project goes read-only, and with it gorilla/mux, one of the most widely used routers in the ecosystem. New maintainers took it over in summer 2023, but the lesson stuck.

February 2024 Go 1.22: the router joins the standard library

The standard router learns to tell HTTP methods apart and to read a path parameter ("GET /notebooks/{id}", r.PathValue("id")), with a clear rule when two routes overlap: the most specific one wins.

The first showed, at scale, what the standard plug is worth: since gorilla/mux keeps it, the affected teams could switch routers by rewriting only their route declarations. The second took away third-party routers’ simplest selling point. Since then, “the standard library is enough” is no longer a purist’s stance, but a reasonable option for many services.

In Go, a framework is an assembly

One last difference, the one that confuses people coming from Java the most: Spring Boot covers the whole stack, while a Go web framework almost always covers only the HTTP layer. The rest is chosen piece by piece, whatever the framework.

NeedWith Spring BootIn Go, in the standard libraryIn Go, common libraries
HTTP serverembedded Tomcatnet/httprarely replaced (fasthttp for Fiber)
Routing, middlewareSpring MVC, filtershttp.ServeMux, functions that wrap a handlerChi Gin Echo Fiber…
Input parsing and validationBean Validationencoding/json, checks by handgo-playground/validator, Huma
Configurationapplication.ymlos.Getenv, flagenvconfig, koanf, Viper
Dependency injectionthe Spring containerconstructors, by handuber-go/fx (rare)
Data accessSpring Data, JPAdatabase/sqlpgx, sqlc, GORM, ent
MigrationsFlyway, Liquibase—goose, golang-migrate, Atlas
API documentationspringdoc—Huma, oapi-codegen, swaggo
ObservabilityActuator, Micrometerlog/slog, pprofOpenTelemetry, Prometheus
TestsSpring Testtesting, httptesttestify, testcontainers-go

The middle column is almost full. In Go, “choosing a framework” therefore means choosing the layer that turns an HTTP request into a call to your code, and deciding how far from the standard library to put it. The only exceptions: a few full platforms, such as Encore, which take over much of the table.

=== RUN TestGoPhilosophy --- PASS: TestGoPhilosophy (part 2)

In Go, a framework doesn’t bring you the server: it brings comfort on top of the standard library, at a greater or lesser distance. To compare those distances, and the rest, we need criteria.

Part 3 · The criteria for choosing
Part 3 · criteria

The criteria for choosing a Go framework

To compare tools this different, you need a grid. Eight criteria make it up. The first outweighs all the others, because it follows directly from the language’s philosophy. And the one people always mention first, speed, barely belongs on the list.

  1. 1Distance from the standard library
  2. 2Error handling
  3. 3Input parsing
  4. 4Contract and documentation
  5. 5Observability
  6. 6Maintenance
  7. 7Protocols
  8. 8The team
  9. ✕Raw speed

1Distance from the standard library, the criterion above all

Go bets everything on its standard library, and for the web on net/http. So a Go framework is judged first by what it puts between your code and that library:

Definition

A framework’s distance from the standard library is everything it replaces with its own types and conventions: the way you write a handler, the router, sometimes even the HTTP server.

That distance has a price, which you only pay when you leave:

Consequence

The exit cost is what you’d have to rewrite the day you leave the framework, to go back to the standard library or move to another tool.

That day comes more often than people think:

The tool is no longer maintained A major version breaks your code Your needs change: gRPC, a published contract, performance

How to measure it: the signature

Distance can be read in a single line, at step 4 of the journey: the signature of the handlers, that is, what they take and what they return. The further it strays from the standard plug, http.Handler, the greater the distance. Here’s a handler’s signature in five tools:

func(w http.ResponseWriter, r *http.Request)net/httpChi
func(c *gin.Context)Gin
func(c *echo.Context) errorEcho v5
func(c fiber.Ctx) errorFiber v3
func(ctx context.Context, in *Input) (*Output, error)Huma

The first line has been the standard library’s since Go 1.0. Every type that replaces http.ResponseWriter and *http.Request brings comfort… and ties you to its tool.

Here’s what that changes for two identical projects, one written with Chi, the other with Gin, on the day you leave them:

to rewrite when leavingsurvives
Gin does more for you, and that’s exactly why your code is more tied to it. The business logic survives in both cases, as long as it never imports the framework.
The more distance a tool puts between your code and the standard library, the more it does for you, and the more leaving costs.

That’s not a flaw: it’s a trade, comfort for freedom. But it’s decided on day one, not on the day you leave.

The seven other criteria

Once the distance is measured, you still need to know what the framework gives you in return. Seven criteria tell you.

2

Error handling

The most underrated criterion. A handler that returns an error (Echo Fiber Huma, or a home-made adapter on Chi) centralizes how errors become responses: a uniform JSON format, consistent status codes, logging in one place. A handler that returns nothing forces every developer to think about it, at every if err != nil.

Check: how does a business error become an HTTP response, and where does that code live?

3

Input parsing and validation

Gin and Echo bind the request body to a struct and validate it with tags. Huma goes further and derives validation from the types. The standard library only provides decoding; since Go 1.27, encoding/json runs on the new v2 implementation.

Check: does a malformed request produce a clear error, in the same format everywhere?

4

Contract and documentation

Three approaches coexist: generate the spec from the code (Huma, or comments with swaggo), generate the code from the spec (oapi-codegen ogen), or describe the API in a dedicated language (Goa, Protobuf). If other teams consume your API, this is often the criterion that should pick the framework, not the other way round.

Check: can the documentation drift from the code? If it can, it will.

5

Observability

OpenTelemetry provides otelhttp, which instruments any http.Handler, and otelgin for Gin. Echo’s instrumentation, on the other hand, was removed from OpenTelemetry’s community repository in October 2026 and now lives with Echo; Fiber’s is maintained by the Fiber team. The further you move from the standard library, the more you depend on the framework’s own ecosystem.

Check: who maintains the instrumentation, and does it keep up with the framework’s major versions?

6

Maintenance and governance

Gorilla showed that a heavily used project can stop. httprouter hasn’t released since 2019, Revel since 2022. Conversely, 2026 saw three major versions (Echo v5 Fiber v3 Kratos v3) that require a migration. Go’s compatibility promise doesn’t extend to frameworks: it’s up to you to read their history.

Check: the date of the latest release, the number of active maintainers, how often things break.

7

Protocols

HTTP/2, h2c behind a load balancer, streaming, WebSocket, gRPC on the same port: all of this assumes net/http or an equivalent. fasthttp, Fiber’s engine, doesn’t speak HTTP/2. If your service may one day need gRPC, stay close to the standard library: that’s the case I go through in my article on Go and Rust.

Check: the protocols you’ll need in two years, not just today.

8

The team

The criterion that often settles it, and that people rarely admit. An experienced Go team will be at ease with the standard library or Chi. A team new to Go will be productive faster with Gin or Echo, which have plenty of documentation and examples. A team coming from Node.js will be tempted by Fiber, because it looks like Express: a good reason to try it, not a good enough reason to adopt it.

Check: who will maintain this code in three years, and what those people already know.

What about speed?

Open any comparison of Go frameworks: it starts with a requests-per-second chart. It’s the most visible criterion and, for most projects, the least relevant. The most cited router benchmark, go-http-routing-benchmark, hasn’t been updated since 2020, and the results published in its README were measured with Go 1.3, in 2014. They show that a fast router finds a route in about a hundred nanoseconds, and a slow one in a few microseconds. The difference is real, but it needs putting in perspective.

Diagram of the time spent by a typical API request. Routing is an almost invisible sliver. Middleware, JSON decoding and encoding take small portions. The database query and the network take up most of the bar. Where a typical API request spends its time not to scale: proportions are indicative database, cache, third-party services network routing: what benchmarks measure middleware decoding and validation logic encoding the response
A router is measured in nanoseconds or microseconds, a database query in milliseconds. As long as your service mostly waits on I/O, switching frameworks to save routing time is like lightening the handle of a full suitcase.

The HTTP engine, however, can matter, in one specific case: a server that receives a huge number of small requests and does almost nothing with each (a proxy, a gateway, an event collector). The only way to know is to profile your service, with pprof, under your load. If the HTTP server doesn’t show up at the top of the profile, the framework isn’t your performance problem.

=== RUN TestCriteria --- PASS: TestCriteria (part 3)

Eight criteria, one of which dominates, and speed put in its place. We can finally ask the right question.

Part 4 · The right question
Part 4 · question

The right question

With these criteria in hand, we can rephrase the starting question. The one everyone asks has no answer; the one you should ask has one, different for every project.

The question everyone asks What’s the best Go framework?
The question to ask What does my team need that the standard library doesn’t provide, and what will leaving cost if I change my mind?

The first question has no answer: the rankings that claim to answer it mostly compare microbenchmarks. The second has one, different for every project. I’ll also tell you which one I like best, and why.

Each half of the right question is weighed with the criteria from part 3:

First half · what you gain What does my team need that the standard library doesn’t provide?

It’s weighed with criteria 2 to 8:

  • 2errors
  • 3input
  • 4contract
  • 5observability
  • 6maintenance
  • 7protocols
  • 8team
Second half · what you’ll pay What will leaving cost if I change my mind?

It’s read with criterion 1:

  • 1distance from the standard library
The rule that follows: pick the tool closest to the standard library that covers your needs.

To apply it, seven concrete questions to ask as a team before writing the first route. The answers almost always point to the right candidate:

  1. What doesn’t the standard library give me?criteria 2 to 8Write the list down. If it’s short, the standard library and twenty lines of adapter may be enough.
  2. Who consumes this API?criterion 4Your own apps, or partners? If a published contract is a deliverable, the contract-first tools come before all the others.
  3. Which protocols in two years?criterion 7HTTP/2, gRPC, streaming, WebSocket: anything added later is easier close to the standard library.
  4. Who will maintain this code?criterion 8Not today’s team: the one three years from now. What it already knows, and what it will easily find online.
  5. Who maintains the tool, and how often does it break?criterion 6Date of the latest release, number of active maintainers, history of major versions.
  6. What would I have to rewrite to leave?criterion 1Look at the handler signature. It’s the most honest answer you’ll get.
  7. Does my business code import the framework?criterion 1The answer must be no, whatever tool you pick. It’s the only rule that holds for every candidate.

=== RUN TestTheRightQuestion --- PASS: TestTheRightQuestion (part 4)

The question is asked, the grid is ready. Let’s see how each candidate answers it.

Part 5 · The candidates, one by one
Part 5 · candidates

The candidates, one by one

Seven candidates, ranked from closest to the standard library to furthest away. For each: an introduction, where it stands on the eight criteria, the situations where it fits best, its challenges and risks, and my verdict.

  1. Zero distancenet/http
  2. LowChi
  3. ModerateHuma
  4. HighGinEcho
  5. Very highFiberEncore
Criterion 1, for the seven candidates. The further a tool moves from the standard library, the more it does for you, and the more there will be to rewrite to leave it. Click a name to jump to its profile.

net/http alone

Go 1.27 · no dependencieszero distance

Add nothing: the server, router and testing tools of the standard library. Since Go 1.22, the router handles methods, parameters and wildcards, and refuses to start if two routes overlap without one being more specific: the error shows up at startup, not in production.

func(w http.ResponseWriter, r *http.Request)

On the eight criteria

1 · Distancezero: nothing to rewrite
2 · Errorswritten by hand
3 · Inputdecoding only
4 · Contractnone
5 · Observabilityotelhttp, slog, pprof
6 · Maintenancethe Go team
7 · ProtocolsHTTP/1.1, HTTP/2
8 · Teamconventions to set
Where it fits
  • small services and internal tools;
  • libraries that expose a handler;
  • server-rendered sites, with html/template or templ;
  • the starting point for a gateway, with httputil.ReverseProxy.
Challenges and risks
  • no route groups or per-subtree middleware;
  • every error is written by hand, followed by a return you can forget;
  • beyond a few dozen routes, the wiring code grows.
Verdict

The best choice when it’s enough, and it’s enough more often than people think. Write a small adapter on day one so your handlers return an error: it’s twenty lines, and it saves you writing a thousand.

The code: GET /notebooks/{id} with the standard library
mux := http.NewServeMux()
mux.HandleFunc("GET /notebooks/{id}",
	func(w http.ResponseWriter, r *http.Request) {
		n, err := store.Notebook(r.Context(), r.PathValue("id"))
		if errors.Is(err, ErrNotFound) {
			http.Error(w, "notebook not found", http.StatusNotFound)
			return
		}
		if err != nil {
			http.Error(w, "internal error", http.StatusInternalServerError)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(n)
	})

Chi

v5.5 · compatible routerMy favoritelow distance

A router that is “100% compatible with net/http”, with no external dependencies. It keeps the standard signature and adds what Go’s router lacks to organize a large API: groups, sub-routers, mounting routers written elsewhere, and a package of battle-tested middleware.

func(w http.ResponseWriter, r *http.Request)

A word of disclosure: it’s my favorite. I don’t love Chi because it does a lot. I love it because it does exactly what the standard library is missing, and nothing else: it’s the smallest distance you can take from it while still gaining something real. Five reasons:

  1. The standard signature. The whole ecosystem plugs in without adapters: otelhttp, httptest, CORS, even a gRPC server mounted on a route.
  2. Composition. Route, Group and Mount: each module of the application exposes its own router, which main assembles.
  3. Sober middleware. Request ID, panic recovery, timeouts, compression: each fits in one file you can read in five minutes.
  4. No dependencies, a stable API. Its v5 dates from 2021: routers I wrote years ago still compile.
  5. It gets out of the way. Since v5.0.12, it fills in r.PathValue: my handlers don’t import Chi, which lives only in the routes file.

On the eight criteria

1 · Distancelow: only the routes
2 · Errorsthrough a home-made adapter
3 · Inputup to you
4 · Contractvia Huma
5 · Observabilityotelhttp, directly
6 · Maintenancev5, stable since 2021
7 · Protocolseverything net/http speaks
8 · Teamconventions to set
Where it fits
  • the API of a product that will grow, with areas under different rules (public, signed-in, admin);
  • a gRPC backend with a little REST, with grpc-go or ConnectRPC mounted on the same router;
  • an internal foundation shared by several teams;
  • a server-rendered site with many pages.
Challenges and risks
  • it decides nothing for you: errors, validation and documentation are up to you;
  • on a given router, middleware must be declared before routes, or it refuses to start;
  • middleware.RealIP trusts the X-Forwarded-For and X-Real-IP headers: only use it for services behind a trusted proxy.
Verdict

My starting point for an API that will grow. Decide early on your conventions for errors, validation and logging, and write them once: my skeleton, below, does it in a few lines. When you need an OpenAPI contract, don’t switch routers: put Huma on top.

gorilla/muxv1.8 · taken over in 2023Same signature, very common in existing projects, but releases rarely.
httprouterv1.3 · no release since 2019Very fast, the ancestor of Gin’s router.
The code: what Chi adds to the standard router

A public route and a protected admin area, written both ways:

net/http alone
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", healthz)

admin := http.NewServeMux()
admin.HandleFunc("GET /admin/users", listUsers)
admin.HandleFunc("POST /admin/users/{id}/suspend", suspend)
mux.Handle("/admin/", requireAdmin(admin))

// Global middleware is stacked by hand,
// from innermost to outermost.
var h http.Handler = mux
h = logging(h)
h = recoverer(h)
http.ListenAndServe(":8080", h)
Chi
r := chi.NewRouter()
r.Use(middleware.RequestID, middleware.Recoverer)
r.Use(logging)

r.Get("/healthz", healthz)

r.Route("/admin", func(r chi.Router) {
	r.Use(requireAdmin)
	r.Get("/users", listUsers)
	r.Post("/users/{id}/suspend", suspend)
})

http.ListenAndServe(":8080", r)

The standard version works, but the sub-router repeats the /admin prefix, the middleware order is the reverse of the reading order, and nothing visually ties the admin routes to their protection. With Chi, the structure of the code is the structure of the API. At two hundred routes, that’s a security matter: reading the code is how you check that no admin route escaped its middleware.

My skeleton: handlers that return an error

Chi doesn’t settle the question of errors, and that’s deliberate. I settle it once and for all, with a small type that lets handlers return an error, as in Echo or Huma, without leaving the standard signature:

internal/web/handler.go
// Handler is an http.Handler that returns an error.
type Handler func(w http.ResponseWriter, r *http.Request) error

func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if err := h(w, r); err != nil {
		writeError(w, r, err) // one place for status codes, the JSON format and logs
	}
}

// A business error becomes an HTTP response here, and nowhere else.
func writeError(w http.ResponseWriter, r *http.Request, err error) {
	status, message := http.StatusInternalServerError, "internal error"
	switch {
	case errors.Is(err, notebook.ErrNotFound):
		status, message = http.StatusNotFound, "notebook not found"
	case errors.Is(err, notebook.ErrInvalid):
		status, message = http.StatusBadRequest, err.Error()
	default:
		slog.ErrorContext(r.Context(), "request failed", "err", err, "path", r.URL.Path)
	}
	writeJSON(w, status, map[string]string{"error": message})
}

Notebook’s handler, rewritten with this type, no longer contains a single Chi-specific line: Chi only appears in the route declarations.

internal/web/notebooks.go
func (a *API) getNotebook(w http.ResponseWriter, r *http.Request) error {
	n, err := a.notebooks.Get(r.Context(), r.PathValue("id"))
	if err != nil {
		return err
	}
	return writeJSON(w, http.StatusOK, n)
}

func (a *API) Routes() http.Handler {
	r := chi.NewRouter()
	r.Use(middleware.RequestID, middleware.Recoverer)
	r.Method("GET", "/notebooks/{id}", Handler(a.getNotebook))
	return r
}

Huma

v2 · the contract derived from the codemoderate distance

You write Go types and typed functions; Huma derives validation and a complete OpenAPI 3.1 spec from them. It doesn’t replace the router: it sits on top of the one you choose (Chi, the standard library, Gin, Echo, Fiber…). The handler no longer speaks HTTP at all: it looks like a business function.

func(ctx context.Context, in *Input) (*Output, error)

On the eight criteria

1 · Distancemoderate: input and output
2 · Errorsreturned
3 · Inputvalidated from the types
4 · Contractgenerated OpenAPI
5 · Observabilitythe router’s
6 · Maintenancesmaller community
7 · Protocolsdepends on the router
8 · Teamconventions to learn
Where it fits
  • public or partner APIs, where the OpenAPI spec is a deliverable;
  • organizations where several teams consume the same services;
  • an API on Chi that needs documenting, route by route.
Challenges and risks
  • the whole input-output layer goes through Huma: leaving it means rewriting that layer;
  • less freedom over the shape of responses;
  • a hand-written spec alongside it will end up contradicting the code: pick one direction.
Verdict

The best ratio between what it brings and the distance it imposes, because it keeps a standard router underneath. If the contract is written first, or negotiated with other teams, go with oapi-codegen instead.

oapi-codegenv2 · contract firstYou start from an OpenAPI file, and the tool generates the types and a server interface to implement, for most routers. Its strict server mode generates typed “input → output” signatures, close to Huma’s.
ogencontract firstSame starting point, but ogen generates its own server and its own serialization code, without reflection. More radical, further from the standard library.
Goav3 · design firstYou describe the API in a dedicated language written in Go, and Goa generates the HTTP, gRPC or JSON-RPC transport, the documentation and the skeletons. Very powerful for many consistent services, more demanding to learn.
ConnectRPCv2 · Protobuf first.proto files, and services that speak gRPC, gRPC-Web and a simple HTTP protocol you can read with curl, all at once. A CNCF project since 2024; its v2 (October 2026) decouples its core from net/http while still using it.
The code: GET /notebooks/{id} with Huma, on top of Chi
type NotebookInput struct {
	ID string `path:"id" maxLength:"36" doc:"Notebook ID"`
}
type NotebookOutput struct {
	Body Notebook
}

r := chi.NewRouter()
r.Get("/healthz", healthz) // internal routes stay pure Chi

// The public API goes through Huma, which generates /openapi.json and the docs
api := humachi.New(r, huma.DefaultConfig("Notebook", "1.0.0"))
huma.Get(api, "/notebooks/{id}",
	func(ctx context.Context, in *NotebookInput) (*NotebookOutput, error) {
		n, err := store.Notebook(ctx, in.ID)
		if errors.Is(err, ErrNotFound) {
			return nil, huma.Error404NotFound("notebook not found")
		}
		if err != nil {
			return nil, err
		}
		return &NotebookOutput{Body: n}, nil
	})

Both live on the same router: you can migrate to Huma route by route, starting with the ones that need documenting.

Gin

v1.12 · the best knownhigh distance

The best-known Go framework. The handler receives a Gin-specific context that bundles the request, the response and a toolbox: parameters, body binding and validation, one-line JSON rendering. It’s still built on net/http.

func(c *gin.Context)

On the eight criteria

1 · Distancehigh: routes, handlers, middleware
2 · Errorsthe handler returns nothing
3 · Inputbinding and validation
4 · Contractthird-party tools (swaggo)
5 · Observabilityotelgin
6 · Maintenancestill on v1
7 · ProtocolsHTTP/2, HTTP/3 since v1.11
8 · Teamthe largest talent pool
Where it fits
  • a team new to Go that has to ship fast, especially one coming from Express, Flask or Laravel;
  • projects that prefer a shared convention to a custom stack;
  • hiring: documentation and examples galore.
Challenges and risks
  • handlers, home-made middleware and part of the tests are written for Gin;
  • standard middleware goes through an adapter (gin.WrapH), mostly in one direction;
  • the handler doesn’t return an error: the same discipline as the standard library, with Gin’s own vocabulary.
Verdict

A good choice to ship fast with a team new to Go: the comfort of a rich context is worth more than purity here. Keep it in the HTTP layer: a *gin.Context that leaks into the business logic multiplies the exit cost by ten.

The code: GET /notebooks/{id} with Gin
r := gin.New()
r.Use(gin.Recovery())
r.GET("/notebooks/:id", func(c *gin.Context) {
	n, err := store.Notebook(c.Request.Context(), c.Param("id"))
	if errors.Is(err, ErrNotFound) {
		c.JSON(http.StatusNotFound, gin.H{"error": "notebook not found"})
		return
	}
	if err != nil {
		c.AbortWithStatus(http.StatusInternalServerError)
		return
	}
	c.JSON(http.StatusOK, n)
})

Echo

v5.4 · returned errorshigh distance

From the same family as Gin, the context-based frameworks, with one difference that matters: its handlers return an error, which a central handler turns into a response. No more forgotten return. It’s also built on net/http.

func(c *echo.Context) error

On the eight criteria

1 · Distancehigh: routes, handlers, middleware
2 · Errorsreturned, centralized
3 · Inputbinding; validator to plug in
4 · Contractthird-party tools
5 · Observabilitymaintained by Echo
6 · Maintenancev5 touched every handler
7 · Protocolseverything net/http speaks
8 · Teamplenty of documentation
Where it fits
  • the same case as Gin: a team new to Go that has to ship fast;
  • APIs where a uniform error format matters a lot;
  • projects that prefer a shared convention.
Challenges and risks
  • handlers, middleware and tests are written for Echo: that’s what you’ll rewrite to leave;
  • v5 (January 2026) turned the context into a struct: migrating from v4 touches every handler, and v4 only gets fixes until the end of 2026;
  • the OpenTelemetry instrumentation left the community repository in October 2026: it now depends on the Echo team.
Verdict

Between the two context-based frameworks: Gin for its talent pool, Echo for its handlers that return an error, a choice I appreciate. Go straight to v5, and keep it, too, out of the business logic.

The code: GET /notebooks/{id} with Echo
e := echo.New()
e.Use(middleware.Recover())
e.GET("/notebooks/:id", func(c *echo.Context) error {
	n, err := store.Notebook(c.Request().Context(), c.Param("id"))
	if errors.Is(err, ErrNotFound) {
		return echo.NewHTTPError(http.StatusNotFound, "notebook not found")
	}
	if err != nil {
		return err // the central handler will respond 500
	}
	return c.JSON(http.StatusOK, n)
})

Fiber

v3.6 · on fasthttpvery high distance

An Express-inspired API, built on fasthttp, an engine designed to keep memory allocations to a minimum. It’s the only candidate that also replaces step 1 of the journey: at the very bottom of the stack, net/http makes way for another engine.

func(c fiber.Ctx) error

On the eight criteria

1 · Distancevery high: everything but the business logic
2 · Errorsreturned
3 · Inputbinding; validation to plug in
4 · Contractthird-party tools
5 · Observabilitymaintained by Fiber
6 · Maintenancev3 in 2026, v2 maintained
7 · Protocolsno HTTP/2
8 · Teamfamiliar to Node.js developers
Where it fits
  • very high-traffic gateways and collectors, when profiling shows the HTTP server itself is the bottleneck;
  • Node.js teams, for an instant start: a good reason to try it, not a good enough reason to adopt it.
Challenges and risks
  • an ecosystem of its own: instrumentation, tests, middleware, everything has to exist “for Fiber”; v3 accepts standard handlers, but converts them on every request;
  • values read from the context are only valid during the handler: keeping them longer without copying causes subtle bugs;
  • fasthttp doesn’t speak HTTP/2.
Verdict

Rarely, but for real. Start with the standard library and httputil.ReverseProxy, profile with pprof under your load, and only switch engines if the HTTP server shows up at the top of the profile.

Hertzv0.10 · on NetpollFrom CloudWeGo, ByteDance’s open source organization, with its own network layer. It targets very large volumes of microservices and doesn’t have a version 1 yet.
The code: GET /notebooks/{id} with Fiber
app := fiber.New()
app.Get("/notebooks/:id", func(c fiber.Ctx) error {
	n, err := store.Notebook(c.Context(), c.Params("id"))
	if errors.Is(err, ErrNotFound) {
		return fiber.NewError(fiber.StatusNotFound, "notebook not found")
	}
	if err != nil {
		return err
	}
	return c.JSON(n)
})

Encore

infrastructure in the codevery high distance

The closest thing to what a Java developer calls a framework, and then some. An annotation declares the APIs, and resources (databases, message queues, scheduled jobs) are declared in the code: Encore derives the local infrastructure and the deployment from them, into your AWS or GCP account, or onto your own servers.

//encore:api public · func(ctx context.Context, id string) (*Notebook, error)

On the eight criteria

1 · Distancevery high: a way of thinking
2 · Errorsreturned
3 · Inputbuilt-in validation
4 · Contractgenerated docs and clients
5 · Observabilitybuilt-in tracing
6 · Maintenancebacked by a company
7 · Protocolsthe platform’s
8 · Teama platform to learn
Where it fits
  • a small team that wants to run a product without spending weeks on cloud configuration;
  • projects where startup time matters more than freedom.
Challenges and risks
  • the most binding choice on the list: you adopt a way of thinking, not a library;
  • a smaller community, so fewer ready-made answers;
  • a strong dependency on the tool, and on the company behind it.
Verdict

A huge accelerator for the right team. Isolate your business logic carefully: it’s your way out.

Encore isn’t alone in this family of full platforms. For dozens of microservices and several teams, go-zero and Kratos impose a common structure out of the box: as much an organizational decision as a technical one. The other path, which I prefer when the platform team has the means: a small internal foundation on Chi or ConnectRPC, plus your conventions.

go-zerov1.10 · .api filesIts goctl tool generates complete microservices from description files, safeguards included (rate limiting, circuit breaking).
Kratosv3 · Protobufv3 (June 2026) starts from Protobuf to expose every service over HTTP and gRPC.
Beegov2 · full MVCVery widespread in China, still maintained.
The code: GET /notebooks/{id} with Encore
//encore:api public method=GET path=/notebooks/:id
func Get(ctx context.Context, id string) (*Notebook, error) {
	n, err := store.Notebook(ctx, id)
	if errors.Is(err, ErrNotFound) {
		return nil, &errs.Error{Code: errs.NotFound, Message: "notebook not found"}
	}
	return n, err
}

No router or server to write: Encore derives them from the annotation. That’s exactly what makes it strong, and what puts it so far away.

What about the others? Iris Revel Buffalo bunrouter Flow… The list is long, and some of them mattered. Revel hasn’t released since 2022; Buffalo, on the other hand, still does. Before adopting a tool outside this list, look at three things: the date of its latest release, the number of people who merge contributions, and the signature of its handlers.

=== RUN TestCandidates --- PASS: TestCandidates (part 5)

Seven candidates, seven distances, and none that wins everywhere. To find yours, over to the compass.

Part 6 · The compass
Part 6 · compass

The compass

The profiles start from the candidates; the compass goes the other way. It starts from your project, asks four questions and tells you which candidate to start with. It doesn’t replace the reasoning of the previous parts: it sums it up.

Four questions, one starting point

1. You’re building…
2. Your team…
3. Eventually, how many routes?
4. The infrastructure…
Suggested starting point Answer the questions above.

=== RUN TestCompass --- PASS: TestCompass (part 6)

The compass gives you a starting point. All that’s left is to remember the essentials.

Part 7 · What to remember
Part 7 · conclusion

What to remember

If you were to keep only one page of this article, it would be this one.

  1. In Go, the standard library already does most of the work

    The HTTP server, the router, testing: a framework doesn’t provide the foundation there, only comfort on top.

  2. The criterion above all: distance

    The further a tool moves from the standard library, the more it does for you, and the more leaving costs. Its handler signature tells you in one line.

  3. The smallest distance that’s enough for you

    List what the standard library doesn’t give you, pick the closest tool that covers it, and keep the business logic out of the framework’s reach.

Your turn

I’ve only given my reading, and my preferences. I’d love to hear yours.

Have you ever had to leave a Go framework? What did you have to rewrite?

Since Go 1.22, have you gone back to the standard library? What do you still miss?

Have you ever seen the framework be the real performance bottleneck, profile in hand?

Code first or contract first: which holds up better over time where you work?

Has a platform like go-zero, Kratos or Encore saved you time, or cost you time, in the long run?

And you: what’s your favorite, and when don’t you pick it?

To reply, write to me by email or on LinkedIn. I’m always happy to reply, as my schedule allows, sometimes a little late, but I always reply.

The last word
In Go, a framework isn’t judged by what it adds, but by the distance it puts between your code and the standard library. Choose the smallest distance that’s enough for you. And always keep a way out.
Rivel Babindamana
Developer, October 2026

PS: the day the standard router can group routes and apply middleware to them, I’ll leave Chi by rewriting a single file. That, in the end, is all I ask of it.