JetBrains published a guide to Go web frameworks last week. It's fair, as these things go. But it buries the lead: the Go team quietly made most framework arguments obsolete over two years ago, and almost nobody noticed.
The JetBrains piece opens with a reasonable question — do you even need a framework? — and then spends most of its length on Gin, Echo, Chi, and Fiber. The net/http case is made in bullet points. The framework cases get code samples, comparison tables, and a section on how GoLand's Endpoints tool helps you navigate them.
That's the tell. JetBrains sells GoLand. GoLand's framework-aware tooling is more valuable when frameworks feel necessary. To be clear: the article isn't dishonest. But the shape of it — and the survey data it draws from (JetBrains users, who skew enterprise and framework-heavy) — nudges you toward a dependency you probably don't need.
Let's work through what the actual picture looks like.
Go 1.22 Closed the Routing Gap
For years, the core reason developers reached for Gin was routing ergonomics. The old net/http mux was genuinely limited: no method-based dispatch, no named path parameters, no wildcard patterns. You either hand-rolled a router or you pulled in a dependency. That argument is dead.
Go 1.22, released in February 2024, shipped enhanced routing patterns into the standard library. Method-scoped routes. Named path parameters. Wildcard matching. Precedence resolution for overlapping patterns. Here's what it looks like in practice:
// Go 1.22+ standard library — no dependencies
func main() {
mux := http.NewServeMux()
// Method + path in one declaration
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("DELETE /users/{id}", deleteUser)
// Wildcard catch-all
mux.HandleFunc("GET /files/{path...}", serveFile)
http.ListenAndServe(":8080", mux)
}
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // native path parameter access
// ...
}
Compare that to what the JetBrains article presents as Gin's ergonomic advantage:
// Gin — requires external dependency
router.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, user)
})
One line more verbose. Native http.ResponseWriter and *http.Request. Zero dependencies. No gin.Context replacing context.Context throughout your codebase. No migration cost when Gin's maintainers make breaking changes.
The JetBrains article mentions Go 1.22's routing improvements in a single bullet point and moves on. That bullet point is the entire story.
The One Real Gap — and How We Closed It
Routing handled, there's one place where raw net/http genuinely hurts: request binding. In every REST handler you write, you're doing some version of this:
func createUser(w http.ResponseWriter, r *http.Request) {
// Extract from path
orgID := r.PathValue("orgId")
// Extract from query string
role := r.URL.Query().Get("role")
// Decode the JSON body
var body CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
// Read a cookie
cookie, _ := r.Cookie("session")
// Now validate everything manually...
}
That pattern repeats across every handler. It's not difficult — but it's mechanical, it's verbose, and it's where subtle bugs live (forgetting to close the body, skipping error handling on one field, inconsistent validation logic scattered across handlers).
This is the gap that frameworks actually fill. But it's a narrow gap, and it doesn't require a framework to close it. It requires a small, single-purpose module — one that does exactly this and nothing else.
The pattern is straightforward: bind request data to a struct via tags, return a typed error, compose with whatever validator or logger you already have. You could write this yourself in a focused afternoon. We did, and we called it binder. It's ~600 lines, zero dependencies, MIT licensed. Use it, fork it, or treat it as a reference for rolling your own — the point is the pattern, not the import path.
func createUser(w http.ResponseWriter, r *http.Request) {
var req struct {
OrgID string `path:"orgId"`
Role string `query:"role"`
Name string `body:"name,required"`
Email string `body:"email,required"`
Tags []string `body:"tags"`
Session string `cookie:"session"`
}
if err := binder.Bind(r, &req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// req is fully populated and validated
user := createUser(req)
json.NewEncoder(w).Encode(user)
}
Path parameters. Query strings. JSON body. Form data. Cookies. Required field enforcement. Custom types via encoding.TextUnmarshaler. Slices. Nested structs. Content-type-aware body parsing (JSON or form, automatic). All handled. All in ~600 lines of code you can read in an afternoon.
The properties that make this the right shape of solution — whether you use ours or write your own:
Just the Go standard library. go.sum stays clean. Your audit surface doesn't grow.
It binds. That's it. You compose it with whatever validator, logger, and middleware you already have.
~600 lines. You can understand the entire thing in an hour. That's the right size for a dependency.
Uses r.PathValue() natively. Not retrofitted onto an older router abstraction.
Benchmark numbers
Sub-millisecond across the board. Reflection cache means the struct layout is computed once per type, not per request. The slowest case — mixed sources including JSON body decode — is 0.004ms. This is not your bottleneck.
The Gorilla Warning No One Is Heeding
The JetBrains survey reports that 17% of Go developers are still using Gorilla in 2025. Gorilla Mux — not actively maintained since November 2023. Last commit over two years ago. One in six Go developers is depending on dead infrastructure for production web routing.
This is the framework-dependency trap made statistical. You pick a dependency at the start of a project because it feels productive. It ages. The maintainers move on. Now you're stuck: either absorb the migration cost, or keep shipping on a foundation that isn't being patched.
The stdlib doesn't do this to you. Go is maintained by Google with a strict backwards-compatibility guarantee. The net/http package you write against today will work, unchanged, in Go 1.32. No migration. No forks. No watching a GitHub repo for signs of life.
The article notes that "community forks continue development" for Gorilla. That's not reassurance. That's a warning. Community forks mean fragmentation, inconsistent maintenance, and no single authoritative source of patches. If you're on Gorilla today, the right move is Chi or native net/http, not a fork.
Fiber: Lock-In With Extra Steps
Fiber deserves special attention because the JetBrains article is almost admirably honest about its problems — and then doesn't label them properly.
Fiber is built on fasthttp, not net/http. It is therefore not a Go web framework in the same sense as Gin or Echo. It's a parallel HTTP ecosystem with its own context types, its own middleware interface, and its own compatibility story. The JetBrains FAQ says:
From the JetBrains article: "You cannot use the vast library of generic Go middleware with Fiber out of the box. You therefore need to use middleware maintained by the Fiber team or employ an adapter, which introduces performance overhead and effectively defeats the reason to use Fiber in the first place."
Read that again. Their own FAQ tells you that Fiber's main selling point — extreme performance via fasthttp — is undermined the moment you need to integrate with the standard Go ecosystem. The lock-in isn't incidental. It's structural.
The benchmark numbers are real. fasthttp is faster than net/http in raw throughput. But unless you're operating at the scale where that difference is the bottleneck — and almost no one is — you're trading ecosystem compatibility for a number that doesn't matter to your users.
The Comparison That Should Exist
The JetBrains article includes a framework comparison table. Here's the one that would be more useful — what you actually get from each approach:
| Concern | net/http | + focused tooling | Gin | Fiber |
|---|---|---|---|---|
| Method routing | ✓ Since 1.22 | ✓ Since 1.22 | ✓ | ✓ |
| Path parameters | ✓ PathValue() | ✓ | ✓ | ✓ |
| Request binding | ✗ Manual boilerplate | ✓ Struct tags, ~600 LOC | ✓ | ✓ |
| File uploads | ~ r.FormFile() | ~ r.FormFile() | ✓ Built-in | ✓ Built-in |
| External dependencies | ✓ Zero | ✓ Zero | ✗ Moderate | ✗ Significant |
| stdlib middleware compat | ✓ Native | ✓ Native | ~ Partial | ✗ Adapter only |
| Context type | ✓ Standard | ✓ Standard | ✗ gin.Context | ✗ fiber.Ctx |
| Abandonment risk | ✓ None | ✓ Readable — fork if needed | ~ Low | ~ Medium |
| Gorilla migration path | ✓ Direct | ✓ Direct | ~ Moderate | ✗ Significant |
The pure net/http column is honest: request binding is manual and that's real work. The "focused tooling" column shows what closing that gap actually costs — a single small module or an afternoon writing your own. Compared to taking on a full framework, it's not a close call. The only genuine wins for Gin and Fiber are file uploads and, in Fiber's case, raw throughput numbers that most services will never need.
When a Framework Actually Makes Sense
Intellectual honesty requires carve-outs. There are legitimate reasons to reach for a framework.
File uploads. If your API handles multipart/form-data — user avatars, document ingestion, anything with binary payloads — Echo or Gin give you this out of the box. Binder deliberately doesn't. You can handle multipart natively with r.FormFile(), but it's more work than a framework's built-in abstraction.
Porting an Express application. Fiber is genuinely reasonable here. If your team is migrating a Node.js service and the developers know Express patterns deeply, Fiber's structural similarity reduces cognitive load during the transition. Just treat it as a transitional choice, not a permanent one.
Chi. The article is right that Chi is barely a framework — it's a router with full net/http compatibility. If you want slightly more ergonomic route grouping than the standard mux and you're not on 1.22 yet, Chi is a defensible pick. It doesn't lock you in.
Team familiarity. If your entire team knows Gin and you're two weeks from a production deadline, this is not the moment to refactor your HTTP layer. Technical purity has a cost. Measure it.
The honest position: none of this is "frameworks are evil." The position is that the default should be no framework until you have a reason, rather than framework first and justify it later. Most Go services are built with the second posture. The first is cheaper to maintain, easier to audit, and harder to break.
The Auditable Surface Argument
We've written before about minimal code as the only auditable surface in the AI era. The argument applies here too, and more concretely.
Every dependency you add is code you didn't write, can't fully audit, and don't control. Gin pulls in several transitive dependencies. Fiber's dependency tree is larger. Each one is a surface for supply chain compromise — a risk profile that isn't hypothetical. The March 2026 Claude Code incident was a reminder that the attack vector isn't your code. It's the graph of code you trust.
net/http is maintained by the Go team. It is, for all practical purposes, the most audited Go code in existence. Binder is ~600 lines with no dependencies — you can read it, understand it, and decide whether you trust it in an hour. That's not true of Gin's codebase, and it's certainly not true of Fiber plus fasthttp.
The framework question is not just an ergonomics question. It's a trust and audit question. The answer to "do I need a framework?" should start with "what am I taking on?" not "what features does it have?"
- Go 1.22 ships method routing and named path parameters. The core routing argument for Gin is gone.
- The remaining friction — request binding — is real, but it doesn't need a framework. It needs a small, single-purpose module with no deps and readable source. Write one, borrow one, or use ours.
- Pure
net/httpis a legitimate choice. The boilerplate is explicit, auditable, and yours. That's not always the wrong tradeoff. - 17% of Go devs are on Gorilla, which hasn't been actively maintained since 2023. That's the dependency trap in a statistic.
- Fiber's own FAQ admits that ecosystem compatibility overhead defeats its performance advantage.
- Frameworks are justified for file uploads, Express migration, or team familiarity under deadline. Not as a default.
- The right question isn't "which framework?" It's "what's the smallest thing I actually need?"
The JetBrains article is worth reading. It's more balanced than most of its genre. But the genre exists to make tooling feel necessary, and it's worth remembering that when you're reaching for go get gin at the start of a new service.
Read the changelog first. It might already have what you need.