agentsclimarketplace

Go performance

Skill ctoth/golang-skills-plugin/plugins/golang/skills/go-performance

Guides Go performance work as a measure-first discipline — never optimize from intuition; profile or benchmark first, then optimize only what the data proves is hot, without sacrificing clarity for unmeasured speed. Covers benchmarking with go test -bench -benchmem and comparing runs with benchstat (not eyeballing one noisy run); profiling with pprof (CPU -cpuprofile, heap -memprofile, net/http/pprof for live servers; top/list/web); escape analysis via go build -gcflags=-m (stack vs heap); allocation reduction (preallocate make([]T,0,n), reuse buffers, sync.Pool, strings.Builder, avoid []byte<->string copies); PGO (default.pgo); and the free runtime wins (container-aware GOMAXPROCS 1.25, Green Tea GC 1.26). Auto-invokes when optimizing Go performance, reducing allocations, profiling with pprof, reading escape analysis, or enabling PGO, and on "make this faster" / "why is this allocating" / "profile this" requests. Routes benchmark mechanics to go-testing-advanced, sync.Pool to go-sync-primitives.From its SKILL.md

Install
npx -y skills add ctoth/golang-skills-plugin --skill go-performance

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

15.5 KB, ~3.8k tokens by cl100k_base, as published. Nobody here has run it

Go Performance

"You can't tell where a program is going to spend its time. Bottlenecks occur in surprising places, so don't try to second guess and put in a speed hack until you've proven that's where the bottleneck is." "Measure. Don't tune for speed until you've measured, and even then don't unless one part of the code overwhelms the rest." — Rob Pike, Notes on Programming in C

"We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil." — Donald Knuth, Structured Programming with go to Statements (Computing Surveys, 1974)

Performance work in Go is a measure-first discipline, not a bag of tricks. The single most expensive mistake is optimizing code that was never hot — paying readability to speed up a path the program barely runs. This skill makes the loop explicit: profile or benchmark, find the real hot path, change one thing, measure again. Everything below serves that loop.

All Go below builds clean under gofmt, go vet, and go test on Go 1.26.


1. The Discipline: Measure First, Never Guess

The order is non-negotiable, because "bottlenecks occur in surprising places" (Pike):

  1. Profile or benchmark to find where the time and allocations actually go.
  2. Change one thing that the data points at.
  3. Measure again — and compare statistically, not by eyeball.

This is the same dual-axis judgment as the policy root (go-idiomatic-discipline): "clear AND correct." An unmeasured micro-optimization that trades clarity for speed you never proved is the ceiling failure — it ships, it's correct, and the next reader pays for cleverness that bought nothing. The bar for sacrificing readability is a profile or a benchmark that names the cost. No profile, no trade. "Clear is better than clever" (Go Proverbs) loses only to evidence.

ToolQuestion it answersCommand
Benchmark (-benchmem)How fast / how many allocs is this function?go test -bench=. -benchmem
benchstatIs the change real or noise?benchstat old.txt new.txt
CPU profileWhere does wall/CPU time go?go test -cpuprofile=cpu.prof
Heap profileWhere do allocations come from?go test -memprofile=mem.prof
Escape analysisWhat is forced onto the heap?go build -gcflags=-m
PGOFree inlining of measured hot funcsdefault.pgo in the main package

2. Benchmark with -benchmem, Compare with benchstat

A benchmark is the unit of evidence. go test -bench=. -benchmem adds the allocation columns; b.ReportAllocs() turns them on per-function. The benchmark mechanicsfor b.Loop() (Go 1.24), the dead-code/sink trap, b.ResetTimer, b.RunParallel — are owned by go-testing-advanced §2–3; read it for how to write the loop. This skill owns what you do with the numbers.

BenchmarkJoinNaive-32      6748058    179.3 ns/op   216 B/op   7 allocs/op
BenchmarkJoinBuilder-32   24888777     49.11 ns/op   48 B/op   1 allocs/op

The allocs/op column is the lever: 7 → 1 allocations is a real, durable win (string += in a loop reallocates every iteration; strings.Builder with one Grow does not). But one run is not a measurement — CPU scaling, GC, and noise swamp small deltas. To know a change is real, run each side ≥10 times and let benchstat do the statistics:

go test -run='^$' -bench=. -count=10 > old.txt
# ... make the change ...
go test -run='^$' -bench=. -count=10 > new.txt
benchstat old.txt new.txt

"Benchstat computes statistical summaries and A/B comparisons of Go benchmarks" and "Each benchmark should be run at least 10 times to gather a statistically significant sample" (benchstat). It reports the delta with a p-value: a ~ "indicates no statistically significant difference," while -17.20% (p=0.000 n=10) is a change you can trust (benchstat). Reporting a single before/after pair as proof is the classic eyeball error.


3. Profiling with pprof — Find the Real Hot Path

When you don't already know which function is hot, profile; don't guess. "Profiling tools analyze the complexity and costs of a Go program such as its memory usage and frequently called functions to identify the expensive sections" (Diagnostics). The two profiles you reach for most:

  • CPU — "determines where a program spends its time while actively consuming CPU cycles (as opposed to while sleeping or waiting for I/O)" (Diagnostics). When enabled, "the Go program stops about 100 times per second and records a sample consisting of the program counters on the currently executing goroutine's stack" (Profiling Go Programs).
  • heap — "reports memory allocation samples; used to monitor current and historical memory usage, and to check for memory leaks" (Diagnostics).

The fastest way to get both is straight from a benchmark — no code changes:

go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof
go tool pprof -top cpu.prof      # the heaviest functions
go tool pprof cpu.prof           # interactive: top, list Fn, web

Inside go tool pprof, three commands carry the work: "The most important command is topN, which shows the top N samples"; list Fn shows per-line counts where "the first three columns are the number of samples taken while running that line, the number … in code called from that line, and the line number"; and "the web command writes a graph of the profile data in SVG format and opens it in a web browser" (needs graphviz) (Profiling Go Programs). Use -cum to sort by cumulative cost up the call tree.

Live servers: import net/http/pprof for its side effect — "Package pprof serves via its HTTP server runtime profiling data" under paths that "all begin with /debug/pprof/" (net/http/pprof):

import _ "net/http/pprof"  // registers /debug/pprof/ handlers on the default mux
// go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }()
go tool pprof http://localhost:6060/debug/pprof/heap
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30   # 30s CPU

It is "safe to profile programs in production, but enabling some profiles (e.g. the CPU profile) adds cost" (Diagnostics). One caveat: "Use tools in isolation to get more precise info" — precise memory profiling skews CPU profiles (Diagnostics). block and mutex profiles are off by default (runtime.SetBlockProfileRate, runtime.SetMutexProfileFraction).


4. Escape Analysis — Stack Is Free, Heap Costs GC

Stack allocations are reclaimed for free when the function returns; heap allocations create GC work. The compiler decides per value, and it will show you its reasoning: -m is documented as "Print optimization decisions" (cmd/compile).

go build -gcflags=-m ./...     # add a second -m (-gcflags='-m -m') for more detail

The lines that matter read like moved to heap: x, ... escapes to heap, or the reassuring ... does not escape. A verified example — returning the address of a local forces it onto the heap:

// escapes: the local p outlives the call, so it must live on the heap
func NewPointHeap(x, y int) *Point { p := Point{x, y}; return &p }
//   ./perf.go:51:2: moved to heap: p

// stays on the stack: the value is copied in, nothing outlives the call
func SumPointValue(p Point) int { return p.X + p.Y }

The recurring escape triggers to recognize in a hot path:

  • Returning a pointer to a local (above) — return the value if it's small; the copy is cheaper than the GC.
  • Interface boxing — assigning a concrete value to an any/interface{} (and fmt.Sprint(x), whose ...any boxes every argument) heap-allocates it.
  • Closures capturing by reference — a variable captured by a closure that outlives the frame escapes.
  • Slices/maps that outlive the call or grow past a size the compiler can't bound.

Escape analysis tells you why the heap profile looks the way it does — read them together.


5. Reduce Allocations Once a Profile Points There

Only after a heap profile or -benchmem names the cost. The high-leverage moves, each verified by an alloc delta:

  • Preallocate when the size is known. make([]T, 0, n) gives append its backing array once instead of growing-and-copying. Measured: appending 1024 ints with no prealloc was 25208 B/op, 12 allocs/op; with make([]int, 0, n) it was 0 B/op, 0 allocs/op. Same for maps: make(map[K]V, n). (Slice/map mechanics: go-slices-and-maps.)
  • Build strings with strings.Builder, not +=. s += p in a loop reallocates the whole string every iteration. Measured: naive += was 216 B/op, 7 allocs/op; strings.Builder with one Grow(n) was 48 B/op, 1 allocs/op. (Builder + conversion cost: go-strings-bytes-runes.)
  • Avoid []bytestring conversions in hot loops — each conversion copies. Work in one representation; reach for strconv over fmt for number formatting.
  • Reuse buffersbuf = buf[:0] keeps the capacity and resets the length.
  • sync.Pool for transient objects. "A Pool is a set of temporary objects that may be individually saved and retrieved"; its "purpose is to cache allocated but unused items for later reuse, relieving pressure on the garbage collector" (sync.Pool). The caveat: "Any item stored in the Pool may be removed automatically at any time without notification" — so it's for fungible scratch (byte buffers), never for objects that must persist. Mechanics and copylock rules: go-sync-primitives.

6. PGO — Free Inlining of Functions a Profile Proves Hot

Profile-Guided Optimization feeds a real CPU profile back into the compiler. "The Go toolchain will automatically enable PGO when it finds a profile named default.pgo in the main package directory" (PGO blog); "By default, go build will detect default.pgo files automatically and enable PGO" (PGO guide). Profiles "from runtime/pprof and net/http/pprof can be used directly as the compiler input" — so the CPU profile you already collected is the input.

go tool pprof -proto ... > default.pgo   # a representative production CPU profile
go build ./...                           # auto-detected; no flag needed

With it, "the compiler may decide to more aggressively inline functions which the profile indicates are called frequently" (PGO guide). The payoff is modest and free: "benchmarks for a representative set of Go programs show that building with PGO improves performance by around 2-14%" (PGO guide). Commit default.pgo to the repo — it "ensures that users automatically have access to the profile … and that builds remain reproducible" (PGO blog). Collect it from production, not a synthetic micro-benchmark, and refresh it occasionally as the code drifts.


7. Runtime Wins You Get for Free

Two recent releases cut overhead with no code change — they raise the floor before you optimize anything:

  • Container-aware GOMAXPROCS (Go 1.25). "On Linux, the runtime considers the CPU bandwidth limit of the cgroup containing the process … If the CPU bandwidth limit is lower than the number of logical CPUs available, GOMAXPROCS will default to the lower limit" (Go 1.25). This stops a containerized binary from spawning a thread per host core and thrashing under a Kubernetes CPU limit. It "periodically updates GOMAXPROCS if … the cgroup CPU bandwidth limit change[s]," and is disabled if you set GOMAXPROCS manually.
  • Green Tea GC default (Go 1.26). The collector that "improves the performance of marking and scanning small objects through better locality and CPU scalability," previously a 1.25 experiment, "is now enabled by default" — "we expect somewhere between a 10–40% reduction in garbage collection overhead in real-world programs that heavily use the garbage collector" (Go 1.26). Opt out with GOEXPERIMENT=nogreenteagc (expected removed in 1.27).

Gate any version-specific idiom on the module's go directive — see go-version-feature-map.


8. Don't

  • Don't optimize without a profile. Guessing the hot spot is the root error; "bottlenecks occur in surprising places" (Pike).
  • Don't micro-optimize cold code. A 50% speedup on a path that runs 0.1% of the time is noise you paid clarity for.
  • Don't trade readability for unproven speed. That's the axis-2 failure of go-idiomatic-discipline. The price of clever is a profile that names the win.
  • Don't eyeball one benchmark run. Use -count and benchstat; a single delta is within the noise.
  • Don't tune the GC (GOGC, GOMEMLIMIT) before measuring that GC is your bottleneck — a mutex or a syscall usually isn't fixed by GC knobs.
  • Don't sync.Pool objects that must persist — pooled items can vanish at any time.

9. Routing to Related Skills

  • go-idiomatic-discipline — the policy root; unmeasured micro-opt vs clarity is its axis-2 ("clear AND correct") failure.
  • go-testing-advanced — benchmark mechanics: for b.Loop(), the sink/dead-code trap, b.ReportAllocs, b.RunParallel. This skill interprets the numbers it produces.
  • go-sync-primitivessync.Pool mechanics, copylock, and the rest of shared-memory synchronization.
  • go-slices-and-maps — preallocation (make([]T,0,n)), capacity, append/aliasing semantics.
  • go-strings-bytes-runesstrings.Builder and the cost of []bytestring conversion.
  • go-version-feature-map — version floors: PGO GA 1.21, container-aware GOMAXPROCS 1.25, Green Tea GC default 1.26.

10. Reference Files

High-frequency performance anti-patterns in LLM-generated Go, each with wrong/right code and citations:

${CLAUDE_SKILL_DIR}/references/common-mistakes.md

Source provenance for every claim in this skill:

${CLAUDE_SKILL_DIR}/references/sources.yaml

What ships with it: 2 files

18.9 KB alongside SKILL.md

references/

Keep looking

Skills are one crate of 326,851. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.