# Goroutines and Channels

> Goroutines are cheap but leak without an exit, channels synchronise, and one goroutine owning state needs no lock. Plain counters want a mutex or atomic.

Source: https://sheng.page/en/posts/go-goroutines-and-channels/ · Published: 2025-07-27 · Author: Sheng

Go's concurrency model is more than a wrapper around threads. Effective Go summarises it as: "Do not communicate by sharing memory; instead, share memory by communicating." The advice has limits, especially for small pieces of shared state where a lock is clearer.

## Goroutines are cheap

A goroutine isn't an OS thread; it's a user-space coroutine scheduled by the runtime. The initial stack is around 2KB and grows and shrinks on demand, so running hundreds of thousands at once is routine. `go f()` starts one:

```go
go handle(conn) // one goroutine per connection, no thread pool to manage
```

Cheap isn't free, though. A leaked goroutine — one with no exit condition — holds its memory forever, and it's Go's most common leak. Every long-lived goroutine should have an exit: a cancelled `context` or a closed channel.

## Channels and select

A channel is a typed pipe between goroutines that carries synchronisation. An unbuffered send and receive must rendezvous; a buffered one blocks only when full:

```go
ch := make(chan int)     // unbuffered: send and receive must meet
ch := make(chan int, 64) // buffered: blocks once 64 are queued
```

`select` waits on several channels at once and takes whichever is ready; pair it with `ctx.Done()` for cancellation and `time.After` for timeouts:

```go
select {
case v := <-in:
    use(v)
case <-ctx.Done():
    return ctx.Err() // bail out on cancellation
}
```

## Share by communicating: the actor mindset

Rather than have several goroutines fight over a lock to mutate shared state, give one goroutine sole ownership of that state and let everyone else send it messages over a channel. The state is touched by exactly one goroutine, so no lock is needed. That's Go's take on the actor model.

Gortex's WebSocket Hub is exactly this: one `Hub.Run()` goroutine owns the whole clients map, and every register, unregister, and broadcast arrives through a channel into that single goroutine (covered in [The WebSocket Hub: Go's Take on the Actor Model](/en/posts/gortex-websocket-actor-hub/)).

## When not to use a channel

A channel isn't a cure-all. Effective Go says so itself: when all you want is to guard one variable so a single goroutine accesses it at a time, wrapping that counter in a mutex is often simpler and more direct.

Plain counters, simple flags, a small piece of guarded state → `sync.Mutex` or `atomic` is faster and clearer. Channels earn their keep for passing ownership, event streams, and cross-goroutine coordination — not every shared value. This sets up the framework posts: gortex's metrics counters use `atomic` plus sharded locks (see [Concurrent Counting and Resilience: Sharded Metrics and a Circuit Breaker](/en/posts/gortex-metrics-and-circuit-breaker/)), while the WebSocket Hub uses a channel — one project, two tools chosen by situation.

## From Effective Go: Concurrency

Effective Go's [Concurrency](https://go.dev/doc/effective_go#concurrency) opens with that "share memory by communicating" line, but the same passage adds the balancing note: channels aren't meant to replace mutexes, and sometimes guarding a small piece of state with a plain lock fits better. The point is the CSP spirit — coordinate concurrency through communication rather than reaching for locks reflexively — and for something as small as a counter, pick whichever is simplest and clearest.


## Notes

- Goroutines are cheap user-space coroutines (~2KB to start), but they leak; give each one an exit condition.
- Channels carry synchronisation; `select` waits on many, with `ctx.Done()` / `time.After` to finish up.
- The actor mindset: let one goroutine own the state and others send messages; no lock.
- Don't fetishise channels: plain counters and small guarded state are simpler with a mutex / atomic (setup for the sharded metrics and WebSocket Hub posts).
- The mantra is "share memory by communicating"; but reach for a lock when a lock is the right tool.

Further reading: the series goes on to dissect the gortex framework built on these ideas — source at [yshengliao/gortex](https://github.com/yshengliao/gortex).

---

> Ideas and technical judgement by Sheng; drafted with Claude · Go 1.24 (latest release at the time) · compiled retroactively · part of the 2026-06-13 blog renovation, paint still drying.