~/blog/choosing-go-for-touchnote-backend
Engineering2017·20 July 2017

Why We Chose Go for TouchNote's Backend (And What Surprised Us)

In 2016-17, TouchNote's backend was a mix of PHP and a growing need for high-concurrency services. Go was the answer — but the migration surfaced surprises we didn't anticipate.

GoPHPperformanceconcurrencyTouchNote

The PHP Ceiling

PHP is an excellent language for many applications. TouchNote had been running on PHP for years, and it served us well. But as the product evolved — real-time order tracking, high-frequency fulfilment status updates, websocket-based features — we kept hitting PHP's concurrency model limitations.

PHP's traditional shared-nothing model processes one request per process/thread. For I/O-heavy workloads with many concurrent users, you need many processes — which means significant memory overhead. PHP-FPM with 100 worker processes uses meaningful RAM before handling a single request.

Go's goroutine-based concurrency model is fundamentally different. A single Go process can handle thousands of concurrent goroutines with modest memory overhead.

Why Go Over Node.js or Kotlin?

We evaluated alternatives:

Node.js: The event-loop model handles I/O concurrency well, and the team had JavaScript experience. But callback/promise complexity for multi-step operations felt like trading one problem for another. (This was 2016 — async/await wasn't universal yet.)

Kotlin/JVM: Strong typing, excellent libraries, mature ecosystem. But JVM startup time and memory overhead at container scale felt heavy for the microservices we were building.

Go: Static binary compilation (no runtime dependencies), goroutine-based concurrency, simple deployment, excellent standard library. The net/http package alone is production-grade. The learning curve from PHP was steeper than Node.js but the operational simplicity was compelling.

What Surprised Us

Goroutine leaks are real and silent. Unlike thread leaks in most languages, goroutine leaks don't throw exceptions — goroutines just accumulate in memory. Our first Go service had a subtle bug where goroutines launched for async processing were never guaranteed to terminate. It took weeks of memory growth to diagnose with pprof.

Go's error handling is verbose but honest. Coming from PHP exceptions, every function returning (result, error) felt clunky. Over time, it forced better error handling discipline. Errors were handled at the source, not accidentally swallowed by broad catch blocks.

The standard library is genuinely excellent. net/http, encoding/json, database/sql — these are not toy implementations. We ran Go HTTP services handling thousands of requests per second with the standard library's HTTP server and encountered no limitations.

Dependency management was immature (pre-modules). GOPATH and early dep/glide dependency management was painful. Go modules (introduced in 1.11) fixed this, but the period before modules was genuinely frustrating.

Kubernetes Made Go Even Better

The combination of Go's static binaries with Kubernetes was transformative for operational simplicity. Go services compile to a single binary with no runtime dependencies. Docker images could be FROM scratch (literally just the binary). Image sizes went from 500MB+ (PHP + Apache) to 5-10MB.

Fast image pulls, fast container startup, predictable memory usage — Go's operational characteristics were a perfect fit for Kubernetes.