Go Developer Beginner to Expert
A roadmap for Go from syntax and the type system through goroutines and channels, building HTTP services, databases, testing, and running Go binaries in production.
Go is small enough that you can read the whole specification in an afternoon, and that is precisely why people plateau. The syntax stops being the obstacle within a week, and the remaining difficulty is idiom: what to do with errors, when a channel is the wrong tool, and how to keep concurrency bounded.
Work through the stages in order. Two of them are worth resisting the urge to skip. Interfaces, because implicit satisfaction is the thing that makes Go code testable without a mocking framework, and it is genuinely different from every language with an implements keyword. And concurrency, because goroutines are so cheap that the mistake is not using too few, it is spawning them without a ceiling.
That diagram is the argument for the last two stages. The static binary is an abstract fact until you see a 12 MB image with no shell in it. context.Context is boilerplate until a Lambda deadline arrives as a cancelled context and the code that ignores it keeps billing you. And SetMaxOpenConns is a line in the docs until an autoscaling group opens 900 connections against a database configured for 200.
Language Foundations
Syntax, types, and zero values
Variables, constants, control flow, and the fact that every type has a usable zero value: 0, "", nil, and a struct with all fields zeroed. Designing around zero values rather than constructors is idiomatic and removes a lot of nil checking.
Slices, maps, and arrays
Understand that a slice is a view over a backing array with length and capacity. append may or may not allocate, and two slices can share memory, which is the source of most surprising mutation bugs.
Structs, methods, and pointer vs value receivers
Methods on types, and when a receiver should be a pointer: when you mutate, or when the struct is large. Mixing both on one type is a lint warning worth heeding.
Interfaces, satisfied implicitly
No implements keyword: a type satisfies an interface by having the methods. Keep interfaces small, define them where they are consumed rather than where the type lives, and accept interfaces while returning structs.
Errors are values
Return errors, do not throw them. Wrap with fmt.Errorf("...: %w", err) to preserve the chain, then inspect with errors.Is and errors.As. Learn this before adopting an error library.
Generics
Type parameters and constraints, added in 1.18. Useful for containers and a handful of helpers. Most Go code does not need them, and reaching for them early usually produces something an interface did better.
Standard Library Fluency
net/http, both directions
Write a server with http.ServeMux and handlers, and a client with a configured http.Client. Note the default client has NO timeout, which is a production incident waiting to happen.
The io interfaces
io.Reader and io.Writer are the most important abstractions in the language. Composing them means streaming through a pipeline instead of buffering a whole file in memory.
encoding/json
Struct tags, omitempty, and the difference between a missing field and a zero value. Use a pointer or json.RawMessage when that distinction matters.
time, and why monotonic matters
time.Duration arithmetic, tickers, and using time.Since for elapsed time rather than subtracting wall clocks, which can jump.
log/slog for structured logging
Structured logging in the standard library since 1.21. A JSON handler to stdout is all a container needs, and it removes a dependency most projects used to carry.
Concurrency
Goroutines and the scheduler
A goroutine costs a couple of kilobytes, so a goroutine per request is normal. The runtime multiplexes them across OS threads itself. There is no GIL and no single event loop to block.
Channels, and when not to use one
Channels transfer ownership of data. Buffered versus unbuffered, closing, and select. The idiom worth internalising early: if you are only protecting shared state, a sync.Mutex is simpler and clearer than a channel.
context.Context everywhere
Cancellation, deadlines and request-scoped values. Any function doing I/O takes a context as its first parameter. Without this, a cancelled request keeps working and you cannot shed load.
sync primitives
WaitGroup to wait for a set of goroutines, Mutex and RWMutex for shared state, sync.Once for one-time init, and errgroup for a group where the first error cancels the rest.
The race detector
Run go test -race in CI, always. It finds data races that no code review reliably catches, and a race that has never fired is not a race that is absent.
Bounded worker pools
An unbounded go f() per item is a goroutine leak and a way to exhaust a downstream dependency. Use a fixed pool reading from a channel, or a semaphore, so concurrency has a ceiling you chose.
Goroutine leak detection
A goroutine blocked forever on a channel is a leak the garbage collector cannot help with. go.uber.org/goleak in tests, and the /debug/pprof/goroutine endpoint in production.
Testing
Table-driven tests
The dominant Go idiom: a slice of cases, one loop, t.Run for subtests so failures name themselves. This replaces most of what other ecosystems use a framework for.
Interfaces as seams for fakes
Because interfaces are implicit and defined at the consumer, a hand-written fake is usually less work than a mocking library. Depend on a small interface, pass a fake in a test.
httptest for handlers and clients
httptest.NewRecorder to exercise a handler without a socket, and httptest.NewServer to give a client a real endpoint to talk to.
Benchmarks and fuzzing
go test -bench with b.N, and built-in fuzzing since 1.18. Fuzzing pays off immediately on anything that parses untrusted input.
Golden files
Compare output against a checked-in file, regenerated behind a -update flag. Well suited to templates, generated code and serialisation.
Building Services
An HTTP service without a framework
Since 1.22 http.ServeMux handles method and path patterns, so a routing library is genuinely optional. Start with the standard library and add chi or echo only when you can name the missing feature.
Middleware as handler wrapping
Middleware is a function taking an http.Handler and returning one. Logging, recovery, auth and timeouts all compose this way, with no framework involved.
Graceful shutdown
signal.NotifyContext plus server.Shutdown(ctx) so in-flight requests finish before the process exits. Without it, every deploy drops connections and the errors look like someone else's fault.
database/sql and connection pooling
database/sql with pgx, and SetMaxOpenConns set deliberately. The default is unlimited, which will exhaust your database before your service notices.
sqlc or hand-written SQL over an ORM
The Go community leans away from heavy ORMs. sqlc generates typed code from real SQL, keeping the query readable and the result typed.
gRPC and protobuf
Worth learning for service-to-service work: a schema, generated clients, and streaming. Overkill for a public API that a browser will call.
Modules and Tooling
Modules, go.mod and go.sum
go mod tidy, semantic import versioning, and why go.sum belongs in version control. Minimal version selection means Go picks the lowest version satisfying all requirements, which is different from most package managers.
Project layout, without cargo-culting
cmd/ for binaries, internal/ for packages that must not be imported externally (the compiler enforces it). Resist the elaborate layouts circulating online until the project needs them.
go vet, staticcheck, golangci-lint
go vet catches real mistakes and ships with the toolchain. Add staticcheck via golangci-lint in CI and fix what it reports rather than suppressing it.
go generate and code generation
Stringer for enums, mockgen, sqlc, protoc. Committing generated code is the Go norm, so builds do not depend on generator availability.
Cross compilation
GOOS=linux GOARCH=arm64 go build produces a binary for another platform with no toolchain gymnastics. This is how one laptop builds for Graviton.
Shipping the Binary
Static binaries and scratch images
CGO_ENABLED=0 go build -ldflags="-s -w" gives a static binary with no libc dependency, which a FROM scratch image can run alone. Images under 15 MB with almost no attack surface are routine.
Multi-stage Dockerfiles
Build in the golang image, copy only the binary into the final stage. Copy go.mod/go.sum and run go mod download before the source, so the dependency layer caches.
Embedding assets with go:embed
//go:embed puts templates, migrations and static files inside the binary, so deployment stays a single file.
Build info and versioning
Stamp a version with -ldflags="-X main.version=...", or read runtime/debug.ReadBuildInfo(). Being able to ask a running binary what it is saves real time in an incident.
Running Go in Production
Structured logging and correlation
A slog JSON handler to stdout, with a request ID carried in the context so every line for one request can be found together.
pprof, in production
Import net/http/pprof behind an internal port and you can capture a CPU or heap profile from a live process. Combined with go tool pprof, this is the best profiling story of any mainstream runtime.
Timeouts on everything
http.Server needs ReadHeaderTimeout and WriteTimeout; http.Client needs a Timeout. The zero value means wait forever, and a hung upstream then exhausts your goroutines.
The memory model in practice
Go is garbage collected with a low-latency collector. GOMEMLIMIT (1.19+) is what keeps a container from being OOM-killed, and it matters more than GOGC tuning for most services.
OpenTelemetry tracing
otel-go with context propagation, exporting to X-Ray, Tempo or Jaeger. Because context already threads through your code, adding spans is mostly mechanical.
Health checks that mean something
Separate liveness (is the process wedged) from readiness (can it serve, are its dependencies reachable). A readiness check that always returns 200 is decoration.
Comments
Was this useful?
Continue on this topic
The same subject, covered a different way from the roadmap above.
QuizKotlin: Modern JVM Language Fundamentals
Test your knowledge of Kotlin covering null safety and smart casts, data classes, coroutines and structured concurrency, extension functions, scope functions, sealed classes, and Java interoperability.
Code snippetBash Script Locking: Prevent Concurrent Runs with a PID File
A Bash snippet that uses a PID file so only one instance of a script runs at a time. Essential for cron jobs and automation scripts that must not overlap. Includes stale lock detection and cleanup on exit.
Case studyHardening a CI/CD Supply Chain to SLSA Level 3
How we made a build pipeline tamper-evident end to end: hermetic builds, KMS-signed provenance, and an admission gate that refuses anything it cannot verify.
- Cheatsheet
jq
A jq reference for engineers who live in JSON APIs and AWS CLI output. Field selection, array iteration, select and conditionals, map and reduce, string interpolation, @tsv and @csv, and the flags that make jq safe inside a shell script.
You might also enjoy
More posts on similar topics

Python Developer Beginner to Expert
Python is easy to start and deceptively deep, which produces a common shape of developer: fluent in syntax, productive in a framework, and unsure what happens when the code meets real traffic. This ro

Node.js Developer Beginner to Expert
Node.js Developer Beginner to Expert This roadmap takes you from JavaScript foundations and the event loop through building APIs, working with databases, streams, testing, and security, and on to r

Full-Stack Developer Beginner to Expert
Full-Stack Developer Beginner to Expert This roadmap walks you from your first web page to shipping and operating a complete production application. Work the stages in order. Build the frontend fun

Backend Developer Beginner to Expert
Backend Developer Beginner to Expert This roadmap walks you from your first server-side program to designing scalable, secure systems. Work through the stages in order. Nail a language, the command

React Developer Beginner to Expert
React Developer Beginner to Expert This roadmap takes you from JavaScript prerequisites through JSX, hooks, and state management, and on to data fetching, meta-frameworks, performance, and testing.

Frontend Developer Beginner to Expert
Frontend Developer Beginner to Expert This roadmap walks you from absolute beginner to a strong, hireable frontend engineer. Work through the stages in order. The early ones build the mental model
6 related posts