Skip to content
gohacks.dev
Web

The Best Go Web Frameworks Compared by Use Case and Scale

Pick a Go web framework by what you are building: standard library, chi, gin, echo, fiber or huma, compared on dependency weight, handler portability and fit.

8 min read
Summarize in AI
Flat illustration of a request passing through concentric middleware layers to a handler.

Every few months someone asks which Go web framework to use, and the honest answer starts with a question: for what? A prototype built over a weekend, a public JSON API that will be maintained for six years, a gateway that forwards a hundred thousand requests a second, and a server rendered site with forms all want different things. The framework wars on forums skip that, and they lean on benchmarks that, as the router comparison showed, measure differences nobody will ever feel.

Tested with Go 1.25.6, chi v5.5.0, gin v1.12.0, echo v4.15.0, fiber v2.52.16, macOS on Apple M1 Max (arm64)

This roundup compares the realistic options on things that do matter over the life of a project: what you pull into your build, how tied your handlers are to the framework, what you get beyond routing, and who each one suits. I ran a small experiment for the weight of each. It belongs to the web cluster that ends in a production HTTP server, and the choice between the first two options is argued from the other side in do you need a framework at all.

What it costs to add one

The experiment creates a fresh module for each option, writes the same hello-world server (one route with a path parameter), pins the version, builds it with -ldflags="-s -w" and reports the number of modules in the build list and the size of the binary.

Shell
set -e
work=$(mktemp -d)
cd "$work"
build() { # name, import path@version, source
  mkdir "$1" && cd "$1"
  go mod init "example.test/$1" >/dev/null 2>&1
  printf '%s\n' "$3" > main.go
  [ -n "$2" ] && go get "$2" >/dev/null 2>&1
  go mod tidy >/dev/null 2>&1
  go build -ldflags="-s -w" -o app . 
  printf '%-10s modules in build list: %-3s binary: %s KB\n' "$1" "$(go list -m all | wc -l | tr -d ' ')" "$(( $(wc -c < app) / 1024 ))"
  cd ..
}
build stdlib "" 'package main
import ("fmt"; "net/http")
func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /hello/{name}", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "hello ", r.PathValue("name")) })
	http.ListenAndServe(":0", mux)
}'
build chi github.com/go-chi/chi/[email protected] 'package main
import ("fmt"; "net/http"; "github.com/go-chi/chi/v5")
func main() {
	r := chi.NewRouter()
	r.Get("/hello/{name}", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "hello ", chi.URLParam(r, "name")) })
	http.ListenAndServe(":0", r)
}'
build gin github.com/gin-gonic/[email protected] 'package main
import "github.com/gin-gonic/gin"
func main() {
	r := gin.New()
	r.GET("/hello/:name", func(c *gin.Context) { c.String(200, "hello "+c.Param("name")) })
	r.Run(":0")
}'
build echo github.com/labstack/echo/[email protected] 'package main
import "github.com/labstack/echo/v4"
func main() {
	e := echo.New()
	e.GET("/hello/:name", func(c echo.Context) error { return c.String(200, "hello "+c.Param("name")) })
	e.Start(":0")
}'
build fiber github.com/gofiber/fiber/[email protected] 'package main
import "github.com/gofiber/fiber/v2"
func main() {
	app := fiber.New()
	app.Get("/hello/:name", func(c *fiber.Ctx) error { return c.SendString("hello " + c.Params("name")) })
	app.Listen(":0")
}'
Text
stdlib     modules in build list: 1   binary: 5181 KB
chi        modules in build list: 2   binary: 5431 KB
gin        modules in build list: 56  binary: 13388 KB
echo       modules in build list: 20  binary: 5697 KB
fiber      modules in build list: 18  binary: 6096 KB
ServerModules in the build listBinary size
standard library15.1 MB
chi25.3 MB
fiber186.0 MB
echo205.6 MB
gin5613.1 MB

The standard library version has a build list of one entry, the module itself, because nothing outside the toolchain is imported. chi adds exactly one module, with no dependencies of its own. echo and fiber bring around twenty each: validators, color and template helpers, a JSON library, and for fiber the fasthttp server and its support packages. gin is the heaviest at 56 modules and a binary two and a half times the size of the others, because it bundles several serialization formats (the protobuf, msgpack and YAML renderers among them) whether or not your service uses them.

None of that is a reason to avoid them; they pay for features you may want. It is a reason to know what you are signing up for. Every module is a supply chain link you track for security advisories, an upgrade you will do, and something that must support the next Go release before you can move to it. A build list of one is the easiest to keep current for years, and in my experience the dependency that breaks your upgrade is rarely the router and often something it dragged in.

The options, by use case

The standard library: most JSON services

Since Go 1.22 the mux does methods and path parameters (the patterns article has the rules), the handler signature is the portable func(w, r), and nothing needs updating when Go does. You write your own middleware chain, your own route grouping, your own JSON helpers. That is perhaps 150 lines you will own and understand completely. Choose it for services you expect to maintain, small teams, and anywhere dependency count matters. Avoid it if nobody on the team wants to write the 150 lines, or the product needs features such as a generated OpenAPI document immediately.

chi: structure without lock-in

chi is a router and a small set of middleware built directly on net/http. It adds route groups, per-group middleware stacks, mounting of sub-routers and some regex constraints, and its handlers stay func(w, r). Moving to or from the standard mux means editing the router file. Choose it for medium and large APIs that want groups and middleware stacks, and teams that want a router they can leave later. Avoid it if you only have a dozen routes, where the standard mux is enough.

gin: bundled helpers, large community

gin has a custom context, struct tag binding and validation, rendering helpers, groups, and a big ecosystem of middleware and tutorials. Handlers are func(*gin.Context). Choose it for teams that already know it and value productivity from bundled binding and validation. Keep in mind that the context ties handlers and tests to gin, that its defaults differ from net/http in places (a 404 where others answer 405 for a wrong method, as the router comparison showed), and that you will use its validator library, which has sharp edges described in the validator article.

echo: gin's close relative

echo is similar in spirit: a custom context, binding, groups, middleware, and handlers that return error, which centralises error handling in one place and is the feature I like best. Choose it for the same cases as gin, particularly if you prefer the error return and its HTTP error handler. The caveats about lock-in apply equally. One practical note from this experiment: the newest echo release at the time of writing required a newer Go than the one I ran, so I pinned the previous minor version. Check the minimum Go version of any framework release before upgrading, because a framework can force a toolchain upgrade on you.

fiber: fast, and a different ecosystem

fiber is modelled on Express and built on fasthttp, not net/http. That is the whole story. It can be fast in benchmarks, and the API is pleasant for people coming from JavaScript. But fasthttp has its own request and response types, so the standard library's http.Handler, httptest, http.Client-based tooling, most third-party middleware and the OpenTelemetry HTTP instrumentation do not apply directly; fiber provides adaptors, with some overhead and some gaps. Choose it for a team that understands the trade, and a workload where raw request throughput really is the constraint. Avoid it if you want the broader Go HTTP ecosystem to just work.

huma: when the API description is the product

huma is not a router replacement but a layer that runs on top of several routers (the standard mux, chi, gin, echo and others) and generates an OpenAPI document and validation from typed handler inputs and outputs. Choose it for APIs with external consumers, where a correct, always-current OpenAPI document and generated clients are valuable. It is covered in code-first API frameworks, and generating a server from a spec instead is the other path, in oapi-codegen.

By scale and team

SituationWhat I would pick
One developer, internal tool, 20 endpointsStandard library
Small team, public JSON API, years of maintenanceStandard library, or chi once groups help
Larger team that wants one style and bundled helpersgin or echo, with business logic kept out of handlers
API with external developers and generated clientshuma on top of the standard mux or chi
Gateway or proxy at very high request ratesStandard library with ReverseProxy, and measure before considering fasthttp
Server rendered site with forms and templatesStandard library or chi, with html/template or templ
Team arriving from Expresschi or echo; fiber if you accept fasthttp

Keeping the choice reversible

Whatever you pick, two habits make the decision cheap to undo. First, put business logic in plain functions and types that take ordinary arguments and return ordinary values, and keep handlers thin: decode the input, call the function, encode the result. A service layer that never sees a *gin.Context can be moved to another framework by rewriting the thin layer. Second, write the middleware you depend on against http.Handler wherever you can, even inside a framework that has its own type, because the net/http form works in all of them (gin and echo both provide wrappers for it).

The mistake to avoid is deciding by a benchmark table or a star count. The framework you will be glad you picked is the one whose behavior you understand at 3 a.m., whose upgrades are boring, and whose absence from your handlers would let you replace it in a week. For the reference on the language-level pieces underneath every option, see the net/http documentation, and for the official view on frameworks the Go team's own guidance on writing web applications starts with nothing but the standard library.

Also inGinChiEcho
Taggedroundup
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.