Go Performance
Profiling with pprof, bottleneck analysis, PGO, scaling strategies, testing, and mocking dependencies.
8 questions
JuniorTheoryVery commonWhat is the difference between horizontal and vertical scaling?
What is the difference between horizontal and vertical scaling?
Horizontal scaling adds more instances of the service and spreads load across them, which needs stateless instances and a load balancer. Vertical scaling gives one instance more CPU and RAM — simpler, but bounded by the largest single machine and usually requires a restart.
Common mistakes
- ✗Swapping the definitions — horizontal adds instances, vertical adds resources to one instance
- ✗Forgetting that horizontal scaling needs stateless instances and a load balancer
- ✗Believing vertical scaling has no ceiling — it is bounded by the largest machine
Follow-up questions
- →Why does horizontal scaling usually require the service to be stateless?
- →When would you prefer vertical scaling despite its ceiling?
JuniorTheoryCommonWhat is profiling in Go, and which tool produces a CPU or heap profile?
What is profiling in Go, and which tool produces a CPU or heap profile?
Profiling samples a running program to see where it spends CPU or memory. Go's runtime/pprof (and net/http/pprof) produces CPU, heap, goroutine, and block profiles, which you inspect with go tool pprof as a top list, call graph, or flame graph.
Common mistakes
- ✗Confusing profiling with the
-racedetector - ✗Thinking Go has no built-in profiler and only manual timing works
- ✗Confusing runtime profiling with static escape analysis (
-gcflags=-m)
Follow-up questions
- →What is the difference between a CPU profile and a heap profile?
- →How does
net/http/pproflet you profile a live server?
JuniorTheoryCommonWhat does go test support out of the box, and what kinds of tests can you write?
What does go test support out of the box, and what kinds of tests can you write?
go test runs functions named TestXxx(*testing.T) in _test.go files with no extra framework. Out of the box it supports unit tests, table-driven tests, benchmarks (BenchmarkXxx), runnable examples, and fuzzing (FuzzXxx). Mocks are usually small hand-written fakes behind an interface. Useful flags: -run, -v, -race, -cover, -bench.
Common mistakes
- ✗Thinking you need a third-party framework to run Go tests at all
- ✗Forgetting test files must end in
_test.goand functions start withTest - ✗Assuming Go auto-generates mocks rather than writing small fakes by hand
Follow-up questions
- →What is a table-driven test, and why is it the idiomatic Go pattern?
- →What does the
-raceflag add, and why not run it on every build?
MiddleTheoryCommonHow do you mock external dependencies when testing Go code?
How do you mock external dependencies when testing Go code?
Depend on a small interface, not a concrete type, and inject a fake in the test. Define the interface at the consumer, pass it in via a constructor or field, and in the test supply a hand-written stub or a generated mock that records calls and returns canned values. This isolates the unit from real I/O and keeps behavior deterministic.
Common mistakes
- ✗Defining the interface next to the implementation instead of at the consumer that needs to mock it
- ✗Making the interface huge, so a test fake must stub dozens of unused methods
- ✗Reaching for global function-pointer swaps instead of injecting a dependency
Follow-up questions
- →Why is a narrow consumer-defined interface easier to mock than a wide one?
- →When is a hand-written stub preferable to a generated mock like
gomock?
MiddleTheoryCommonHow do you profile a Go service running in production with pprof?
How do you profile a Go service running in production with pprof?
Import net/http/pprof to expose profiles over HTTP, or call runtime/pprof directly. You collect CPU, heap, goroutine, block, or mutex profiles, then analyze them with go tool pprof, which shows hot functions as a top list, graph, or flame graph.
Common mistakes
- ✗Thinking profiling needs a special build flag rather than the
net/http/pprofimport - ✗Believing Go only exposes a heap profile and not CPU, goroutine, or block profiles
- ✗Forgetting that a CPU profile must run for a sampling window, not capture an instant
Follow-up questions
- →How does a Go CPU profile sample the program, and at what rate?
- →What does the goroutine profile reveal that a CPU profile does not?
MiddleTheoryCommonWhat must a service avoid in order to scale horizontally?
What must a service avoid in order to scale horizontally?
It must keep no per-instance state a request depends on — no in-memory sessions, caches, counters, or local files another replica lacks. Push state to shared backing services (DB, Redis, object storage) so any replica can serve any request. Add a load balancer, sticky-session-free routing, and idempotent handlers so a request can safely retry on another instance.
Common mistakes
- ✗Keeping sessions or counters in process memory, so replicas disagree and scaling breaks
- ✗Relying on sticky sessions as a fix instead of externalizing the state
- ✗Confusing horizontal scaling with simply using more CPU cores on one instance
Follow-up questions
- →How do idempotency keys let a load balancer safely retry a request on another replica?
- →Where should a per-user rate-limit counter live if every replica must enforce it?
SeniorTheoryCommonHow do you tell whether a service is CPU-bound or I/O-bound?
How do you tell whether a service is CPU-bound or I/O-bound?
A CPU-bound service shows high CPU utilization and a CPU profile dominated by a few hot functions; adding cores helps. An I/O-bound service shows low CPU, many goroutines blocked in network or disk waits, and latency that does not improve with more CPU.
Common mistakes
- ✗Judging from latency alone without checking CPU utilization or profiles
- ✗Forgetting cgroup CPU throttling can make a CPU-bound service look I/O-bound
- ✗Assuming blocked goroutines burn CPU rather than parking off-thread
Follow-up questions
- →Which pprof profiles would you compare to confirm an I/O-bound diagnosis?
- →How does cgroup CPU throttling distort a CPU-bound vs I/O-bound reading?
MiddleTheoryRareWhat is Profile-Guided Optimization (PGO) in the Go toolchain?
What is Profile-Guided Optimization (PGO) in the Go toolchain?
Profile-Guided Optimization, stable since Go 1.21, feeds a runtime CPU profile back into the compiler. You commit it as default.pgo in the main package and go build reads it with no flags, optimizing hot paths via inlining and devirtualization.
Common mistakes
- ✗Confusing PGO with JIT or runtime recompilation — it is a compile-time, build-step optimization
- ✗Thinking pprof and PGO are the same thing; pprof is for humans, PGO feeds the profile to the compiler
- ✗Believing PGO needs a special CLI flag rather than a
default.pgofilego buildfinds automatically
Follow-up questions
- →How does a stale or unrepresentative profile affect a PGO build?
- →Why is devirtualization of interface calls only applied to hot call sites?