Why I Chose Go for My Backend

By Yusuf Setiyawan
Picture of the author
Published on
Go Backend Development

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:

  1. CPU-bound tasks were painful — The single-threaded nature of Node.js made heavy computations slow everything down.
  2. Callback hell and complexity — Even with async/await, managing complex asynchronous flows felt messy.
  3. Memory consumption — For high-traffic services, memory usage was higher than I'd like.
  4. 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:

  1. Hot reloading — Go requires recompilation. Tools like Air help, but it's not as seamless.
  2. NPM ecosystem — The Go package ecosystem is smaller. Sometimes I need to build what I could just npm install.
  3. Rapid prototyping — JavaScript is faster for quick experiments.

The Results

After migrating to Go for Ruang Bimbel's backend:

MetricNode.jsGo
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!