Building Gin: Simple Over Easy — Manu Martínez-Almeida
In 2014 I came back from San Francisco with no plan. I spent a year building SDKs at Joypad and TinySpark after shipping one of my first games, and that year gave me a good sense of what small software teams need from their tools. Back in Spain, about to start Telecommunications Engineering, I had to decide what to build next.
The answer was Fyve, a social network built around people’s interests. I chose Go for the backend because the language felt plain in the right way, and Gin started as the web framework for that product. The code still lives at gin-gonic/gin.
!Image 1: Gin Gonic Go framework illustration Fyve never took off. Gin, the tool I built along the way, is still going twelve years later.
Simple over easy
At the time, the Go web framework people kept pointing me to was Martini. I understood why immediately. The README was small, the middleware model felt elegant, and you could get a route responding in minutes.
Martini used reflection-based dependency injection to wire handlers together, which made the first demo feel smooth but also moved important behavior out of sight. Services appeared in your handlers without any visible wiring, so when something misbehaved the control flow was hard to trace. And all of that reflection ran on every single request.
Around then I watched Rob Pike’s Simplicity is Complicated, and it gave me vocabulary for what bothered me about Martini. What stuck with me was the idea that simple software often takes more work from the person building it so that it can take less work from the person using it.
That became the design brief for Gin. Easy is about how good the first example looks, and Martini’s first example looked great. Simple is about how many moving parts you have to understand, and how many exceptions you have to remember, once the codebase is old enough to surprise you.
Finding the middle ground
Aristotle’s version of virtue was the middle ground: **not too much, not too little.** That was the shape of the framework problem too.
Martini gave you too much magic. Plain net/http gives you full control and no surprises, but it helps you with almost nothing, so you end up writing the same plumbing for route params, request parsing, validation, and responses in handler after handler. None of it is hard, but it makes the code noisy.
Gin was my attempt at the point between the two. The request path stays explicit, nothing on it uses reflection, and the repetitive plumbing lives in a single object called the Context.
``` r := gin.Default() r.GET("/users/:id", func(c *gin.Context) { id := c.Param("id") // path params, no reflection c.JSON(200, gin.H{"id": id}) // response rendering, one call }) r.Run(":8080") ```
That `*gin.Context` carries the request, the response writer, path parameters, validation helpers, and rendering, so it’s the only thing you pass around. The common operations sit one method call away, and behind them is plain Go code you can step into when production gets weird.
Funny enough, `gin.Context` shipped in 2014, two years before the standard library’s `context.Context` existed. When the standard one arrived we kept our name and made `gin.Context` satisfy the new interface, so every existing program kept compiling and you could pass a `gin.Context` anywhere a `context.Context` was expected.
That instinct came from the SDK years. When the convenient way to do something is also the right way, people write better code without noticing.
A router built around a radix tree
The router is where the simple-over-easy line became concrete. Martini matched requests by walking a list of regular expressions and asking each one whether it matched. Regexes are flexible, since you can make a route match only numbers or hide extra rules inside the pattern, but they’re also a second language living inside your framework.
Gin’s route language is smaller. You get static segments, named parameters, and catch-alls, and that restriction is exactly what lets the router use a radix tree, the same approach httprouter made popular in Go. It also had a side effect I came to appreciate. Since the route language gives you nothing to be clever with, routes written for Gin tend to be regular and boring.
#### Route lookup, without scanning routes
routes **0**nodes **1**matched **/**
Figure. A compressed radix tree, built and matched live. Shared prefixes collapse into one path, then /blog/42/comments walks the tree and binds 42 as :slug.
#### Start with ordinary routes
The router sees route strings like `/search`, `/support`, and `/blog/:slug/comments`. A naive router would test them one by one on every request, while a radix tree merges their shared text into a single structure up front.
#### Shared prefixes collapse
`/search` and `/support` share `/s`, and the two blog routes share `/blog/` and then `:slug`, so each shared prefix gets stored once.
#### Matching walks one path
For `/blog/42/comments`, the lookup follows `/blog/`, binds `42` at `:slug`, then continues into `/comments`. The other routes never even get visited.
#### The hot path stays small
By the time the walk reaches the final node, the handler is already attached to it and the path parameters are sitting in a preallocated slice. Nothing along the way needed reflection.
Matching `/blog/42/comments` walks `/blog/` down to the `:slug` node, binds `42`, and continues into `/comments`. The cost of a lookup depends on the length of the URL, and it stays the same whether the app has ten routes registered or ten thousand. Routes that share a prefix also share nodes, so a big route table stays compact in memory.
For a router holding $n$ routes and a request path of length $k$, a radix-tree lookup runs in
$$ T_{\text{match}} \left(\right. k \left.\right) = O \left(\right. k \left.\right) , \text{independent}\textrm{ }\text{of}\textrm{ } n , $$
while checking a list of $n$ regexes costs $O \left(\right. n \cdot m \left.\right)$ for patterns of length $m$. The tree trades the per-request scan for a single walk down the shared prefix.
The allocations follow the same thinking. Path parameters go into a preallocated slice, and Context objects come out of a `sync.Pool` and get reset between requests, so the garbage collector has less junk to clean up and latency has fewer reasons to wobble.
I trust this kind of performance work because the speed comes from doing fewer things, and doing fewer things also leaves the person reading the code with less to understand.
Designing for zero breaking changes
The other constraint I gave myself was backward compatibility. Go had made a compatibility promise for the language itself, and I wanted Gin to offer its users the same deal.
That constraint changes how you design. Before anything goes into the public API you ask whether you’d be happy maintaining it for ten years, because removing it later is off the table. You learn to reject the clever rename that saves five characters, and to treat every exported function as something a stranger might have built a company on.
The constraint held. Some of the first programs ever written against Gin still compile and run today, more than a decade later, and I’m prouder of that than of any benchmark.
Hacker News, and then growth
I released Gin on Hacker News at the right moment. Go was getting attention there, and a framework that fit in one README and benchmarked well was easy for people to try.
The growth after that was steady. People used it, filed issues, sent patches, and put it in real services, and today Gin sits around 88k stars with more than 290k projects depending on it.
Stars are a vanity metric, and I mostly treat them that way. The dependency count is the number I care about, because each of those projects is a bet that the API won’t break underneath them, and that bet has kept paying off long after Fyve disappeared.
Letting it graduate
A few years in, I stepped back and handed Gin to maintainers who kept improving it without me. I think of that as the project graduating. Special kudos to Bo-Yi Wu and Javier Provecho, who carried it forward and kept the bar high.
That’s the part of open source I respect most, when a project stops needing its author. Gin has since absorbed years of other people’s use cases, priorities, and taste, and it came out better for it.
If you’re building a library, that’s the bar I’d aim for. Design an API you can imagine keeping for ten years, and make it simple underneath even when that costs more than making it easy, because with any luck the thing will outgrow you.
I tried to do the same thing a few years later on a compiler, which is the story of Qwik.