Day-0 → Month-3 · curriculum
Study path · Go

Go,
the language and the runtime.

One pass through the language, one pass through the runtime, one pass through the tooling. Read what to read, run what to run. Backed by the runtime source — when something is unclear, the answer lives in src/runtime/.

Tap a tier to see which sections matter most for you.


Why Go.

Go is what you reach for when you want a single static binary, fast compile times, and concurrency that does not destroy you. It is small. The spec fits in a long evening. The tooling is in the box. There is one formatter, one linter, one test runner, and one build system — all named go. The cost: the language refuses features other languages take for granted (no exceptions, no inheritance, no operator overloading, no generic methods). The benefit: a Go codebase you have not seen before is legible in five minutes.

The runtime is the second half of the story. The GMP scheduler multiplexes hundreds of thousands of goroutines onto a small set of OS threads. The garbage collector keeps stop-the-world pauses under a millisecond at almost every heap size. The toolchain knows how to cross-compile to anything, profile anything, and fuzz anything. None of these required you to install a plugin.

When not to use Go. Hard real-time (the GC, even sub-ms, is not zero). Numerical computing where SIMD-heavy libraries dominate (Rust + portable-simd is closer). Code where exhaustive enum matching matters (sum types are not idiomatic in Go). Anything that has to ship as a library to a non-Go runtime — Go's runtime ships with the binary and is not pleasant to embed.

The twelve mental models you must build.

These twelve concepts cover ~95% of the language and runtime surface. Get them in your bones in the first two weeks; the rest is library API. The runtime-tier ones (GC, escape analysis) come later but pay back enormously.

01 Goroutine Day-zero

A user-space thread, ~2 KB stack at start, multiplexed onto OS threads by the runtime scheduler.

02 Channel Day-zero

A typed, FIFO synchronisation primitive. The "share by communicating" idiom that makes Go concurrent code reasonable.

03 select Day-zero

A blocking switch over channel operations. Combined with context cancellation, the foundation of every request-scoped pipeline.

04 Interface Day-zero

Structural typing — a type satisfies an interface implicitly if it has the methods. No "implements" keyword.

05 Slice Day-zero

A view into an underlying array — pointer + length + capacity. The data structure used 90% of the time.

06 Map Practitioner

Hash table, not safe for concurrent write. sync.Map exists for the few cases where it helps.

07 context.Context Practitioner

A cancellation tree threaded through every blocking call. Deadlines, values, child cancellation — all via one interface.

08 error Day-zero

A built-in interface with one method: Error() string. Errors are values; you check them, wrap them, never throw them.

09 Module Practitioner

A versioned unit of code, declared by go.mod, identified by an import path. The end of GOPATH.

10 Garbage collector Runtime

Concurrent, tri-color, mark-sweep. Sub-millisecond stop-the-world pauses. The thing that lets you not think about memory.

11 Escape analysis Runtime

Compile-time decision: does this allocation escape the function? If not, stack-allocate. If yes, heap. The single biggest performance lever in Go.

12 Generics Practitioner

Type parameters — added in Go 1.18 (2022). Constraints via interfaces. Used sparingly in idiomatic Go; common in libraries.

Day-zero — your first hour.

One hour. Install the toolchain, write the canonical "echo server", run the race detector against it. You'll have exercised six of the twelve mental models by the end.

# 1. Install Go (any platform)
brew install go              # macOS
sudo apt install golang-go   # Debian / Ubuntu
# or download from https://go.dev/dl/

go version
# go version go1.22.x linux/amd64

# 2. New module, single file
mkdir hello && cd hello
go mod init example.com/hello
cat > main.go <<'EOF'
package main

import (
    "fmt"
    "net/http"
    "sync/atomic"
)

func main() {
    var n int64
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        c := atomic.AddInt64(&n, 1)
        fmt.Fprintf(w, "request #%d: %s %s\n", c, r.Method, r.URL.Path)
    })
    fmt.Println("listening on :8080")
    http.ListenAndServe(":8080", nil)
}
EOF

# 3. Run it
go run main.go
# in another terminal:
curl localhost:8080/foo

# 4. Build a static binary
go build -o hello

# 5. Race detector — every Go programmer's first habit
go run -race main.go

# 6. Cross-compile to Linux from anywhere
GOOS=linux GOARCH=amd64 go build -o hello-linux

Done. You've used the toolchain, the module system, the standard library's net/http, atomic operations, and the race detector. The next step is muscle memory.

Day-1 to Day-7 — the muscle memory.

Seven days, ~30–60 minutes each. Each day adds one capability. By Friday you can ship a Go service.

Day 1
A Tour of Go

Run through go.dev/tour. Modify each snippet and re-run. About 90 minutes; the standard intro.

→ go.dev
Day 2
Goroutines and channels

Build a small worker pool: N goroutines pulling from one channel, returning results on another. Use sync.WaitGroup.

→ Semicolony
Day 3
select + context

Add cancellation to your worker pool with context.WithTimeout. Confirm goroutines exit when the parent cancels.

→ go.dev
Day 4
Interfaces and method sets

Build an io.Reader/io.Writer pair. Then a Sort interface implementation. Internalise structural typing.

→ go.dev
Day 5
Errors, wrapping, panics

Use errors.Is, errors.As, fmt.Errorf("...%w...", err). Try recover() in a deferred function. Decide when not to use it.

→ go.dev
Day 6
Tests, benchmarks, fuzz

Write _test.go files. Run go test -race. Add a Benchmark. Try go test -fuzz on a parser. The tooling is the appeal.

→ go.dev
Day 7
Modules, builds, cross-compilation

go mod init, go get, go build for darwin/amd64 from a Linux box. Read your go.sum. Inspect the binary with go tool nm.

→ go.dev

Week-1 to Month-3 — pick a track.

After week one you can read most Go you encounter. The next two months should be one track at a time, depth-first. Don't try to learn the runtime and the standard library in the same fortnight.

Concurrency

Goroutines, channels, sync.Mutex / sync.RWMutex / sync.Once / sync.Map / sync.Pool. The Go memory model. The race detector.

→ Semicolony asset
Standard library

net/http, encoding/json, database/sql, io / bufio / fmt, time, regexp, html/template. The stdlib is half the language.

→ Reference
Performance

go test -bench, pprof (CPU + heap + goroutine + mutex profiles), the trace tool, escape analysis, inline budgets, BCE (bounds-check elimination).

→ Reference
The runtime

GMP scheduler, goroutine stacks (segmented → contiguous), the GC pacer, the network poller, sysmon.

→ Semicolony asset
Generics

Type parameters, constraints, when to use them (data structures, algorithm functions) and when not to (simple wrappers).

→ Reference
Web services

net/http with the new ServeMux (1.22+), middleware patterns, graceful shutdown, the pgx Postgres driver, connection pooling.

→ Reference
Tooling and workflow

gofmt, goimports, go vet, staticcheck, golangci-lint, gopls, dlv (look at), the go release workflow.

→ Reference

The runtime — read the source.

The Go runtime is roughly 60,000 lines of Go (with a small amount of assembly per architecture) and lives in src/runtime/. It is exceptionally well-commented. Once you can read it, "what does my goroutine actually do" stops being a mystery.

The four files worth reading first, in order:

runtime/proc.go
The GMP scheduler. Goroutines (Gs), OS threads (Ms), processors (Ps). Work stealing. Park / unpark. The single most important file in the runtime.
runtime/mgc.go
The concurrent tri-color mark-sweep GC. The pacer (how it decides when to run). Write barriers. The reason your STW pauses are sub-ms.
runtime/chan.go
Channels. The hchan struct, the sudog wait queue, the send/receive fast paths and slow paths. Reading this for an hour replaces a year of confused use.
runtime/netpoll.go
The platform-specific I/O multiplexer (epoll on Linux, kqueue on BSD/macOS, IOCP on Windows). How net.Conn.Read parks a goroutine without parking its M.

The runtime is also where GODEBUG lives. Setting GODEBUG=schedtrace=1000,gctrace=1 on a running binary prints scheduler and GC events as they happen. Read the output once and the runtime stops being abstract.

A second-pass tour, after you've read the four files: Russ Cox's Go's GMP scheduler talk (2018), Vyukov's original work-stealing scheduler design doc, and the GC pacer redesign blog post by Michael Knyszek (2022).

The four books that matter.

2015 · Addison-Wesley
Donovan & Kernighan — The Go Programming Language

The book. Co-written by one of the language designers and the author of K&R. Dated in places (pre-modules, pre-generics) but the canonical narrative.

2nd ed · 2024 · Manning
Jon Bodner — Learning Go

The "modern Go" book. Modules, generics, the new ServeMux. If you read one book, this is the most up-to-date answer.

2017 · O'Reilly
Katherine Cox-Buday — Concurrency in Go

The book on goroutines, channels, and concurrency patterns. Pipelines, fan-in / fan-out, error propagation, context. Pre-generics; still the right book on the topic.

2020 · Manning
Teiva Harsanyi — 100 Go Mistakes and How to Avoid Them

The compendium of footguns. Once you've internalised these, you've leapfrogged a year of "why is my Go code subtly wrong".

Honourable mentions: Go in Action (Kennedy), The Go Workshop (Quinn et al), and Jon Bodner's Learning Go 1st edition (still excellent if you can find it cheap).

Courses, tutorials, and interactive learning.

Free
Paid (worth it)

The documentation canon.

Talks worth your evening.

Newsletters to subscribe to.

Go toolchain cheat sheet.

Thirty commands cover ~90% of daily Go work. Memorise these; everything else is go help.

go run main.goCompile + run in one step.
go buildCompile to a single static binary.
go test ./...Run all tests in the module.
go test -race ./...Tests with the race detector.
go test -bench=. -benchmemBenchmarks with allocation reporting.
go test -fuzz=Fuzz -fuzztime=10sRun a fuzz test for 10 seconds.
go vet ./...Static analysis, ships with the toolchain.
gofmt -d .Show formatting diffs without writing.
go mod init example.com/mCreate a new module.
go mod tidyAdd missing, remove unused, in go.mod and go.sum.
go mod why -m <module>Why is this dependency in my graph?
go get -uUpgrade direct dependencies in this module.
go install <pkg>@latestInstall a tool to $GOBIN.
go env -w GOFLAGS=-mod=modPersistently set a GOFLAGS env var.
go tool pprof <binary> <profile>Open a pprof profile.
go tool trace <trace.out>Open the runtime tracer in a browser.
go tool nm <binary>Symbol table — what got linked into the binary.
go tool objdump -s <fn>Disassemble a function from the binary.
go doc <pkg>.<symbol>Local godoc for a symbol.
go list -m allPrint every module in the build graph.
GOOS=linux GOARCH=amd64 go buildCross-compile to Linux/amd64.
GODEBUG=schedtrace=1000 ./binPrint scheduler stats every 1000ms.
GODEBUG=gctrace=1 ./binPrint GC events as they happen.
go test -coverprofile=c.out && go tool cover -html=c.outTest coverage in your browser.
errcheck ./...Find ignored errors (third-party).
staticcheck ./...Deeper static analysis (third-party).
dlv debug ./cmd/app -- arg1Step-debug with look at.
go work init && go work use ./a ./bMulti-module workspace.
go version -m <binary>Show the build info compiled into a Go binary.
go run -gcflags="-m" main.goShow escape-analysis decisions.

Common mistakes you'll make on day 30.

Patterns that bite every Go programmer. Read these once now; come back when you've actually been bitten.

Capturing the loop variable in a goroutine
Pre-Go 1.22: every goroutine sees the final loop value. Either copy: `i := i` inside, or upgrade to Go 1.22+ where the language fixed it. Old code is still everywhere; the race detector finds it.
nil channel send / receive
A send to a nil channel blocks forever. A receive from a nil channel blocks forever. Sometimes intentional (disabling a select case); usually a bug.
Forgetting to close a channel
You only close a channel from the sender side, and only when no further sends will happen. Closing a closed channel panics.
Goroutine leaks via context
A goroutine that does `<-ch` in a select but never gets a context.Done() case will block forever on shutdown. Always include the cancel case.
Modifying a slice while iterating
append() can reallocate. The loop sees the original backing array. Iterate the index, not the value, when mutating.
Map concurrent write panic
Plain map is not safe for concurrent write. Either guard with a sync.RWMutex or use sync.Map (only when access is mostly disjoint per key).
Defer in a long loop
Each defer adds to the function's defer stack; they only run when the function returns. Use a closure or refactor for hot paths.
Comparing time.Time with ==
Wallclock vs monotonic. Use t1.Equal(t2) — it normalises monotonic-clock readings.
Ignoring the return value of bytes.Buffer.Write
It always returns nil error, but Go community style still requires you to handle it. Use _ = w.Write(...) for clarity, or write a helper.
Returning interfaces, accepting structs
Idiom is reversed: accept interfaces, return concrete types. Returning io.Reader from your own function blurs which type the caller actually got.

Semicolony's own Go assets.

Pair these with the curriculum above. The simulators in particular let you push on the abstractions while the explanation is still fresh.

Guides (how it works)
Simulators
Tools

The three-month roadmap.

Day 0 Install. Tour of Go. First HTTP server. Day 7 Goroutines, channels, context. Tests + race. Week 4 A track of depth — concurrency or stdlib or web. Month 2 pprof + escape analysis. The race detector at scale. Month 3 Read runtime/proc.go. Then runtime/chan.go. Then mgc.go.DAY · 0 → MONTH 3

Practice deck.

Ten cards covering the questions Go interviewers ask, the things that trip Go programmers in production, and the runtime trivia that separates "I write Go" from "I read Go".

Card 1 of 10
A goroutine reads from a channel that the sender forgot to close. What happens?
Suggested sequences

Reading progressions

Three ordered paths through this material — pick the one that matches where you are.

Path 01 · Language
Core language in order

From the first program to idiomatic concurrency — the right reading sequence for the language.

  1. Hello, Go — setup & first program
  2. Variables & types
  3. Goroutines — concurrency primitive
  4. Select & synchronisation
  5. Go Channels — how they work
Path 02 · Runtime
Scheduler, GC & memory

The M:P:G scheduler, the GC pacer, and escape analysis — what the runtime is actually doing.

  1. The M:P:G Scheduler
  2. Goroutines — stack growth & parking
  3. Goroutine Scheduler Simulator ↗
  4. Garbage Collection
  5. Memory Allocation
Path 03 · Production
Testing, modules & idioms

The practices that distinguish Go written to run in production from Go written to pass a tutorial.

  1. Testing — table tests & benchmarks
  2. Idiomatic Go — common patterns
  3. Modules & packages
  4. Channel internals

Keep going.

Go rewards reading. The stdlib is well-written and short; the runtime is well-commented; the spec is small. The single best practice in Go is "read the source of the package you import", which is feasible because the source is on your disk after one go get.

Pick a real project: a CLI, a small HTTP service, a Kubernetes controller. Ship it. Then read your own code six months later and be embarrassed by what you'd write differently. That feedback loop is the best Go education there is.