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.
A user-space thread, ~2 KB stack at start, multiplexed onto OS threads by the runtime scheduler.
02 Channel Day-zeroA typed, FIFO synchronisation primitive. The "share by communicating" idiom that makes Go concurrent code reasonable.
03 select Day-zeroA blocking switch over channel operations. Combined with context cancellation, the foundation of every request-scoped pipeline.
04 Interface Day-zeroStructural typing — a type satisfies an interface implicitly if it has the methods. No "implements" keyword.
05 Slice Day-zeroA view into an underlying array — pointer + length + capacity. The data structure used 90% of the time.
06 Map PractitionerHash table, not safe for concurrent write. sync.Map exists for the few cases where it helps.
07 context.Context PractitionerA cancellation tree threaded through every blocking call. Deadlines, values, child cancellation — all via one interface.
08 error Day-zeroA built-in interface with one method: Error() string. Errors are values; you check them, wrap them, never throw them.
09 Module PractitionerA versioned unit of code, declared by go.mod, identified by an import path. The end of GOPATH.
10 Garbage collector RuntimeConcurrent, tri-color, mark-sweep. Sub-millisecond stop-the-world pauses. The thing that lets you not think about memory.
11 Escape analysis RuntimeCompile-time decision: does this allocation escape the function? If not, stack-allocate. If yes, heap. The single biggest performance lever in Go.
12 Generics PractitionerType 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.
Run through go.dev/tour. Modify each snippet and re-run. About 90 minutes; the standard intro.
→ go.devBuild a small worker pool: N goroutines pulling from one channel, returning results on another. Use sync.WaitGroup.
→ SemicolonyAdd cancellation to your worker pool with context.WithTimeout. Confirm goroutines exit when the parent cancels.
→ go.devBuild an io.Reader/io.Writer pair. Then a Sort interface implementation. Internalise structural typing.
→ go.devUse errors.Is, errors.As, fmt.Errorf("...%w...", err). Try recover() in a deferred function. Decide when not to use it.
→ go.devWrite _test.go files. Run go test -race. Add a Benchmark. Try go test -fuzz on a parser. The tooling is the appeal.
→ go.devgo 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.devWeek-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.
Goroutines, channels, sync.Mutex / sync.RWMutex / sync.Once / sync.Map / sync.Pool. The Go memory model. The race detector.
→ Semicolony assetnet/http, encoding/json, database/sql, io / bufio / fmt, time, regexp, html/template. The stdlib is half the language.
→ Referencego test -bench, pprof (CPU + heap + goroutine + mutex profiles), the trace tool, escape analysis, inline budgets, BCE (bounds-check elimination).
→ ReferenceGMP scheduler, goroutine stacks (segmented → contiguous), the GC pacer, the network poller, sysmon.
→ Semicolony assetType parameters, constraints, when to use them (data structures, algorithm functions) and when not to (simple wrappers).
→ Referencenet/http with the new ServeMux (1.22+), middleware patterns, graceful shutdown, the pgx Postgres driver, connection pooling.
→ Referencegofmt, goimports, go vet, staticcheck, golangci-lint, gopls, dlv (look at), the go release workflow.
→ ReferenceThe 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.
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.
The "modern Go" book. Modules, generics, the new ServeMux. If you read one book, this is the most up-to-date answer.
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.
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.
- go.devA Tour of GoThe official interactive tour. ~90 minutes. Run snippets in the browser, modify, re-run. The standard intro.
- go.devEffective GoThe "how to write idiomatic Go" document. Reads like a style guide written by the language designers, because it is.
- Mark McGranaghanGo by ExampleA reference of small annotated programs covering most of the language. Faster than the spec when you've forgotten how channels work.
- ExercismGo track~100 exercises with mentor feedback. The right shape for "I want to actually write some Go before I read more".
- JustForFuncFrancesc Campoy's video seriesLong-form Go video tutorials from a former Go DevRel lead. The "implementation of X in Go" episodes are gold.
The documentation canon.
- go.devThe Go Programming Language Specification~80 pages. The whole language. Read it once cover-to-cover; come back when you have a "is this legal?" question.
- pkg.go.devStandard library referenceThe stdlib is half the language. Browse net/http, encoding/json, io, sync, context. Each package's source is one click away.
- go.devThe Go BlogOfficial posts on language features, runtime improvements, and standards changes. The pace is slow and the signal-to-noise is high.
- go.devThe Go Memory ModelRead once when you're tempted to share state without locks. The synchronisation rules — happens-before, channel ordering, sync.Mutex semantics — are formal here.
- go.devEffective GoThe other half of the canon. Naming, formatting, error handling, concurrency idioms.
- github.com/golang/goThe Go WikiCommunity-maintained. CodeReviewComments, CommonMistakes, and a long FAQ are the high-value entries.
Talks worth your evening.
- Robert Griesemer · 2017Prototype your designOne of Go's three creators on language design. Why the initial Go didn't have generics; why it eventually did.
- Rob Pike · 2012Concurrency is not ParallelismThe talk. Channels, gophers, mailboxes. Forty minutes. The single best framing of why Go's concurrency model is what it is.
- Russ Cox · 2018The Go schedulerLong-form on the GMP scheduler internals. Pair with the runtime/proc.go read-through.
- Brad Fitzpatrick · 2014Go's net packageA tour of the net package by its primary author. A shape-of-things-to-come talk; everything in net/http still derives from these decisions.
- GopherCon · YouTubeGopherCon recordingsAnnual conference. Talks on the runtime, the toolchain, performance, and case studies. The "performance tuning" track ages best.
Go toolchain cheat sheet.
Thirty commands cover ~90% of daily Go work. Memorise these; everything else is go help.
| go run main.go | Compile + run in one step. |
| go build | Compile to a single static binary. |
| go test ./... | Run all tests in the module. |
| go test -race ./... | Tests with the race detector. |
| go test -bench=. -benchmem | Benchmarks with allocation reporting. |
| go test -fuzz=Fuzz -fuzztime=10s | Run 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/m | Create a new module. |
| go mod tidy | Add missing, remove unused, in go.mod and go.sum. |
| go mod why -m <module> | Why is this dependency in my graph? |
| go get -u | Upgrade direct dependencies in this module. |
| go install <pkg>@latest | Install a tool to $GOBIN. |
| go env -w GOFLAGS=-mod=mod | Persistently 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 all | Print every module in the build graph. |
| GOOS=linux GOARCH=amd64 go build | Cross-compile to Linux/amd64. |
| GODEBUG=schedtrace=1000 ./bin | Print scheduler stats every 1000ms. |
| GODEBUG=gctrace=1 ./bin | Print GC events as they happen. |
| go test -coverprofile=c.out && go tool cover -html=c.out | Test coverage in your browser. |
| errcheck ./... | Find ignored errors (third-party). |
| staticcheck ./... | Deeper static analysis (third-party). |
| dlv debug ./cmd/app -- arg1 | Step-debug with look at. |
| go work init && go work use ./a ./b | Multi-module workspace. |
| go version -m <binary> | Show the build info compiled into a Go binary. |
| go run -gcflags="-m" main.go | Show 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.
- → Go channels — buffered, unbuffered, the sender/receiver wait queues
- → Garbage collection — Go's tri-color mark-sweep, the pacer
- → Event loops — Go's netpoll vs Node's libuv
- → Memory allocation — escape analysis, the Go heap
- → Thread pools — what GMP is doing in user space
The three-month roadmap.
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".
Reading progressions
Three ordered paths through this material — pick the one that matches where you are.
From the first program to idiomatic concurrency — the right reading sequence for the language.
The M:P:G scheduler, the GC pacer, and escape analysis — what the runtime is actually doing.
The practices that distinguish Go written to run in production from Go written to pass a tutorial.
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.