---
title: "Go Developer Beginner to Expert"
description: "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."
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/roadmaps/post/go-developer-roadmap
---

# Go Developer Beginner to Expert

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.

_Where the later stages land_

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.

## Roadmap

### Stage 1: Language Foundations

Go is deliberately small. Read all of it, then stop looking for the features it does not have.

#### 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.

- [A Tour of Go](https://go.dev/tour/)

#### 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.

- [Go slices, usage and internals](https://go.dev/blog/slices-intro)

#### 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.

- [Effective Go, interfaces](https://go.dev/doc/effective_go#interfaces)

#### 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.

- [Working with errors in Go 1.13+](https://go.dev/blog/go1.13-errors)

#### Generics _(recommended)_

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.

### Stage 2: Standard Library Fluency

Go's standard library covers more than most ecosystems need a framework for.

#### 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.

- [net/http documentation](https://pkg.go.dev/net/http)

#### 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.

- [log/slog documentation](https://pkg.go.dev/log/slog)

### Stage 3: Concurrency

The reason many people pick Go, and the part most often misused.

#### 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.

- [Go concurrency patterns](https://go.dev/blog/pipelines)

#### 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.

- [Go Concurrency Patterns, context](https://go.dev/blog/context)

#### 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 _(recommended)_

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.

### Stage 4: Testing

Go's testing story is built in and unusually pleasant.

#### 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.

- [Go testing package](https://pkg.go.dev/testing)

#### 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 _(recommended)_

`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 _(optional)_

Compare output against a checked-in file, regenerated behind a `-update` flag. Well suited to templates, generated code and serialisation.

### Stage 5: Building Services

Where Go earns its place, and where the standard library is often enough.

#### 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.

- [Accessing databases in Go](https://go.dev/doc/database/index)

#### sqlc or hand-written SQL over an ORM _(recommended)_

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 _(optional)_

Worth learning for service-to-service work: a schema, generated clients, and streaming. Overkill for a public API that a browser will call.

### Stage 6: Modules and Tooling

A small toolset, and worth knowing exactly.

#### 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.

- [Go modules reference](https://go.dev/ref/mod)

#### 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 _(recommended)_

Stringer for enums, mockgen, sqlc, protoc. Committing generated code is the Go norm, so builds do not depend on generator availability.

#### Cross compilation _(recommended)_

`GOOS=linux GOARCH=arm64 go build` produces a binary for another platform with no toolchain gymnastics. This is how one laptop builds for Graviton.

### Stage 7: Shipping the Binary

The stage where Go's design pays off most visibly.

#### 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 _(recommended)_

`//go:embed` puts templates, migrations and static files inside the binary, so deployment stays a single file.

#### Build info and versioning _(recommended)_

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.

### Stage 8: Running Go in Production

The part that separates working from reliable.

#### 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.

- [Profiling Go programs](https://go.dev/blog/pprof)

#### 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 _(recommended)_

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.
