Don’t communicate by sharing memory; share memory by communicating.
Building blocks
| Tool | What it is |
|---|---|
go f() |
Starts a goroutine: a function running concurrently, with a few KB of stack. Thousands are cheap. |
chan T |
Typed pipe between goroutines. Unbuffered = handoff, buffered = queue. |
select |
Wait on several channel operations at once; default makes it non-blocking. |
context.Context |
Carries cancellation, deadlines and request-scoped values down the call tree. |
sync.Mutex / RWMutex |
Plain locks, for when shared state is simpler than channels. |
sync.WaitGroup |
Wait for a group of goroutines to finish. |
sync.Once / OnceValue |
Lazy one-time initialisation. |
errgroup (golang.org/x/sync) |
WaitGroup + first error + cancel the rest. |
ch := make(chan int)
go func() { ch <- compute(1) }()
result := <-ch
Pattern: fan-out with a slow-consumer policy
One channel per subscriber, and a non-blocking send so a slow client can never stall the publisher.
type Conn struct {
send chan Event // bounded buffer
closed chan struct{}
}
// Broadcast never blocks: a subscriber that cannot keep up is dropped.
func (h *Hub) Broadcast(id SessionID, ev Event) {
h.mu.RLock()
conns := make([]*Conn, 0, len(h.conns[id]))
for c := range h.conns[id] {
conns = append(conns, c)
}
h.mu.RUnlock()
for _, c := range conns {
select {
case c.send <- ev:
default:
h.drop(c) // buffer full: slow consumer
}
}
}
Each connection has its own writer goroutine serving several event sources:
for {
select {
case <-ctx.Done():
return
case <-c.closed:
return
case ev := <-c.send:
write(ctx, ws, ev)
case <-ping.C:
ws.Ping(ctx)
}
}
context.Context
- First parameter of anything that does I/O or may block:
func (s *Store) Quiz(ctx context.Context, id QuizID). - When the HTTP request ends,
r.Context()is cancelled, so everything downstream stops. - Derive, don’t store:
context.WithTimeout,WithCancel,WithDeadline. Alwaysdefer cancel().
wctx, cancel := context.WithTimeout(ctx, writeTimeout)
defer cancel()
return ws.Write(wctx, msg)
Pattern: periodic background work
// every runs f now and then every d until ctx ends.
func every(ctx context.Context, d time.Duration, f func()) {
t := time.NewTicker(d)
defer t.Stop()
for {
f()
select {
case <-ctx.Done():
return
case <-t.C:
}
}
}
go every(ctx, 10*time.Minute, sweepIdleSessions)
Pattern: graceful shutdown
srv := &http.Server{Addr: addr, Handler: h, ReadHeaderTimeout: 10 * time.Second}
errCh := make(chan error, 1)
go func() { errCh <- srv.ListenAndServe() }()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
select {
case err := <-errCh:
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
case <-stop:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return srv.Shutdown(ctx) // stops accepting, drains in-flight requests
}
signal.NotifyContext is a shorter alternative when all you need is a cancelled context.
Pitfalls
- Maps aren’t safe for concurrent use. Concurrent writes crash the program (
fatal error: concurrent map writes). Guard them with a mutex. - Goroutine leaks. Every goroutine needs a way to end: a closed channel or a cancelled context.
- Copy under the lock, work outside it. See
Broadcastabove: collect subscribers underRLock, send after unlocking. - Closing channels: only the sender closes; sending on a closed channel panics.
- Run tests with
-race. The race detector finds unsynchronised access at runtime:
go test -race ./...