I wrote PHP for twelve years. Then I started writing Go for a living. People warned me the hard part would be the syntax — the curly braces, the explicit types, the lack of a hundred array helper functions. They were wrong. The syntax took a weekend. The hard part was unlearning a decade of reflexes that PHP had quietly trained into me.
Every request starts from zero — except it doesn't
In classic PHP, the mental model is beautifully simple: a request comes in, the interpreter boots, your code runs, a response goes out, and everything is thrown away. No state survives. That model makes a whole category of bugs impossible, because there is nothing to leak.
Go is the opposite. The process starts once and lives for weeks. A variable you initialise at startup is shared by every request that follows — across goroutines, concurrently. The first time I cached something in a package-level map and reached for it from a handler, I shipped a data race straight to production.
// Looks innocent. It is not.
var cache = map[string]string{}
func handler(w http.ResponseWriter, r *http.Request) {
cache[r.URL.Path] = "seen" // concurrent write — boom
}
The fix is old news to any Go developer — a sync.RWMutex, or better, sync.Map — but the instinct to even think about it is the thing PHP never had to teach me.
Errors are values, and that's a gift
PHP exceptions encourage a "throw it and let someone upstairs deal with it" style. Go's if err != nil gets mocked endlessly, but after a few months I came to trust it. The error is right there, in the open, at the exact line it happened. You handle it or you very deliberately pass it up. Nothing hides.
The verbosity isn't noise — it's the code refusing to let you pretend the failure won't happen.
What carried over
- Thinking in requests and responses. Twelve years of HTTP is twelve years of HTTP, whatever the language underneath.
- Database discipline. Indexes, N+1 queries, connection limits — none of that changed. Go just made connection pooling something I had to think about on purpose.
- Distrust of third-party input. Validating at the boundary is a habit that pays off in any language.
What didn't
Reaching for an array as a do-everything data structure. Loose typing as a shortcut. The assumption that the garbage collector would clean up after a sloppy request because the request was about to die anyway. In Go, sloppiness compounds, because the process keeps running.
If you're making the jump
Don't fight the explicitness — lean into it. The compiler is not your enemy; it's the colleague who reviews every line before it runs. Write the boring code. Handle the error. Pass the context. And the first week you reach for a global variable, stop and ask who else might be touching it at the same time. That single question is most of what twelve years of PHP didn't prepare me for — and learning to ask it is most of what made me a Go developer.