# Go Perf Audience And Tradeoffs

> Guides framing a Go performance decision by audience — the same change is right for one and wrong for another: the library author who can't profile callers and must not pessimize the common case (offer AppendXxx/[]byte forms, no unsafe in the API), the app developer who can profile and optimizes the proven hot path, the SRE who needs the code diagnosable (pprof labels, metrics, GOMEMLIMIT), the next reader who pays for clever micro-opts; the clarity-vs-speed through-line. Fires on "should a library do this", "is this worth optimizing for my users", "make this diagnosable", "premature optimization vs pessimization", "who am I optimizing for". Routes the policy root to go-idiomatic-discipline, measure-first to go-perf-methodology.

- Skill: `ctoth/go-perf-audience-and-tradeoffs` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add ctoth/go-perf-audience-and-tradeoffs`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ctoth/go-perf-audience-and-tradeoffs/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: ctoth (https://skillmd.com/u/ctoth)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ctoth/go-perf-audience-and-tradeoffs

---


# Go Performance: Audience & Tradeoffs

> "Clear is better than clever." · "A little copying is better than a little dependency."
> — [Go Proverbs](https://go-proverbs.github.io/)

> "Optimization is a form of refactoring … This improvement generally comes at the cost of readability."
> — [go-perfbook](https://github.com/dgryski/go-perfbook)

The people affected by a Go performance change are **not one audience**, and the right call depends on **who you are writing for**. The library author and the application developer make *opposite* correct calls on the same line: one cannot profile the caller and must not pessimize the common case; the other can profile and should optimize only the proven hot path. Before you trade clarity for speed, **name your audience.** This skill owns that framing; depth routes to the `go-perf-*` leaves and the policy root.

The measure-first loop is **`go-perf-methodology`** ("no profile, no trade"); the dual-axis "clear AND correct" policy is **`go-idiomatic-discipline`**. Read those first. All Go below builds clean under `gofmt`, `go vet`, and `go test` on Go 1.25/1.26.

---

## 1. The Four Audiences

| Audience | Can they profile the real workload? | What "performance" means to them | The failure |
|---|---|---|---|
| **Library / package author** | **No** — your callers' workload is unknown | Don't pessimize the *distribution* of callers; offer a zero-alloc form | Forcing an allocation on every caller; `unsafe` in the public API |
| **Application / service dev** | **Yes** — profile your own prod | Optimize the *proven* hot path; ignore the cold 99% | Micro-tuning an unmeasured / cold path |
| **SRE / on-call at 3am** | Only in prod, under fire | The code must be **diagnosable** | Anonymous frames, no labels, no metrics, OOM instead of GC |
| **Next reader of the code** | n/a | Every micro-opt is a tax they pay forever | A clever `unsafe` trick saving 3ns on a cold path |

The unifying lesson: **the same change is right for one audience and wrong for another.** §2–§5 take each in turn; §6 is the through-line that connects them.

---

## 2. The Library / Package Author — Optimize the Distribution of Unknown Callers

You **cannot profile your callers' workload** — there is no single workload. So the library author's job is not to optimize one path; it is to **not foreclose performance for the distribution of unknown callers**, while keeping the API clear. go-perfbook is explicit about the danger of over-fitting: "avoid the temptation to optimize upfront for every single use case. This will result in unreadable code … You can neither read minds nor predict the future" ([go-perfbook](https://github.com/dgryski/go-perfbook)).

The concrete disciplines — each a thing the stdlib itself does:

- **Don't force an allocation on the common case.** Accept `[]byte` (or `io.Writer`) so a caller who already has bytes need not copy into a `string`. Your hot path *is* your caller's hot path.
- **Offer the zero-alloc `Append` form.** The caller controls the buffer. `strconv.AppendInt(dst []byte, …) []byte` exists alongside `strconv.Itoa` precisely so a hot caller appends into a reused buffer with **zero allocations** ([strconv](https://pkg.go.dev/strconv#AppendInt)); `time.Time.AppendFormat`, `fmt.Append*`, `encoding/hex.AppendEncode` follow the same convention.
- **Keep the zero value useful.** `bytes.Buffer` and `strings.Builder` are "ready to use" at zero value — no constructor allocation forced on the caller ([strings.Builder](https://pkg.go.dev/strings#Builder)). "Make the zero value useful" ([Proverbs](https://go-proverbs.github.io/)).
- **Document allocation behavior.** If a method allocates per call, say so; a caller optimizing *their* hot path needs it as a documented contract, not a thing they rediscover in a profile.
- **Do NOT expose `unsafe` in the public API.** `unsafe.String`/`unsafe.Pointer` may live *inside* an implementation behind a safe, copying-by-contract signature — never in the exported type. The caller cannot audit your aliasing invariants.
- **Do NOT over-fit to one caller's microbenchmark.** A 3% win on one reported workload that complicates the API taxes every other caller. go-perfbook's out: "a reasonable answer might be 'Then use this other package over here'."

```go
// WRONG (library) — forces every caller to allocate a string just to format an int
func (f *Formatter) Int(n int) string { return strconv.Itoa(n) } // alloc per call, caller can't opt out

// RIGHT (library) — offer the Append form; the caller owns the buffer and reaches 0 allocs
func (f *Formatter) AppendInt(dst []byte, n int) []byte { return strconv.AppendInt(dst, n, 10) }
// keep an Itoa-style convenience too — but the zero-alloc path EXISTS for the hot caller
```

API-level allocation patterns (`Append`, `[]byte`, buffer reuse) → **`go-perf-strings-bytes-zerocopy`** and **`go-perf-slices`**.

---

## 3. The Application / Service Developer — You Can Profile, So Measure First

The application developer has the one thing the library author lacks: **a real workload you can profile.** So measure-first applies *fully* — optimize the path the profile proves hot, and ignore the cold 99%. Your tradeoff is **clarity vs speed on code your own team maintains**, so the bar is "did the profile rank this in the top of `-cum`, and does benchstat confirm the win?" — not "could this be faster in principle."

```go
// WRONG (app) — hand-rolled, unsafe-laden parser for a config file read ONCE at startup
//   0.01% of runtime; now every teammate maintains the cleverness forever

// RIGHT (app) — profile ranks the real hot path; optimize THAT, leave startup readable
//   go tool pprof -top -cum cpu.prof  → the request decoder is 70% of CPU; optimize there
```

This is the audience for which `go-perf-methodology`'s entire apparatus exists: USE to find the saturated resource, the right profile for the question, benchstat n≥10, Amdahl as the cold-path guard. "Doubling speed in a routine consuming 5% of runtime yields only 2.5% total" ([go-perfbook](https://github.com/dgryski/go-perfbook)). The proof obligation is on you — and you *can* meet it, so you must.

One inversion to watch: the app developer is *also* a library author to their own internal packages. A shared `internal/` helper called from many services has callers you can't all profile from one place — treat its API by §2's rules even though the binary is "an app." The audience is set by **who calls the code**, not by whether the repo ships a binary or a module.

---

## 4. The SRE / On-Call Engineer — "Performance" Includes Diagnosability

The engineer paged at 3am cannot refactor; they can only **observe**. To them "performance" means the running service is **diagnosable** under load — and that is a property you build in *at authoring time*, for free, with no clarity cost:

- **Meaningful names in the profile.** A flame graph full of `func1`, `(*T).bound-method`, and giant inlined anonymous closures is unreadable. Named functions and avoiding needless closures make the profile self-describing. (No tradeoff here — clear names are also idiomatic.)
- **`pprof` labels on request types.** `pprof.Do(ctx, pprof.Labels("endpoint", "/checkout"), …)` lets the on-call **slice one CPU profile by request type** instead of staring at an undifferentiated blob → **`go-perf-pprof-profiling`** (labels).
- **Metrics exposed.** `runtime/metrics` histograms (`/gc/*`, `/sched/latencies:seconds`) surfaced via expvar/Prometheus turn "it feels slow" into a number → **`go-perf-godebug-and-metrics`**.
- **`GOMEMLIMIT` set so the pod GCs instead of OOMing.** A soft memory limit makes the runtime collect harder under pressure rather than getting OOM-killed — the difference between a latency blip and a crash loop the SRE has to firefight → **`go-perf-gc-tuning`**.
- **Tail metrics, not the mean.** The SRE lives on p99/p999; report them → **`go-perf-tail-latency`**.

The diagnosable service and the fast service are not in tension: labels, metrics, and a memory limit cost nothing in clarity and pay off exactly when a human is under the most pressure.

This is the audience whose needs are most often *omitted* rather than wrongly traded: nobody argues against diagnosability, it simply isn't built because the author at write time isn't the one paged. The asymmetry is the point — an unmeasured micro-opt (§5) is clarity spent for nothing, but a `pprof` label is observability bought for nothing. When in doubt about a perf change, the diagnosability additions are the ones you make *without* needing a profile to justify them.

---

## 5. The Next Reader of the Code — Every Micro-Opt Is a Tax

The most common audience, and the one never present at write time, is **whoever reads this code next.** Every micro-optimization is a tax they pay on every future change. "Optimization is a form of refactoring … at the cost of readability" ([go-perfbook](https://github.com/dgryski/go-perfbook)) — so an *unmeasured* micro-opt spends the reader's budget and buys nothing.

The sharp case is the **clever `unsafe` trick that saved 3ns on a cold path**: it is not a speedup, it is a latent bug (an aliasing or lifetime violation the race detector and `go vet` can't see) that every future reader must re-audit before touching the function. "Clear is better than clever" is an empathy rule, not an aesthetic one ([Proverbs](https://go-proverbs.github.io/)). The owner of this axis is the policy root → **`go-idiomatic-discipline`**.

---

## 6. The Through-Line: Premature Optimization vs Premature Pessimization

Every audience above is a point on **one axis: clarity vs speed.** It is bounded on **both** sides, and naming the audience tells you which bound you're near.

- **Premature optimization** (the ceiling): trading clarity for an *unmeasured* speed gain. Knuth's caution, and `go-perf-methodology`'s "no profile, no trade." The app dev who micro-tunes a cold path is here; so is the library author who over-fits one caller's benchmark.
- **Premature pessimization** (the floor): "writing code that is slower than it needs to be, usually by asking for unnecessary extra work, when equivalently complex code would be faster and should just naturally flow out of your fingers" (Herb Sutter, via [go-perfbook](https://github.com/dgryski/go-perfbook)). The library that allocates where a `[]byte`-accepting signature wouldn't is here — *not because it's unmeasured, but because the clear version is also the fast one.*

The rule is **not** "never think about performance." It is: spend clarity only when an audience that can prove the win asks for it. The library author avoids pessimizing the common case (a free win, no clarity cost). The app dev pays clarity only for a *profiled* hot path. The SRE's needs cost no clarity at all. The reader pays for everything else. This is exactly `go-idiomatic-discipline`'s dual axis ("clear AND correct") and `go-perf-methodology`'s "no profile, no trade," applied per-audience.

---

## 7. The Opposite-Call Example

The same line — "should this function allocate a result string?" — has **opposite correct answers** by audience:

```go
// Audience: LIBRARY AUTHOR of a serialization package.
//   You cannot profile the caller. The RIGHT call: don't force the alloc — offer Append.
func AppendQuoted(dst []byte, s string) []byte { /* ... */ } // caller controls allocation

// Audience: APP DEVELOPER, this function is cold (runs once at boot, not in any profile).
//   The RIGHT call: just return a string. Clarity wins; there is no measured hot path here.
func bootBanner(v string) string { return "starting " + v } // an Append form would be pure noise
```

The library author who returns a plain `string` here pessimizes thousands of unknown hot callers. The app dev who writes an `Append` form for a boot-time banner has paid clarity for a cold path no profile justifies. **Same line, opposite calls — because the audience is different.** Name it first.

The one-question litmus, in order:

1. **Who calls this code?** Unknown external/internal callers → you are a library author (§2); your own profilable service → app dev (§3). Set by *who calls*, not by binary-vs-module.
2. **Can I profile the real workload?** No → don't pessimize the common case, offer the zero-alloc form. Yes → profile, then optimize only the proven hot path.
3. **Will a human debug this in prod?** Always — so add the diagnosability (§4) that costs no clarity, regardless of the answers above.
4. **What does the next reader pay?** If the speed gain is unmeasured or cold, the answer is "everything, for nothing" — don't (§5).

---

## 8. Don't

- **Don't optimize before naming the audience.** Library, app, SRE, and reader pull in different directions; "faster" without "for whom" is undefined.
- **Don't (as a library author) force an allocation the caller can't opt out of.** Offer the `Append`/`[]byte` form; keep the zero value useful ([go-perfbook](https://github.com/dgryski/go-perfbook); [strconv](https://pkg.go.dev/strconv#AppendInt)).
- **Don't expose `unsafe` in a public API.** Keep it behind a safe, copying-by-contract signature — the caller can't audit your invariants.
- **Don't over-fit a library to one caller's microbenchmark.** It taxes the whole distribution; "use this other package over here" is a valid answer ([go-perfbook](https://github.com/dgryski/go-perfbook)).
- **Don't (as an app dev) micro-tune an unmeasured or cold path.** Amdahl: 2× on 5% of runtime = 2.5% total ([go-perfbook](https://github.com/dgryski/go-perfbook)). Profile first → `go-perf-methodology`.
- **Don't ship code an SRE can't diagnose.** Add `pprof` labels, expose `runtime/metrics`, set `GOMEMLIMIT`, keep frames named — these cost no clarity.
- **Don't make the next reader pay for a clever trick that saved 3ns on a cold path.** "Clear is better than clever" ([Proverbs](https://go-proverbs.github.io/)).
- **Don't apply the app-dev rule to an internal package with many unprofilable callers.** A shared `internal/` helper is a library to its callers; treat its API by §2's distribution rules.
- **Don't treat "~" from benchstat as a reason to ship anyway.** If the win isn't measurable, the clarity you spent bought nothing for any audience — revert it (→ `go-perf-methodology`).

---

## 9. Routing to Related Skills

**The policy root and the method (read these first):**
- `go-idiomatic-discipline` — the dual-axis "clear AND correct" policy; the next-reader tax (§5) and the clarity bound (§6) are its axis-2.
- `go-perf-methodology` — measure-first, "no profile, no trade," USE, benchstat, Amdahl; the app developer's (§3) entire toolkit.

**Library API allocation patterns (§2):**
- `go-perf-strings-bytes-zerocopy` — `strconv.Append*`, `[]byte`↔`string` no-copy, `strings.Builder`, `unsafe.String` safety rules.
- `go-perf-slices` — `make([]T, 0, n)`, `b = b[:0]` reuse, `append` growth — the buffer the caller owns.

**Diagnosability for the SRE (§4):**
- `go-perf-pprof-profiling` — `pprof.Labels`/`pprof.Do` to slice a profile by request type; production `net/http/pprof`.
- `go-perf-godebug-and-metrics` — `runtime/metrics`, `gctrace`, exposing metrics via expvar/Prometheus.
- `go-perf-gc-tuning` — `GOMEMLIMIT` so the pod GCs instead of OOMing; allocation rate as the lever.
- `go-perf-tail-latency` — p99/p999 the on-call actually watches.

**The `unsafe` risk (§2, §5):**
- `go-perf-strings-bytes-zerocopy` — `unsafe.String`/`unsafe.Slice` invariants; why they stay out of the API.
- `go-perf-simd` — hand assembly / `unsafe` bypasses bounds checks and the race detector; the reader's audit cost.

---

## 10. Reference Files

Audience anti-patterns — the same change right for one audience and wrong for another, each a wrong/right contrast with citations:

[references/common-mistakes.md](references/common-mistakes.md)

Source provenance for every claim in this skill:

[references/sources.yaml](references/sources.yaml)

