If your whole career has been one-request-one-process — which describes most PHP developers, including the one I used to be — then concurrency feels like a foreign country. Goroutines look like magic for about a week, and then like footguns for the month after. The trick is to stop thinking of them as something new and start mapping them onto backend concepts you already understand.
A goroutine is a worker; a channel is the queue
You've already built this system. You had a queue (Redis, a database table, RabbitMQ) and a pool of workers pulling jobs off it. Go just moves that pattern inside a single process.
jobs := make(chan int) // the queue
go func() { // a worker
for j := range jobs {
process(j)
}
}()
jobs <- 42 // enqueue a job
A channel is a typed, in-memory queue. A goroutine is a worker that reads from it. You already know how this behaves — what happens when the queue is full, what happens when there are no workers — because you've debugged it in production with heavier tools.
Closing a channel is "no more jobs coming"
The part that trips people up is the close signal. When the producer is done, it closes the channel, and the for range loop in each worker ends cleanly. It's the in-process equivalent of telling your workers the shift is over.
Don't close a channel from the consumer side, and don't close it twice — that's a panic. The producer owns the close, the same way the thing filling your job queue owns "we're done."
Shared state still needs a lock
Here's where the PHP instinct fails you, and it's the one thing worth burning into memory: in PHP, two requests never touch the same variable, because they're different processes. In Go, two goroutines can hammer the same map at the same time.
- Protect shared state with a
sync.Mutex, exactly like a lock around a critical section. - Or — more idiomatic — don't share the state at all. Pass it through a channel so only one goroutine ever owns it.
"Don't communicate by sharing memory; share memory by communicating" sounds like a slogan until the day a data race teaches you what it means.
The model in one sentence
Goroutines are workers, channels are queues, and shared memory still needs a lock. Everything else — select, buffered channels, context cancellation — is detail you can layer on once that core picture is solid. Build on the queue-and-worker intuition you already have, and concurrency stops being magic and starts being engineering.