Why Go, for a PHP dev

Station 0.1 8 min Interchange: PHP-FPM → a long-running binary

You already know how to build things. This course isn't going to teach you what a loop is. (If you've ever written $response['data']['data'][0]['data'] and shipped it anyway, you're among friends.) It's going to take the PHP instincts you've built up over the years and show you where they carry straight over to Go, where they need adjusting, and where they'll actively trip you up.

Let's start with the biggest one, because it changes how you think about everything else.

Your PHP app doesn't really exist between requests

Here's what happens when a request hits a typical PHP app. nginx hands it to PHP-FPM, a worker picks it up, your code boots (autoloader, container, config, routes), handles the request, sends a response, and then throws everything away. The next request starts from a clean slate.

That's the shared-nothing model, and it's quietly brilliant. PHP is the goldfish of runtimes, and we mean that as a compliment. A memory leak only lasts one request. A global variable can't bleed into someone else's request. If something goes badly wrong, the worker dies and FPM starts another one. You've probably never had to think about any of this, and that's the point. (Your memory_limit = -1 is safe with us.)

A Go app is a program that keeps running

A Go web app is a single compiled program. You start it, it boots once, opens its database connections once, and then sits there handling requests for days or weeks, many of them at the same time.

flowchart TB
  accTitle: Request lifecycle, PHP-FPM compared with Go
  accDescr: With PHP-FPM, every request boots the app, handles the request, then throws everything away. With Go, the program boots once and every request goes through the same running process, which keeps shared state such as the database pool and caches between requests.
  subgraph php ["PHP-FPM: on every request"]
    direction LR
    R1([Request]) --> B1[Boot app] --> H1[Handle it] --> D1[Throw it all away]
  end
  subgraph go ["Go: boot once, serve forever"]
    direction LR
    Q([Requests 1, 2, 3…]) --> Hd[Handler] <--> St[(Shared state: DB pool, caches)]
    S([Start binary]) --> B2[Boot app once] --> St
  end
  php ~~~ go
  style php stroke:#4F5394,stroke-width:2px
  style go stroke:#00728F,stroke-width:2px

So a variable declared outside your handler isn't reset between requests. It's just... still there, like that one .env value nobody remembers setting. Check the Interchange tab (the side-by-side PHP⇄Go view you'll get in every lesson): the same visit counter gives you 1 every time in PHP, and 1, 2, 3… in Go.

PHP-FPM, 09:00:00Boots app. Serves one request. Forgets everything.
PHP-FPM, 09:00:01Boots app. Serves one request. Forgets everything.
PHP-FPM, 09:00:02You get the idea.
Go, 09:00:00Boots app.
Go, 3 weeks laterStill running. Remembers everything. Including that cache you forgot to clear.
Live departures from the request lifecycle.

Mind the gap

Shared state is Go's superpower and its sharpest edge. Anything you put in a package-level variable is shared by every request, at the same time, for as long as the process runs. That's great for a connection pool or a cache, and a disaster for "the current user". Units 6 and 8 cover how to do this safely.

What you get for that trade

  • One file to deploy. go build produces a single executable with no runtime to install, no vendor/ to upload and no FPM pool to tune. Copy it to a server and run it.
  • Concurrency built in. Handling thousands of requests, or fanning a job out across 50 workers, is a language feature (goroutines and channels), not a queue package and a supervisor config.
  • Mistakes caught before deploy. It's compiled and statically typed, so a typo in a variable name is a build error, not a 500 in production.
  • A small language. Go has 25 keywords. There's usually one obvious way to do something, and gofmt ends the style arguments.
  • A serious standard library. HTTP servers, JSON, testing and crypto are all in the box.

Where PHP is still the better choice

This isn't a "PHP is dead" course. (The name's a pun, honest.) If you're building a server-rendered app with auth, queues, mail, an admin panel and a dozen integrations, Laravel or Symfony will get you there faster than Go will. Go has no Eloquent, no Artisan and a lot less magic, and its error handling is famously wordy. You'll type if err != nil so often your fingers will do it in your sleep.

Where Go earns its place is next to your PHP app: the queue worker that needs real concurrency, the API that needs to be fast and boring, the CLI tool your team runs everywhere. That's where this course is aimed.

Laravel corner

If you've used Laravel Octane (Swoole, RoadRunner or FrankenPHP), you've already met this model. Octane boots your app once and keeps it in memory, which is exactly why its docs warn you about state leaking between requests. Go works like that all the time, except the language and tooling were designed around it from day one.

Recap

  • PHP-FPM is shared-nothing: every request boots, runs and is thrown away.
  • A Go program boots once and serves every request from the same process, so state persists and is shared.
  • Go buys you single-binary deploys, built-in concurrency and compile-time safety. PHP still wins on framework-heavy web apps.

1State between requests

PHPcounter.php
<?php
// Even a static is reset: every request starts from scratch.
static $count = 0;
$count++;

echo "Visit number {$count}"; // always 1
GOcounter.go
var count int // lives as long as the process

func handler(w http.ResponseWriter, r *http.Request) {
	count++ // 1, 2, 3… across requests
	fmt.Fprintf(w, "Visit number %d", count)
}

Mind the gap

Even a static doesn't survive a PHP request. In Go it does, though this version has a concurrency bug when two requests land at once. Unit 6 fixes it.

2The server is the program

PHPhello.php
<?php
// nginx + PHP-FPM boot this file on every request.
echo "Hello from PHP";
GOhello.go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello from Go")
	})
	// No nginx or FPM required: the program *is* the server.
	http.ListenAndServe(":8080", nil)
}