Go Performance
Optimizing without measuring is guessing. Go ships a profiler built into the runtime: it is always available, runs on a live service, and needs no special build flag. First you capture a profile, then you read it — and only then do you know where the time goes. The core mistake here is judging the nature of a load from latency alone: a high latency appears in both a CPU-bound and an I/O-bound service, and without CPU utilization and a profile those are two indistinguishable cases.
Once the bottleneck is found, you have levers: steer the compiler toward the hot paths with a production profile (PGO), scale the service out or up. But any optimization risks breaking behavior — so the second half of this topic is about testing. In Go a test is part of the toolchain, not a third-party library: go test builds the test binary itself and runs table-driven cases, benchmarks, and fuzzing. And to make a unit test check your logic rather than the availability of a database or the network, the dependency is hidden behind a narrow interface and a fake is injected in the test. This topic walks that cycle — from capturing a profile to an isolated test.
Topic map
- Go profiling — how to capture CPU, heap, goroutine, block, and mutex profiles via
pprofand analyze them withgo tool pprof. - Bottleneck analysis — how CPU utilization and a profile tell a CPU-bound service from an I/O-bound one.
- Profile-Guided Optimization — a
default.pgoprofile in the main package guides the compiler to inline and devirtualize hot paths. - Scaling strategies — horizontal adds instances behind a load balancer, vertical gives one instance more resources.
- Testing in Go —
go testwith no framework: unit and table-driven tests, benchmarks, examples, and fuzzing, plus the-race/-coverflags. - Mocking dependencies — depending on a narrow interface and injecting a fake in the test isolates the unit from real I/O.
Common mistakes and traps
| Mistake | Consequence |
|---|---|
| Thinking profiling needs a special build flag | An net/http/pprof import or a runtime/pprof call is enough |
| Believing a CPU profile is an instant snapshot | It is a sampling window; the program must run under load while it is collected |
Exposing the pprof port publicly | /debug/pprof/* serves the service's internals — keep the port on the internal network |
| Judging the nature of a load from latency alone | Without CPU utilization and a profile you cannot tell CPU-bound from I/O-bound |
| Forgetting cgroup throttling when diagnosing | A cgroup CPU limit masks CPU-bound as I/O-bound — low utilization with rising latency |
| Believing a blocked goroutine burns CPU | A goroutine waiting on net is parked and adds no load |
Placing the PGO profile not as default.pgo or not in main | go build will not pick it up — the optimization silently does not apply |
| Confusing horizontal and vertical scaling | Horizontal adds instances, vertical adds resources to one |
| Believing you can scale up forever | Vertical hits the largest available machine and usually requires a pod restart |
| Setting the benchmark iteration count by hand | b.N is chosen by the tool — a hand-set number breaks the measurement |
Pulling every method of the real client into an interface | The wider the contract, the more code in the fake and the higher the coupling — take only what you need |
| Reaching a real dependency through a global variable | Global state leaks between tests and breaks a parallel run |
Interview relevance
Performance questions in a Go interview test not your knowledge of commands but your method: can you find the bottleneck by measuring rather than guessing, and not break behavior while optimizing. The interviewer wants the cycle "capture a profile → read it → diagnose → verify with a test".
What interviewers check:
- How to profile a Go service in production with
pprof— without a special build flag. - How CPU utilization and a profile tell a CPU-bound service from an I/O-bound one.
- What PGO is and where to place the profile so the compiler picks it up.
- How horizontal scaling differs from vertical and what each one requires.
- How testing in Go works — table-driven tests, benchmarks, the
-race/-coverflags. - How to isolate a unit from real I/O — a narrow
interfaceand a fake instead of global state.
A typical wrong answer: "the service is slow, so it is I/O-bound, adding cores will not help." That opens a discussion of how no diagnosis is possible without CPU utilization and a profile, how cgroup throttling masks CPU-bound as I/O-bound, and how the direction of the fix is decided by measurement.