Why I Chose Go for My Backend

- Published on

After spending years building backends with Node.js and Express, I made a significant decision for my latest project, Ruang Bimbel: I switched to Go.
It wasn't an easy choice. Node.js had been my go-to for everything — APIs, real-time features, and even some CLI tools. But as my projects grew more complex and performance became critical, I started exploring alternatives. Here's why Go won me over.
The Problem with Node.js (For My Use Case)
Don't get me wrong — Node.js is fantastic. Its ecosystem is massive, the learning curve is gentle (especially if you already know JavaScript), and it's perfect for many applications.
But I started hitting some walls:
- CPU-bound tasks were painful — The single-threaded nature of Node.js made heavy computations slow everything down.
- Callback hell and complexity — Even with async/await, managing complex asynchronous flows felt messy.
- Memory consumption — For high-traffic services, memory usage was higher than I'd like.
- Type safety — TypeScript helped, but it's still a layer on top of JavaScript.
Why Go?
After researching alternatives (Rust, Elixir, and Go), I chose Go for several reasons:
1. Performance Out of the Box
Go is compiled to native machine code. This means:
- Faster startup times
- Lower memory footprint
- Better CPU utilization
For an EdTech platform handling thousands of concurrent users taking exams, this was crucial.
// Simple HTTP server in Go - blazing fast
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run() // listen on :8080
}
2. Concurrency is a First-Class Citizen
Go's goroutines and channels make concurrent programming intuitive. Unlike Node.js where you're fighting against the single-threaded model, Go embraces parallelism.
// Handling multiple operations concurrently
func processExamResults(userIDs []string) {
var wg sync.WaitGroup
for _, userID := range userIDs {
wg.Add(1)
go func(id string) {
defer wg.Done()
calculateScore(id)
updateRanking(id)
sendNotification(id)
}(userID)
}
wg.Wait()
}
3. Simplicity and Readability
Go has a small, consistent language spec. There's usually one obvious way to do things. Coming from JavaScript where there are 10 ways to do everything, this was refreshing.
No more debates about:
- Tabs vs spaces (Go has
gofmt) - Semicolons (Go handles it)
- Import styles (Go has a standard)
4. Static Typing Without the Ceremony
Go's type system is strict but not verbose. Unlike Java, you don't need to write novels to define a simple struct:
type User struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
}
5. Excellent Standard Library
Go's standard library is incredibly comprehensive. HTTP servers, JSON handling, cryptography, testing — it's all built-in. Fewer dependencies mean fewer security vulnerabilities and maintenance headaches.
The Gin Framework
For Ruang Bimbel, I chose the Gin framework. It's minimalist, fast, and has a familiar feel for anyone coming from Express.js.
// Middleware, routing, and handlers - clean and simple
func main() {
r := gin.Default()
// Middleware
r.Use(cors.Default())
r.Use(rateLimiter())
// Routes
api := r.Group("/api/v1")
{
api.POST("/auth/login", authController.Login)
api.POST("/auth/register", authController.Register)
// Protected routes
protected := api.Group("/")
protected.Use(authMiddleware())
{
protected.GET("/exams", examController.GetAll)
protected.POST("/exams/:id/submit", examController.Submit)
}
}
r.Run(":8080")
}
What I Miss from Node.js
It's not all roses. Here's what I sometimes miss:
- Hot reloading — Go requires recompilation. Tools like Air help, but it's not as seamless.
- NPM ecosystem — The Go package ecosystem is smaller. Sometimes I need to build what I could just
npm install. - Rapid prototyping — JavaScript is faster for quick experiments.
The Results
After migrating to Go for Ruang Bimbel's backend:
| Metric | Node.js | Go |
|---|---|---|
| Response time (avg) | ~120ms | ~15ms |
| Memory usage | ~500MB | ~50MB |
| Concurrent users | ~1000 | ~10000+ |
| Cold start | ~2s | ~100ms |
The numbers speak for themselves.
Should You Switch?
It depends. Here's my recommendation:
Stick with Node.js if:
- You're building MVPs or prototypes
- Your team is JavaScript-focused
- You need access to a massive package ecosystem
- Your app is I/O bound, not CPU bound
Consider Go if:
- Performance and efficiency are critical
- You're building microservices or high-traffic APIs
- You want simpler deployment (single binary)
- You value maintainability over rapid development
Final Thoughts
Switching from Node.js to Go was one of the best decisions I made for Ruang Bimbel. The performance gains, code clarity, and deployment simplicity have been game-changers.
That said, I still use Node.js for frontend tooling and some internal scripts. The right tool for the right job.
If you're considering the switch, start with a small service. Build something, deploy it, and see how it feels. You might be surprised.
Have questions about Go or my experience building Ruang Bimbel? Feel free to reach out!