As a Go developer with significant experience in web development, I have seen the cycle repeat itself. New developers often jump straight into heavy frameworks like Gin or Beego, or reach for complex ORMs like GORM, thinking they need them to be productive.
I used to do the same. But over time, I realised that Go is different. The standard library is incredibly powerful, and adding "magic" usually just adds technical debt.
My philosophy for 2025 is simple: Stick to the Standard Library for the core, and use specialised libraries only to fill the gaps.
Here is the "Standard+Gap" stack I use to build production-ready services, and why I chose these specific tools over the alternatives.
1. The Core: Why I Reject Web Frameworks and ORMs
Before talking about the libraries I do use, let's talk about what I don't use.
HTTP Server: net/http vs. Frameworks
I don't use heavy HTTP frameworks to start the server.
-
The Reality: Go’s standard
net/httpis production-grade. It handles HTTP/2, timeouts, and cancellation contexts natively. -
The Problem with Frameworks: Frameworks often wrap the
http.ResponseWriterandhttp.Requestin their own context objects. This breaks compatibility with the rest of the Go ecosystem and hides what is actually happening over the wire.
Database: database/sql vs. ORMs (GORM, Ent)
I stick to the standard database/sql (occasionally with sqlx for struct mapping).
- The Cons of ORMs: ORMs promise speed but often deliver inflexibility. They rely on reflection (slow) and introduce "magic" behavior. Simple queries are easy, but complex joins or performance tuning become a nightmare of fighting the ORM's syntax.
- The Pros of Standard Lib: SQL is the universal language of data. Using the standard library forces you to write efficient queries and gives you full control over database interactions.
2. Filling the Routing Gap: Chi
While net/http is great, its default request multiplexer (router) can be a bit basic for complex REST APIs. This is where I fill the gap.
My Choice: Chi
Why:
Chi is barely a framework; it's a router that is 100% compatible with net/http.
-
Pros: It uses standard
http.Handler. It has zero external dependencies. Middleware integration is intuitive. - Cons: You have to handle your own JSON encoding/decoding (which I prefer, as it gives me control).
Comparison:
- Gin/Echo: These are full web frameworks. They are faster in micro-benchmarks, but they lock you into their vendor-specific context. If you use Chi, your code is just "Go code."
3. Filling the Logging Gap: Zap
The standard log package is insufficient for modern observability. You need structured (JSON) logging for tools like Datadog or ELK.
My Choice: Zap
Why:
Zap is Uber's structured logger. It is obsessively optimized for performance.
- Pros: Zero-allocation in hot paths. Extremely fast. Strongly typed fields prevent runtime errors in logs.
-
Cons: The syntax is verbose (
logger.Info("msg", zap.String("key", "value"))).
Comparison:
- Logrus: The old standard. It’s easy to read but slow and uses a lot of memory.
- Slog: The new standard library addition. It is excellent and likely the future, but Zap currently has a larger ecosystem and battle-tested edge cases in high-load systems.
4. Filling the Migration Gap: Go-Migrate
Database schemas evolve. You need a way to version control your database.
My Choice: golang-migrate
Why:
It treats migrations as what they are: SQL files.
-
Pros: It supports almost every database driver. It keeps
upanddownmigrations in plain SQL files, separating your DB logic from your Go code. - Cons: Error handling can be tricky; if a migration fails halfway, you often have to manually fix the "dirty" state in the schema table.
Comparison:
-
Goose: Goose allows migrations written in Go code. This is powerful for data backfilling, but for schema changes, I prefer the strict isolation of SQL files that
go-migrateenforces.
This stack represents the "sweet spot" of Go development:
- Server: Standard
net/http(Reliable, Standard). - Database: Standard
database/sql(Explicit, High Control). - Router:
Chi(Fills the routing gap without locking you in). - Logs:
Zap(Fills the observability gap with high performance). - Migrations:
Go-migrate(Fills the versioning gap).
By avoiding bloated frameworks and inflexibility ORMs, you keep your Go applications readable, maintainable, and close to the metal.
What is your "Gap-Filling" stack? Tell me in the comments!
Top comments (0)