In 2014 I came back from San Francisco with no plan. I’d spent a year there building SDKs at small startups (Joypad, TinySpark) after shipping one of my first games, and now I was home in Spain, about to start computer science at a university where I’d already been writing code for most of a decade. I needed something to build.
So I started a startup. It was called Fyve, a social network built around people’s interests, and I decided to write the backend in Go. Gin came out of that. It was the framework I built for my own product while I was still learning the language, and most of what makes it what it is comes from being written in pure learning mode.
Simple over easy
Back then the dominant Go web framework was Martini. It was a pleasure to pick up. The whole thing fit in one README, the middleware model was elegant, and you could be productive in minutes. I loved how approachable it felt.
The trouble showed up underneath. Martini leaned on reflection-based dependency injection to wire everything together, which made it easy to start and hard to reason about. Services appeared in your handlers by magic, the control flow was implicit, and the reflection cost real time on every request.
Around then I was taking the Go community’s design values to heart, especially the idea in Rob Pike’s Simplicity is Complicated. Something that’s simple to use is expensive to build. The simplicity is the work, buried under the surface so the person using it never feels it.
So I drew my own line between easy and simple. Easy is familiar and close at hand. Simple is few moving parts, nothing braided together. Martini optimized for easy. I wanted it simple underneath, and I was willing to pay for that with work a user would never see.
The middle ground
Go’s standard net/http gives you simple. There’s no magic, the control flow is explicit, and you can read exactly what happens. The price is boilerplate, because you write the same plumbing for parsing, params, and responses in every handler.
Gin aims for the point between Martini’s magic and net/http’s verbosity. It keeps everything explicit, runs no reflection on the request path, and clears the boilerplate by giving you one object to work with: 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 single *gin.Context carries the request, the response writer, the path parameters, validation
helpers, and rendering. You pass one thing around and it does most of what a handler needs. The
ergonomics were the goal. Keep the obvious things one method call away, and keep the machinery
visible.
A router built around a radix tree
The other half of Gin was speed, and most of a router’s speed comes down to a single operation: finding the handler that matches an incoming path. I spent a lot of that early research on the data structure and landed on a radix tree, the approach httprouter made popular in Go.
A radix tree stores shared prefixes once. Register a handful of routes and they collapse into a compact tree, so matching a URL is a walk proportional to the length of the path and independent of how many routes you’ve registered.
Routes registered:
/search
/support
/blog/:slug
/blog/:slug/comments
Radix tree (shared prefixes stored once):
/s
├─ earch -> handler
└─ upport -> handler
/blog/
└─ :slug -> handler
└─ /comments -> handler
Matching /blog/42/comments walks /blog/ down to the :slug node, binds 42, and continues into
/comments. The work scales with the URL, and the hot path is built to avoid heap allocations.
Parameters land in a preallocated slice, and Context objects come from a sync.Pool and get reset
between requests, which keeps the garbage collector quiet. Under load, that restraint is what keeps
latency flat.
Stable from the first commit
There was a third goal, quieter than performance but the one I’m proudest of. I wanted the API to be stable from day one, in the spirit of Go’s own compatibility promise.
It held. Some of the first programs ever written against Gin still compile and run more than ten years later. Backward compatibility like that is a constraint you feel on every API decision, and it forces a useful discipline. You add a method before you’d ever remove one, and you resist the clever change that saves five characters and breaks a thousand projects.
Hacker News, and then growth
I released Gin on Hacker News and it landed well. Go was having a moment there, new projects in the language drew a curious crowd, and Gin’s pitch of simple, fast, one README was an easy thing to try. It grew steadily from there, and today it sits around 88k stars with more than 290k projects depending on it.
Letting it graduate
A few years in, I stepped back and handed Gin to a team of maintainers who have kept improving it without me. I think of that as the project graduating. For me, the best moment in open source is the one where the thing you made stops needing you, sustains itself, and gets better in other people’s hands.
If you’re building a library, that’s the bar I’d aim for. Design the API you’d be willing to keep for ten years, make it simple underneath even when that costs more than making it easy, and build it so it can outgrow you. I tried to do the same thing a few years later on a compiler, which is the story of Qwik.