Writing / 2016
Why We Chose Go for Our Backend Services
How Go replaced Python and Node as our default backend language at a mobility startup, and the tradeoffs we accepted.
At a mobility startup we had a fleet of shared vehicles, a mobile app, and a backend stitched together from Python and Node services. The system worked. Mostly. But every time we pushed past a few hundred concurrent riders the cracks showed: event loops choking on CPU-bound geofencing, Python processes eating RAM, and deploys that required matching the exact virtualenv on the target box or everything fell over. The Node services were fast to write and slow to operate. Debugging a callback-hell crash at 3 AM isn’t my idea of a good time.
We needed something that compiled to a single binary, handled thousands of concurrent connections without drama, and let a small team move fast without an ops nightmare.
Go won us over with fast compiles, dead-simple deployment, and goroutines that scale. The error handling is verbose and generics are nowhere in sight, but for network-heavy backend services in 2016, nothing else hit the same balance.
Why Go
I started experimenting with Go a few releases back, and it didn’t take long to convince me. Three things sold me.
Compilation speed. Our Python and Node services had no compile step, which sounds like an advantage until you realize the “compile step” just moved to production as runtime errors. Go gave us a real compiler that caught problems early and still built a full service in seconds. Coming from a brief stint with Java microservices, this felt like cheating.
Single binary deploys. One binary. No runtime. No dependency manager on the server. No “works on my machine.” We could cross-compile for Linux on a Mac, scp the binary to a server, and it ran. Later we moved to Docker
, but even there the images were tiny because there was nothing to install.
Goroutines and channels. This was the real hook. At the mobility startup, every vehicle sends a GPS heartbeat every few seconds. That problem is embarrassingly concurrent. Go made that concurrency feel natural rather than bolted on.
Here is a simplified version of how we handled vehicle heartbeats:
func processHeartbeats(heartbeats <-chan Heartbeat, store *VehicleStore) {
for hb := range heartbeats {
go func(hb Heartbeat) {
if err := store.UpdatePosition(hb.VehicleID, hb.Lat, hb.Lng); err != nil {
log.Printf("failed to update vehicle %s: %v", hb.VehicleID, err)
return
}
if outsideGeofence(hb.Lat, hb.Lng) {
alertOps(hb.VehicleID)
}
}(hb)
}
}
Nothing clever. No framework. Just goroutines, a channel, and straightforward control flow. A junior engineer could read this and understand what happens. That mattered to me more than elegance.
What we gained
The improvements were immediate and measurable.
Memory usage on the vehicle tracking service dropped from around 400 MB under the Python version to about 30 MB with Go. Tail latency at p99 went from unpredictable spikes to a consistent sub-10ms for the heartbeat path. Deploy time went from “run Ansible and hope” to “copy binary and restart.”
The standard library deserves credit too. net/http, encoding/json, crypto/tls: all solid, all built in. We pulled in very few external dependencies in the first year, which meant fewer things to audit and fewer things to break.
The downsides
Go isn’t perfect, and some of the tradeoffs sting.
Error handling verbosity. if err != nil after every single call. I’ve written that line thousands of times. It makes control flow explicit, which I genuinely value, but it also adds noise. Some files are 40% error checks. You get used to it, but it never stops being verbose.
No generics. In late 2016 this is Go’s most obvious gap. Every time I write a utility function that works on a slice, I either duplicate it for each type or reach for interface{} and lose type safety. We ended up with a pkg/sliceutil package full of type-specific helpers that felt like something a code generator should handle. It works, but it isn’t satisfying.
Smaller ecosystem. Python has a library for everything. Go in 2016 has a library for most things, but the quality varies and some areas are just thin. ORM support, for instance, was rough. We ended up writing raw SQL with database/sql and that turned out to be a better long-term choice, but it was more work upfront.
Dependency management. In 2016, Go’s dependency story is a mess. Vendoring, godep, glide, and no official solution. We settled on glide and it mostly worked, but it wasn’t a confidence-inspiring experience. This is the one area where I envied the Node and Python ecosystems.
What I would tell another team
If your backend is mostly network services (APIs, data pipelines, real-time processing), Go is a strong default in 2016. The language is deliberately boring, and boring is a feature when you’re running production systems with a small team.
Don’t adopt Go because it’s trendy. Adopt it because you want fast compiles, predictable performance, and deploys that don’t require a prayer. Then accept the verbosity and the missing generics as the price of admission.
Start with one service. Pick the one that hurts the most operationally. Rewrite it in Go, measure the difference, and let the results speak. That’s what we did at the mobility startup with the heartbeat service, and within two months every new backend service was Go by default.
Looking ahead
Go 1.7 landed in August with context in the standard library, which cleaned up a lot of our cancellation and timeout patterns. The tooling keeps getting better. gofmt alone has saved us more arguments than any code review policy.
I don’t know if Go will get generics. I hope it does. But even without them, the language has earned its place in our stack through discipline and simplicity. Not everything needs to be expressive. Sometimes you just need it to work, to be fast, and to be obvious.
That’s Go for me. And I’m not going back.