Skip to content
QSWEQB

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.

High demand20 min readBackend engineer (Go), Platform engineer, Site reliability engineer, Infrastructure and cloud-native developer, Observability engineer, Payments and high-throughput services engineerUpdated 2026-07-27

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.

SubsectionWhat it is for
Go LanguageThe type system, the data structures, and the error convention
Goroutines & ChannelsThe concurrency model and how it goes wrong
Go Memory & PerformanceAllocation, the collector, and profiling with real data
Building Services in GoStructure, 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.

MechanismThe tool that shows youThe question it answers
SchedulerGoroutine profile, execution traceWhy is nothing progressing
CollectorHeap profile, GODEBUG GC traceWhy is memory growing
Escape analysis-gcflags='-m'Why is this path allocating
Data racesgo test -raceWhy is this only wrong sometimes
CPU timeCPU profile via pprofWhere 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 ready

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

All Go questions
golangconcurrencyinfrastructurebackendcloud-native