Why We Chose Go for Our Backend Services

November 28, 2016

When we started building a new set of backend services last year, we evaluated several languages: Python (our existing stack), Node.js, Java, and Go. We chose Go. After a year of production use, that decision has proven sound. Here’s our reasoning and what we’ve learned.

The Context

Our existing backend was Python with Django. Python served us well for years, and we still use it extensively. But the new services had different requirements:

Python’s GIL and memory footprint made it suboptimal for these workloads. We needed something that handled concurrency better and deployed more efficiently.

Why Go

Concurrency Model

Go’s goroutines and channels make concurrent programming accessible. Spawning thousands of goroutines is cheap; coordinating them is straightforward.

func handleConnections(connections chan net.Conn) {
    for conn := range connections {
        go handleConnection(conn) // Each connection in its own goroutine
    }
}

Compare this to Python’s threading (GIL-limited), multiprocessing (high overhead), or async (callback complexity). Go’s model fits naturally with our workloads.

Performance

Go compiles to native code. CPU-bound tasks run fast. Memory usage is predictable. Garbage collection is optimized for low latency.

Our API endpoints that processed JSON data were 10-20x faster in Go than Python equivalents. For high-throughput services, this matters.

Deployment Simplicity

Go compiles to a single static binary. No runtime dependencies, no virtual environments, no dependency resolution at deployment time.

# Build for Linux
GOOS=linux GOARCH=amd64 go build -o service

# Deploy: copy binary and run
scp service server:/app/
ssh server "/app/service"

Docker images are tiny (10-20MB with Alpine base). Startup time is instant. This simplifies deployment and reduces infrastructure costs.

Static Typing and Tooling

After years of Python, static typing felt restrictive initially. But the compiler catches errors that Python discovers at runtime—often in production.

Go’s tooling is excellent:

The language enforces consistency; all Go code looks similar regardless of author.

Standard Library

Go’s standard library is comprehensive and high-quality. HTTP servers, JSON handling, cryptography, compression—most needs are covered without external dependencies.

// Complete HTTP server
http.HandleFunc("/api/data", handleData)
log.Fatal(http.ListenAndServe(":8080", nil))

Fewer dependencies means fewer security vulnerabilities, version conflicts, and maintenance burden.

What We Gave Up

Go isn’t perfect. Here’s what we miss:

Expressiveness

Go is deliberately simple. No generics (as of 2016), no exceptions, no inheritance, no operator overloading. Code that’s elegant in Python can be verbose in Go.

// Sum of integers in Go (no generics)
func sumInts(nums []int) int {
    sum := 0
    for _, n := range nums {
        sum += n
    }
    return sum
}

// Need a separate function for floats
func sumFloats(nums []float64) float64 {
    sum := 0.0
    for _, n := range nums {
        sum += n
    }
    return sum
}

This repetition is annoying. The Go community has developed patterns (interfaces, code generation) to mitigate it, but it’s a real cost.

Error Handling

Go’s error handling is explicit but verbose:

data, err := fetchData()
if err != nil {
    return nil, err
}

processed, err := processData(data)
if err != nil {
    return nil, err
}

result, err := saveResult(processed)
if err != nil {
    return nil, err
}

if err != nil appears constantly. It’s explicit and clear, but the repetition clutters code.

Dynamic Capabilities

Go’s static nature makes certain patterns difficult:

These are sometimes anti-patterns anyway, but their absence occasionally requires workarounds.

Ecosystem Maturity

Go’s ecosystem is younger than Python, Java, or Node.js. Libraries exist for most needs, but options are fewer. Quality varies more. Documentation is sometimes sparse.

We occasionally had to build functionality that would be a library import in Python.

Lessons Learned

Embrace Simplicity

Go’s simplicity is a feature, not a limitation. Resist the urge to build abstractions. The language discourages cleverness; accept that.

Code that feels verbose is often more readable six months later. Explicit beats implicit.

Interfaces for Testing

Go’s implicit interfaces enable clean testing:

// Define interface for what you need
type DataStore interface {
    Get(key string) ([]byte, error)
    Put(key string, value []byte) error
}

// Real implementation uses database
type PostgresStore struct { ... }

// Test implementation uses memory
type MemoryStore struct { ... }

// Code using DataStore doesn't care which implementation
func ProcessData(store DataStore, key string) error { ... }

Define small interfaces at the point of use. Mock only what you need.

Structured Concurrency

Go makes concurrency easy—sometimes too easy. Without care, you create goroutine leaks and race conditions.

Patterns that help:

Error Handling Patterns

We adopted patterns to reduce error handling noise:

// Named return values for deferred error handling
func doSomething() (result Result, err error) {
    defer func() {
        if err != nil {
            // Cleanup on error
        }
    }()
    // ...
}

// Error wrapping for context
if err != nil {
    return fmt.Errorf("processing user %d: %w", userID, err)
}

Embrace the Standard Library

Before reaching for external packages, check the standard library. net/http, encoding/json, database/sql—they’re well-designed and well-maintained.

External dependencies should add significant value to justify their cost.

When Go Fits

Go works well for:

Go fits less well for:

A Year Later

After a year of production Go:

Go didn’t replace Python entirely—we use each where it fits. But for services requiring concurrency, performance, and deployment simplicity, Go has earned its place in our stack.

The initial investment in learning paid off. Go’s opinions, once accepted, become productivity.

Key Takeaways