Go for infrastructure and services
Go is deliberately small — one obvious way to do most things, a compiler that finishes in seconds, and a single static binary at the end. That smallness is the actual value proposition, and it shapes the interview, because there is no framework knowledge to hide behind.
Assumes you know: Programming fundamentals in any language, A working idea of what a process, a thread and a stack are, HTTP, and enough networking to describe a TCP connection, Comfort with a terminal; no prior concurrency experience required
Overview
What this area actually covers
Go is a statically typed, garbage-collected, compiled language with 25 keywords, no inheritance, no exceptions, no constructors, no operator overloading and no macros. You can read the entire specification in an afternoon, and that is the design goal rather than an accident: Go was built for large teams maintaining large programs over long periods, and it trades expressive power for the property that any Go programmer can read any Go codebase.
The area covers the language, which is the smallest part; the runtime, which is where the
substance is; and the standard library, which is unusually complete for network services.
The runtime gives you goroutines multiplexed onto operating-system threads by a scheduler
you do not control directly, channels, a concurrent garbage collector, and a memory model
with defined guarantees. The library gives you a production-capable HTTP server and
client, TLS, JSON, templating, testing with benchmarks, and pprof — which is why Go
projects have far shallower dependency trees than equivalents elsewhere.
It also covers the toolchain, and that deserves naming explicitly because in most
languages the toolchain is a separate skill and in Go it is part of the language's
value. One command builds, one tests, one formats, one vets, one profiles, and there
is no configuration file to argue about. gofmt has no options, which ended
formatting debates by fiat. Dependency management is built in through modules rather
than delegated to a third-party tool. That integration is a large part of why teams
adopt Go for infrastructure work, and it is why a Go interview can reasonably ask what
go vet catches that the compiler does not.
What people wrongly bundle in is Kubernetes. Go is the language Kubernetes is written
in, and a great many Go jobs are adjacent to it, but knowing Kubernetes is
operational knowledge and knowing Go is not. The other thing wrongly bundled in is a
web framework. Frameworks exist — Gin, Echo, Chi, Fiber — but net/http plus a
router is the default in serious codebases, and a candidate who cannot write a
handler without a framework is a recognisable pattern.
The four areas underneath
Go's subsections are fewer than most sections on this site, and that is a fair reflection of the language: there is less surface to divide up. The four move from the language, through the concurrency model that is the reason most people learn it, into the runtime behaviour that senior interviews examine, and out to the practice of building an actual service.
| Subsection | What it is for |
|---|---|
| Go Language | The type system, the data structures, and the error convention |
| Goroutines & Channels | The concurrency model and how it goes wrong |
| Go Memory & Performance | Allocation, the collector, and profiling with real data |
| Building Services in Go | Structure, dependencies, HTTP and testing in practice |
Go Language
This covers slices and maps and what they actually are underneath, interfaces and
implicit satisfaction, struct embedding, the error convention and error wrapping,
and generics. It exists as its own area because Go's language surface is small
enough to cover properly, and because the traps are concentrated in a handful of
places where the syntax hides a representation. A slice looks like an array and is a
three-word header pointing at one, which is why append sometimes mutates a slice
you thought you had copied and sometimes does not. An interface value is two words,
which is why an interface holding a nil pointer is not equal to nil. You will find
questions here that are really questions about memory layout wearing language
clothing.
Goroutines & Channels
The scheduler, channel semantics for buffered and unbuffered channels, select,
sync primitives, context and cancellation, and the diagnosis of goroutine leaks
and races. This is the centre of gravity of every Go interview. It exists separately
because concurrency in Go is cheap enough to use casually, which means every codebase
uses it heavily and every codebase therefore contains the bugs. The questions are
rarely about how to start a goroutine and almost always about how one stops: who
closes the channel, what happens on cancellation, whether this select can block
forever, whether this handler leaks one goroutine per request.
Go Memory & Performance
Escape analysis and why a variable ends up on the heap rather than the stack,
reducing allocations, how the garbage collector behaves and what the tuning knobs
actually do, and profiling with pprof. It is a separate area because Go gives you
unusually direct visibility into all of this — the compiler will tell you why a value
escaped, and the profiler ships in the standard library — so the expectation in an
interview is that you have looked rather than guessed. This is also where the honest
answer to "is Go fast" lives: fast to build, fast to start, predictable under load,
and usually allocation-bound rather than CPU-bound when it is slow.
Building Services in Go
Standard-library HTTP, project structure, dependency management with modules, configuration, graceful shutdown, and testing patterns including table-driven tests and interface-based fakes. This exists because Go declines to make these decisions for you and therefore they become interview material. There is no canonical project layout blessed by the language, no dependency-injection container, no ORM everyone uses. What you will find here is judgement rather than API knowledge: where to put the interface, how deep to nest packages, when a fake is better than a mock, and how to structure a handler so it is testable without a running server.
Where it sits in a real system
Go occupies the layer between the operating system and the business logic. Container
runtimes, orchestrators, service meshes, proxies, databases, metrics systems,
configuration management tools, CI agents and CLI tooling are disproportionately
written in it: Docker, Kubernetes, etcd, Prometheus, Terraform, Vault, containerd,
CoreDNS, CockroachDB, Traefik and Caddy are all Go programs. There is a reason, and
it is deployment rather than elegance. go build produces one statically linked
binary with no runtime to install, cross-compiles to another platform by setting two
environment variables, and starts in milliseconds. For anything shipped to machines
you do not control, that is worth more than any language feature.
In application architecture Go sits where any service sits, and it is a strong choice where you have a lot of concurrent work and care about tail latency and memory footprint. A Go service holding a hundred thousand connections is ordinary, because a goroutine starts with a small stack that grows on demand rather than costing a megabyte of thread stack. That makes it the usual answer for gateways, proxies, streaming consumers, webhook fan-out and anything with a fan-out-then-gather shape.
flowchart TD
A[Incoming request] --> B[Handler goroutine]
B --> C[Context with deadline]
C --> D[Call service A]
C --> E[Call service B]
C --> F[Call service C]
D --> G[Gather results]
E --> G
F --> G
G --> H[Response]The part that matters is the context sitting above the fan-out rather than beside it: when the caller disconnects or the deadline passes, cancellation propagates down every branch at once, and the three calls abandon their work instead of finishing into a response nobody will read.
Where it is a weaker fit is where the domain model is the difficulty. Go's type system has no sum types, no enums worth the name, and limited generics, so encoding complex invariants in types is harder than in Rust, Kotlin, TypeScript or C#. Teams building rich domain models in Go end up validating at runtime what another language would have refused to compile.
The runtime in three mechanisms
Almost every senior Go question resolves to one of three runtime mechanisms. None of them is visible in the language, and all of them are observable with tools that ship in the box.
The scheduler. Goroutines are not operating-system threads. The runtime maintains a set of logical processors, each with a queue of runnable goroutines, and maps them onto a smaller number of real threads. When a goroutine blocks on a channel or a network read it is parked and the thread picks up something else; when it blocks in a system call the runtime can hand its queue to another thread. Idle processors steal work from busy ones. The practical consequences are the ones to remember: a goroutine is cheap enough to create per request, blocking on I/O costs you nothing in thread terms, and the number of goroutines running your code simultaneously is bounded by the processor count rather than by how many you started.
The garbage collector. Go's collector is concurrent, runs alongside your program
rather than stopping it for long, and does not compact or move objects. It is also
not generational, which is unusual and is why allocation reduction matters more in Go
than in Java: there is no cheap nursery to absorb short-lived garbage. Two controls
exist. GOGC sets how much the heap may grow relative to live data before a cycle
starts. GOMEMLIMIT, added in Go 1.19, sets a soft ceiling on total memory, which is
what you want in a container because the failure it prevents — the kernel killing
your process for exceeding its limit — is not one the collector would otherwise see
coming.
Escape analysis. The compiler decides for each value whether it can live on the goroutine's stack, which costs nothing to reclaim, or must be allocated on the heap because a reference outlives the function. Returning a pointer to a local, storing a value in an interface, or capturing a variable in a closure that outlives the call all typically force an escape. You do not have to guess:
go build -gcflags='-m' ./... # prints each decision and why
Reading that output on a hot path is the single highest-return performance exercise in the language, because it turns a vague question about speed into a specific list of allocations you can decide to remove or accept.
| Mechanism | The tool that shows you | The question it answers |
|---|---|---|
| Scheduler | Goroutine profile, execution trace | Why is nothing progressing |
| Collector | Heap profile, GODEBUG GC trace | Why is memory growing |
| Escape analysis | -gcflags='-m' | Why is this path allocating |
| Data races | go test -race | Why is this only wrong sometimes |
| CPU time | CPU profile via pprof | Where is the time going |
The row that surprises people is the first: a program that has stopped making progress is diagnosed by dumping goroutines and reading their stacks, and the answer is usually that several of them are blocked on each other in a way no test caught.
From source to a deployed artefact
The build story is short enough to hold entirely in your head, which is itself the
point. A module is declared by a go.mod file naming the module path, the language
version and the dependencies. Version selection is deliberately boring: rather than
resolving the newest version that satisfies every constraint, Go picks the minimum
version that every requirement is satisfied by, so adding a dependency cannot
silently upgrade an unrelated one. A go.sum file records the cryptographic hash of
every module in the graph, and the toolchain verifies against it on every build,
which is why a Go dependency tree is unusually resistant to something changing
underneath you. Since Go 1.21 the go.mod file can also pin the toolchain itself, so
the compiler version becomes part of the project rather than a property of whoever's
laptop ran the build.
flowchart TD
A[Source and go.mod] --> B[Module download and checksum]
B --> C[Compile and link]
C --> D{CGO enabled}
D -->|No| E[Fully static binary]
D -->|Yes| F[Dynamically linked binary]
E --> G[Scratch or distroless image]
F --> H[Image needs a matching libc]The branch is the one that decides your container image. With cgo disabled the output depends on nothing at all, so the image can be an empty filesystem containing one file; with cgo enabled, usually pulled in by a DNS resolver or a database driver, you inherit a dependency on the system C library and the image has to carry one.
Cross-compilation is two environment variables and no extra tooling, which is why Go became the default for command-line tools distributed to users on operating systems the author does not run:
GOOS=linux GOARCH=arm64 go build -o bin/agent ./cmd/agent
None of this is intellectually demanding, and that is exactly the property teams are buying. Compare it honestly against the alternative you know: in most ecosystems the build configuration is a subsystem with its own experts, and in Go it is a file with five lines in it.
Who does this work
Platform and infrastructure engineers write the tooling other engineers deploy onto — controllers, operators, provisioning services, internal CLIs. Their day is part Go and part the system it manipulates. Backend engineers on Go product teams build APIs and consumers, usually in an organisation that chose Go for operational reasons at some scale. SREs write Go for the same reason they write Python, but reach for it when the tool needs to be distributed as a binary or handle real concurrency. Observability engineers work on collection and processing pipelines, which are almost all Go by convention now. Payments and trading backends use Go where predictable latency matters more than raw throughput.
The distinguishing feature of Go teams is that the split between the people who build
and the people who operate is narrow or absent. Go work is very often work on the
thing that runs other things, so the author is frequently also on call for it. That
has a knock-on effect on interviews: questions about operating a service — what
happens on SIGTERM, how you drain connections, what your health check should
actually check — turn up in Go loops far more often than in equivalent Java or Node
loops, because in a Go team those are the same person's job.
Demand, adoption and how that is changing
Demand is high but concentrated, and being clear-eyed about the concentration is more useful than the headline. Go roles cluster in infrastructure, platform, developer tooling, cloud vendors, observability, networking and fintech. They are comparatively scarce in traditional enterprise IT, almost absent in data science and machine learning, and thin in agency and consultancy work. That has a practical consequence: there are fewer Go jobs than Java or Node jobs, and the ones that exist skew senior and skew towards companies whose product is technical. If you want to work on infrastructure, this is close to the ideal language to know. If you want maximum optionality, it is a second language rather than a first.
The adoption story is settled rather than growing explosively. Go won the cloud-native infrastructure niche a decade ago and holds it; the competition for new projects in that niche is now Rust, which wins where memory safety without a garbage collector matters and loses where compile times and onboarding speed matter. Neither is displacing the other, and Go's advantage is that a team can be productive in it in a fortnight.
Several language changes are worth knowing because they date a candidate. Generics
arrived in Go 1.18, ten years after 1.0, and the standard library grew slices and
maps packages off the back of them in Go 1.21, along with min, max and clear
as builtins and log/slog for structured logging. Go 1.22 changed loop variable
semantics so that for loops declare a new variable per iteration — which quietly
retired the single most common goroutine bug in the language's history, the one where
every goroutine in a loop saw the final value — and allowed ranging over an integer.
Go 1.23 added range-over-function iterators, which is the first change in years that
meaningfully alters how library authors expose sequences. Someone who still teaches
the x := x shadowing workaround as necessary has not read a release note since
2023.
The compatibility promise underpins all of this: code written against Go 1 still compiles, which is why upgrading a Go service is a non-event and why the ecosystem has so little churn compared with JavaScript's. It is also the reason for some of the awkwardness elsewhere in the language, and the trade is worth stating plainly. Nothing gets removed, so pre-generic APIs in the standard library stay pre-generic forever, and mistakes made in 2012 are still present. In exchange, an upgrade is a version bump rather than a project, and dependency trees do not rot. Most teams consider that an excellent bargain.
What makes it hard
The interview follows from the smallness. There is no framework surface to discuss,
no ORM behaviour to know, no dependency-injection container, no annotations. So the
questions go straight to the runtime and to your judgement, and there is nowhere to
hide. Concurrency carries most of the weight, and it is not the syntax that is hard —
go f() is trivial to write — it is correctness. Where does this goroutine stop. Who
closes this channel. What happens when the caller cancels. Is this select able to
block forever. Does this code leak a goroutine per request, which is the most common
production bug in Go services and does not show up in tests that finish quickly.
The goroutine lifecycle is worth holding as a picture, because every leak is the same missing edge.
stateDiagram-v2
[*] --> Runnable: go f
Runnable --> Running: scheduled
Running --> Blocked: channel or IO wait
Blocked --> Runnable: ready or cancelled
Running --> Done: function returns
Done --> [*]
Blocked --> Leaked: never becomes readyThe transition that costs money is the last one. Nothing in the language, the compiler or the race detector will tell you a goroutine reached it, because a permanently blocked goroutine is not an error — it is simply waiting, forever, holding whatever it captured.
The second hard thing is knowing when not to use channels. Go's slogan about sharing memory by communicating leads newcomers to model everything as pipelines, producing elaborate channel topologies where a mutex and a map would have been clearer and faster. Experienced Go engineers use channels for handing off ownership and signalling, and plain locks for protecting state.
The third is that the language gives you very little help with mistakes, so
discipline is your own responsibility. A nil map read works and a nil map write
panics. An interface holding a nil pointer is not equal to nil, which produces error
checks that pass when they should fail. Zero values are always valid, so a missing
field and a deliberate zero are indistinguishable. defer inside a loop accumulates
until the function returns. None of these are subtle once you know them, and all of
them are found the hard way.
And the honest complaint list, which deserves to be taken seriously rather than defended. Error handling is verbose:
package store
import (
"context"
"database/sql"
"fmt"
)
type User struct{ Name string }
func LoadUser(ctx context.Context, db *sql.DB, id string) (*User, error) {
var u User
err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", id).Scan(&u.Name)
if err != nil {
// %w wraps, so callers can still use errors.Is and errors.As up the stack.
return nil, fmt.Errorf("load user %s: %w", id, err)
}
return &u, nil
}
Three lines of ceremony per fallible call, repeated everywhere, and several proposals to
shorten it have been rejected. The defence — that it puts the failure path in front of the
reader instead of hiding it in a catch somewhere else — is genuinely persuasive, but the
verbosity is real, and a candidate who pretends to like everything about it sounds
rehearsed. Generics arriving in year ten left the standard library with pre-generic APIs
that will never be revisited, and the implementation is deliberately limited: no methods
with their own type parameters, no variance, and constraints that get awkward quickly.
Struct tags do configuration through strings. There is no enum, so every package invents
its own convention. These are fair criticisms of a language that is still, on balance, an
excellent tool.
There is a fourth difficulty that only appears at scale, and it is the one experienced
Go engineers complain about most. Because the language declines to prescribe structure,
every codebase invents its own, and the inventions do not agree. Where do the interfaces
live, how deep does the package tree go, does configuration arrive as a struct or as
arguments, is there a service layer or do handlers talk to the database. Go's own advice
— define interfaces where they are consumed, keep packages shallow, avoid a package
called util — is good and insufficient. In a small program it does not matter. In a
large one it is the difference between a codebase people can navigate and one where
everything imports everything, and there is no framework to blame when it goes wrong.
Why study it
Study Go if you want to work on infrastructure, platforms or developer tooling, where it is the default and the alternatives are worse. Study it if you want to learn concurrency properly, because Go makes concurrency cheap enough to use casually and therefore forces you to learn the discipline. Study it if you value shipping: one binary, no runtime, fast builds, a formatter with no options to argue about, and a test runner in the box.
There is also a pedagogical argument that is easy to overlook. Go is the most approachable language in which you can see the machine. It has pointers but not pointer arithmetic, a garbage collector you can watch working, a scheduler you can profile, and a compiler that will explain its allocation decisions on request. If you want to understand what a runtime does without first spending six months on manual memory management, this is the shortest honest route, and everything you learn here transfers to reasoning about any other managed runtime.
Do not study it if you want to build rich domain models with a type system doing the heavy lifting. Do not study it for data or machine-learning work. And do not study it expecting the small language to mean a small skill: it is unusually fast to become productive in and unusually hard to be excellent at, because everything the language declines to decide for you is now your problem. There is no framework telling you how to structure a service, so structure is a judgement call every time — and judgement is exactly what a senior Go interview is examining.
Your first hour
Do not start with the tour's syntax pages. Start with concurrency, because that is what the language is for and what the interview is about.
go mod init example.com/scratch
Write a program that fetches five URLs concurrently with a sync.WaitGroup, collects
the status codes, and prints them. Then break it deliberately: remove the Wait and
see the program exit before the work finishes. Add a context.WithTimeout shorter
than one of the requests and confirm which error you get. Then start a goroutine that
reads from a channel nobody ever writes to, and run the program with go run -race
and with go vet to see what each does and does not catch.
Finally, run this and read the output:
go test -run XXX -bench . -benchmem # in a package with one small benchmark
The -benchmem column is the one to look at. It reports allocations per operation,
and in Go that number is very often the whole performance story — a function that
allocates twice per call in a hot loop is the thing to fix, and the benchmark tells
you so without any profiling setup at all. Change one line to reuse a buffer instead
of creating one, rerun, and watch the number move. That loop, from measurement to
change to measurement, is the habit the whole performance subsection is built on.
The artefact is one file under a hundred lines plus a sentence explaining what the
race detector reported and what it could not have reported. If you can explain why a
blocked goroutine is invisible to both go vet and the race detector, you have
already met the bar most candidates miss.
What this is not
Go is not simple, it is small. Those differ. The language surface is tiny; the runtime underneath — scheduler, garbage collector, memory model, escape analysis — is sophisticated, and senior interviews live there. "Go is simple" is a statement about the syntax that candidates mistakenly extend to the semantics.
Goroutines are not threads. They are scheduled by Go's runtime onto a pool of operating-system threads, start with a small stack that grows by copying, and cost little enough that hundreds of thousands are reasonable. That is why the usual thread-pool advice does not transfer.
Go is not object-oriented in the way the syntax hints. There are methods but no classes, embedding but no inheritance, and interfaces that are satisfied implicitly — a type never declares that it implements one. The idiomatic consequence is that interfaces are defined by the consumer, small and often single-method, rather than published by the implementer as a contract.
Channels are not a general-purpose replacement for locks, and the slogan about sharing
memory by communicating was never meant as an absolute. The standard library's own
sync package exists, is used heavily inside the runtime, and is the right answer
whenever the problem is protecting a piece of shared state rather than transferring
ownership of it. Reaching for a channel to guard a counter is a recognisable
beginner's move.
And Go's error handling is not the absence of exceptions with nothing in its place.
panic and recover exist, wrapped errors form chains you inspect with errors.Is
and errors.As, and errors are ordinary values you can compare, wrap and carry
context on. It is a design, not an omission — which is the fair way to argue about it
in an interview.
Everything distinctive about Go, good and bad, comes from the same decision to keep the language small. It is why you can be useful in two weeks, why the interview goes straight to the runtime, and why judgement rather than knowledge is what gets graded.
Where to go next
Now practise it
8 interview questions in Go, each with the rubric the interviewer is scoring against.
- How do you tell whether a value escapes to the heap, and how would you find the allocations that are costing you?
- How do goroutines leak, and how would you find a leak in a running service?
- Your function returns nil on the success path, the caller checks err != nil, and the check fires anyway. What happened?
- A client hangs up halfway through a request. How do you structure a Go HTTP service so it stops doing the work?