Boring on purpose: why Go quietly won the modern backend
Go was never the fashionable choice. It was always the durable one. Here's why we reach for Go first when we're building backend services that have to last — and the specific patterns we use to keep them clean as the codebase grows.

Walk into a senior backend engineer's office in 2016 and ask what language to use for a new service. The answer would have been a long, animated conversation about Node vs. Java vs. Python vs. Ruby — with Go mentioned, briefly, as "the Google language."
Walk into the same office in 2026 and ask the same question. The answer is, increasingly, just: Go. Not with excitement. Not with the missionary energy of an early adopter. With the bored, settled confidence of someone who has shipped Go in production for years and stopped having to defend the choice.
At Hexcore, Go is our default backend language for new services. Not because it's the most expressive. Not because it has the richest ecosystem. Because it's the language we make the fewest expensive mistakes in — and after sixty-plus shipped systems, that's the variable that compounds.
Here's what changed, why it matters, and the patterns we use to keep Go codebases clean as they scale.
What changed in ten years
Three things, mostly invisible from outside the language:
1. Generics landed without ruining the language
Go held off on generics for over a decade — through a chorus of derision from people whose languages had ten flavors of generics each. When generics finally arrived in Go 1.18 (2022), they were narrow, disciplined, and shipped without breaking a single existing program. That restraint is the point.
The result: Go now has the type-system features it actually needed (typed collections, generic constraint-based functions) without the cognitive tax that drove people off Scala in the late 2010s. The language is bigger, but only barely.
2. The standard library finally absorbed structured logging, slog, and net/http v2
The pre-slog Go landscape had six competing logging libraries, all subtly incompatible. As of Go 1.21, log/slog is in the standard library — structured, contextual, performant, and what every new project should use.
The same quiet absorption has been happening across the stdlib: better HTTP routing in net/http, improved error handling, better context propagation. The result is a Go stdlib in 2026 that handles 80% of what a backend service needs, with zero dependencies pulled in.
3. The deployment story collapsed to a single binary
This was always Go's claim. In 2026, with Kubernetes ubiquitous and distroless container images standard, it has become a measurable production advantage. A typical Go service deploys as a 12–25 MB binary on a gcr.io/distroless/static image. No runtime, no package manager, no surprise CVE in a base layer. The container scan is two lines long.
Compare to a typical Node or Python deploy — node_modules directories with three thousand packages, transitive vulnerability fan-out, runtime updates that change behaviour. The boring deployment wins every single time at the SRE post-mortem.
The deep reasons Go wins for backend
Beyond what's changed, Go's original architectural choices have aged better than almost anyone predicted in 2012.
Concurrency is in the language, not in a library
Goroutines and channels are not a framework you bolt on. They're language primitives. Which means: every Go developer writes concurrent code in the same idiom. There's no "async/await camp" vs. "Promise camp" vs. "callback camp" inside the same codebase.
For backend work — which is almost entirely about coordinating I/O across many concurrent requests — this is decisive. A Go HTTP server with a worker pool, a circuit breaker, and bounded fan-out for downstream calls looks the same in every Go codebase we've ever read. That uniformity is a property of the language, and it's worth more than any individual feature.
The type system is small, but it's there
Go's type system is not Haskell. It barely qualifies as a type system in the eyes of an Idris user. But here's what matters for backend services:
- Compile-time catching of nil dereferences, wrong-type passes, missing fields
- IDE refactors that actually work because the compiler can verify them
- Onboarding new engineers without "what's the shape of this dict?" being a constant question
The bar for "valuable type system" in backend engineering is much lower than the type-theory community wants to admit. Go clears that bar with margin and stops there. Which is exactly what you want when the goal is shipping, not exploring.
Compilation is sub-second
A Go service of modest size compiles in under 800 milliseconds on a recent laptop. The full test suite for a 50k-LOC Go backend often runs in under 30 seconds. The feedback loop matters more than any single language feature, and Go's feedback loop is unrivaled outside of dynamic languages — and even most dynamic languages don't have full type checking running that fast.
We've measured developer velocity across language stacks at Hexcore. The teams shipping Go consistently report fewer interruptions to deep work because they don't lose flow waiting for compiles or test runs. That's not romantic; it's just measurable.
Backward compatibility is taken seriously
Code we wrote in Go 1.4 still compiles, with minor adjustments, on Go 1.22. Try that with most other ecosystems.
This is a quiet but enormous advantage in long-lived backend services. A system we're operating for a client today might still be in production in 2031. The probability that today's Go code will still compile and run on whatever Go is current in 2031 is essentially 1.0. The same cannot be said for Node, Python, or even Java in many cases.
What Go is not good at
To be honest — and because the case for Go is stronger when its limits are stated clearly — Go is not our first choice when:
- The workload is CPU-bound numerical work. Reach for Rust, C++, or Julia. Go's runtime overhead and lack of SIMD intrinsics in the standard toolchain make it suboptimal for tight numerical loops.
- The system is fundamentally a transformation pipeline over heterogenous data. Python's data ecosystem (pandas, polars, Arrow, the ML stack) is still the right answer. Don't fight it.
- You need a language that's idiomatic for frontend or universally available. TypeScript wins. Use it.
- The interface is the system. For prototyping, exploratory data analysis, or research-oriented work, Python or even a notebook is faster.
Go is a backend service language. It's exceptionally good at building network services that handle traffic, integrate with databases, coordinate background work, and run for years without surprises. Outside that lane, other tools win. We use them.
The patterns we ship in production
A few things we've learned about keeping Go codebases clean as they scale. None of these are novel; all of them are dropped from real engagements when teams get sloppy and regretted later.
1. Package by feature, not by layer
The temptation is to have models/, handlers/, services/, repositories/. Resist it. Package by feature: billing/, users/, orders/. Each package owns its types, its database access, its handlers, its tests. The unit of cohesion is the business capability, not the technical layer.
This single decision determines whether your codebase reads cleanly at 50k lines or becomes spaghetti at 15k.
2. Interfaces are defined by consumers, not producers
This is idiomatic Go and worth restating. If package billing needs to send notifications, billing defines the Notifier interface it consumes. The notification package implements it. This keeps the dependency graph clean and makes test doubles trivial.
The Java instinct — implementor-defined interfaces — produces tightly-coupled designs that are painful to refactor. Don't bring that habit into Go.
3. Context plumbing is non-negotiable
Every function that does anything I/O-bound takes ctx context.Context as its first argument. Every. Single. One. This is how cancellation, timeouts, deadlines, and request-scoped values flow through the system. Skipping it for "convenience" is a debt you will pay back painfully during the first production incident that requires graceful shutdown.
We enforce this with linter rules. Yes, it's annoying. Yes, it's worth it.
4. Errors are values; treat them like values
Go errors are not exceptions and shouldn't be treated like them. Wrap them with fmt.Errorf("doing X: %w", err). Check them with errors.Is and errors.As. Define sentinel errors and custom error types where the caller needs to distinguish failure modes.
The codebases where Go feels painful are almost always codebases where the team is doing if err != nil { return err } everywhere with no context. The codebases where Go feels clean are the ones where errors carry the information needed to diagnose a production incident without attaching a debugger.
5. Structured logging via slog, with consistent fields
Pick five or six fields that should appear on every log line — request_id, user_id, tenant_id, service, version, trace_id — and make them automatic via a context-aware logger. Don't let individual log statements decide what fields to include; that road leads to unsearchable logs and incident triage misery.
log/slog handles this well. Use it.
6. Database access through pgx, not an ORM
We default to pgx (PostgreSQL) or the native driver of the database, with hand-written SQL and sqlc for type-safe generation. We do not default to a heavyweight ORM.
Two reasons:
- The SQL you generate from an ORM is rarely the SQL you'd write. This becomes a performance problem at scale, often invisible until it isn't.
- The cognitive load of debugging an ORM is higher than the cognitive load of writing SQL. Once you accept this, you stop fighting your tools.
This is opinionated and we lose this argument with some clients. The clients who let us hold the line on this don't regret it eighteen months later.
7. Tests are colocated, fast, and use real databases
Go tests live next to the code they test (foo.go → foo_test.go). They run in parallel. They use real PostgreSQL via testcontainers, not mocks. They complete in under 30 seconds for a 50k-LOC service.
The temptation to mock the database for "speed" is the wrong trade. Real-database tests catch real bugs. They also make refactors fearless, because you know your tests are actually exercising the data layer.
Where Go is going
A few quiet trends worth watching:
- Better profile-guided optimisation (PGO) — Go 1.21+ supports profile-guided builds with measurable performance wins on hot paths. This is now part of our deploy pipeline for latency-sensitive services.
- Slog-based observability stacks — the structured logging story is finally good enough to be the basis of a unified observability pipeline alongside OpenTelemetry tracing.
- Wasm targets — Go's WebAssembly support is steadily improving. Not production-ready for our use cases yet, but the trajectory is clear.
- Generics adoption stabilising — most codebases now use generics for collection helpers, but resist the temptation to over-genericize business logic. The "boring code wins" instinct is holding, which is healthy.
The honest conclusion
Go is not exciting. Go is not the language you reach for if you want to feel clever. Go is the language you reach for when you want to ship something today, operate it for five years, hand it to the next engineer without apology, and never spend a Sunday afternoon debugging a dependency-resolution issue.
For backend services — which is most of what we build at Hexcore — that's the entire deal. The flashier languages will keep winning conference talks. Go will keep winning production.
If you're starting a new backend service in 2026 and the workload is in Go's lane (network services, business logic, integrations, background work), the question to ask is not "should we use Go?" It's "what would have to be true for us not to?" If you can't answer that crisply, default to Go and move on. The compounding starts the moment you stop deliberating.
We're happy to talk through specific architectural questions if you're considering Go for a new build, or wondering whether to migrate an existing service. Drop a line at hello@hexcore.ng — or read the rest of our blog posts.
15 years across payments, telco, and platform engineering. Founded Hexcore to prove African engineering can ship at world-class standards.

The four practices that turn a launch into a durable product
Most launches succeed. Most products fail soon after. The gap is not engineering talent — it's a set of operating practices that the strongest teams treat as defaults. Here are the four we install on every Hexcore engagement.

Shipping Onsight: lessons from building workforce intelligence at field scale
Onsight is a workforce intelligence platform we built end-to-end at Hexcore — admin dashboard, native mobile app, exception queue, and the OpsIQ AI command layer. Here's what we learned shipping a four-surface product to teams operating in tough field conditions.

Why your RAG demo doesn't survive production
A working LLM prototype and an enterprise-grade RAG system are separated by a body of unglamorous infrastructure: evals, guardrails, observability, cost controls, drift detection. Here's the checklist we use to close the gap.