Learn Go concurrency: 27 real patterns run in slow motion, and you step through the source like a debugger.
Goroutine Lab is a hands-on visualizer for Go concurrency. Every pattern is
genuine Go — real goroutines, channels, select, context, and sync
primitives — instrumented so your browser can animate what the runtime is
actually doing: goroutines starting and blocking, values flowing through
channels, mutexes being held, contexts cancelling.
Then it goes one step further. You can step through the Go source like a debugger — set breakpoints on real statements, single-step, and watch the highlighted line move in lock-step with the animation. The instrumentation is hidden, so what you read is the clean, interview-defensible concept code.
The Go module is named
goroutinelab; the repository isgoroutine-lab.
┌────────────┐ goroutines run for real ┌──────────────────────┐
│ Go backend │ ───── events over SSE ────▶ │ React (SVG) live viz │
│ 27 demos │ go_start / chan_send / … │ + source debugger │
└────────────┘ └──────────────────────┘
default.demo-upload.mp4
One pattern from each of the six categories — goroutines/WaitGroup, a buffered channel, select, a worker pool, a mutex, and context cancellation — each run end-to-end with the Go source stepping alongside.
Concurrency is the part of Go that's easy to write and hard to reason about. Textbooks show the code; they can't show the interleaving — which goroutine is parked, what's sitting in a channel buffer, who holds the lock, when a context cancellation actually propagates.
Goroutine Lab makes that interleaving visible. Each demo narrates itself through
a small Tracer, and the backend paces the run with real, cancellable sleeps so
it unfolds over a few seconds instead of microseconds. Because the demos are
real Go (not a simulation), what you watch is the code that runs — and the
source debugger maps every animation step back to the exact line that produced
it, using Go's own go/ast to keep the mapping honest.
It's built as a companion for learning Go concurrency deeply enough to defend every line in an interview.
- Backend: Go standard library only — no third-party modules.
- Transport: Server-Sent Events (one dependency-free, long-lived HTTP response) rather than WebSocket.
- Frontend: React + Vite, dependency-free SVG (no charting/animation libs).
Prerequisites: Go 1.23+ and Node 20+ for local development, or Docker for the one-command path.
git clone https://github.com/burrows99/goroutine-lab.git
cd goroutine-labdocker compose up --build
# frontend → http://localhost:5173
# backend → http://localhost:8080# terminal 1 — backend (Go 1.23+)
cd backend
go run ./cmd/server # or: go run -race ./cmd/server
# terminal 2 — frontend (Node 20+)
cd frontend
npm install
npm run dev # opens http://localhost:5173A Makefile wraps the common tasks: make up (Docker), make backend,
make race, make frontend, make vet, make build.
The frontend proxies /api and /healthz to the backend, so you only open
:5173 in the browser.
- Open
http://localhost:5173and pick a pattern from the sidebar. - Press Run. Watch the goroutines, channels, and messages come alive: green = running, amber = blocked, grey = done; tokens fly along channels; sync-primitive nodes light up as they're held.
- Open the Go source tab to read the pattern — and drive it like a debugger.
The source debugger
- Live highlighting — as the run plays, the currently-executing Go statement is highlighted and auto-scrolled into view.
- Step controls — a transport bar with restart · back · play · step · step-over. Stepping advances one concept line at a time (several runtime events on one line collapse into a single step), so the highlight always moves in step with the picture.
- Breakpoints — click the gutter next to any line that actually runs a step
(a send, a receive, a spawn, a lock, the critical section, a
cancel()…) to set a breakpoint; playback halts exactly on that line, and Play doubles as continue to next breakpoint. The dot never moves and never lands on a brace, a declaration, or a dead line — the breakpoint stops precisely where it sits. - Clean concept view — the tracer hooks (
t.Spawn,t.Send, …) live on their own lines and are stripped from the displayed source, along with thet *trace.Tracerparameter and the pacing calls, so what you read is pure, idiomatic Go — no instrumentation leaks in. Every event is mapped onto the exact statement it narrates (the hook sits directly on that statement), so highlight and breakpoints are precise, not heuristic. The same pure source is also materialized intobackend/internal/concepts/— one compilable package per pattern — so you can read (andgo build) the concept on its own.
Each pattern also ships How it works and Interview notes tabs.
27 runnable demos across six categories:
- Goroutines & WaitGroups — goroutines +
sync.WaitGroup - Channels — unbuffered (rendezvous) · buffered ·
close+range+ comma-ok - Select — multiplex · non-blocking (
default) · timeout (time.After) · ticker - Core Patterns — generator · pipeline · fan-out/fan-in · worker pool · semaphore (bounded concurrency) · rate limiting (token bucket) · or-done · tee · pub/sub
- Sync Primitives — mutex & the data race ·
RWMutex·sync.Once· atomics ·sync.Cond - Context & Lifecycle —
WithCancel·WithTimeout· errgroup (bounded, first-error-cancels) · graceful shutdown · goroutine leak (and the fix)
backend/
cmd/server/main.go entrypoint (graceful shutdown via signal.NotifyContext)
cmd/genconcepts/main.go generates internal/concepts/ from the instrumented demos
internal/trace/trace.go the dedicated event library (Tracer, Event, source-line capture)
internal/patterns/*.go one file per demo (+ registry.go: catalog + go/ast strip/map)
internal/patterns/helpers.go pace(): the pure, speed-scaled sleep (no tracer)
internal/concepts/<name>/ generated pure-Go concept, one compilable package per pattern
internal/server/server.go catalog + SSE routes
frontend/
src/App.tsx layout, run orchestration, the debugger play-head
src/api.ts SSE stream client (fetch + AbortController)
src/components/Visualizer.tsx animated SVG topology engine
src/components/CodeBlock.tsx source view: line highlight + breakpoint gutter
src/components/*.tsx sidebar, controls, transport, event log, info panel
docker-compose.yml backend + frontend, one command
trace— the dedicated event library. ATracerthat pattern code calls to narrate itself (Spawn,Send,Recv,Block,Lock,Cancel, …); each call becomes a JSONEventstamped with the source line it came from. Safe for concurrent use. Pacing is deliberately not here — the watchable, cancellable delay ispatterns.pace(ctx, d), a tracer-free helper that scales by the run speed carried on the context — so the concept code stays instrumentation-free.patterns— each demo registers itself and exposesRun(ctx, *Tracer). Narration hooks are written as their own statements sitting directly on the concept operation they describe. The source is//go:embed-ed; at catalog timeregistry.godrops the hook lines and the residual tracer parameter and maps each event to the exact adjacent statement — an exact, non-heuristic map. The same strip feedscmd/genconcepts, which writes the pure concept out tointernal/concepts/(go generate ./...).server—GET /api/patternsreturns the catalog;GET /api/stream?pattern=…&speed=…runs one pattern and streams its events as SSE. The run is bound to the request context, so aborting the fetch (the Cancel button, or closing the tab) cancels the context and the pattern unwinds — a live cancellation demo in itself.
@burrows99 — Raunak Burrows.
Issues and PRs are welcome — see CONTRIBUTING.md. Adding a new concurrency pattern is a great first contribution; the guide walks through it. Ask questions via GitHub issues.
MIT © Raunak Burrows.