<< All versions

Skill v1.0.2

currentAutomated scan100/100
caarlos0/dotfiles/go-performance

2 files

──Details
PublishedOctober 25, 2026 at 10:15 PM
Content Hashsha256:fcbe0ef595c6919d...
Git SHA8f5a11fcf925
Bump Typepatch
Compare with v1.0.1
──Files
Files (1 file, 5.5 KB)
SKILL.md5.5 KBactive
SKILL.md · 157 lines · 5.5 KB

version: "1.0.2" name: go-performance description: Profile and optimize Go CPU, allocations, GC, concurrency, and I/O with benchmarks and pprof.


Go Performance

Profile before optimizing. Pin the Go version, GOMAXPROCS, input, and benchmark flags. Change one measured bottleneck and compare with benchstat.

Benchmarks

Benchmark production code with testing.B. Report allocations and throughput where relevant:

go
func BenchmarkEncode(b *testing.B) {
b.ReportAllocs()
b.SetBytes(int64(len(input)))
for b.Loop() {
Encode(input)
}
}

Use b.RunParallel only when contention or scaling is the behavior under test. Collect multiple samples:

bash
go test -run='^$' -bench=BenchmarkEncode -benchmem -count=10 ./pkg > old.txt
go test -run='^$' -bench=BenchmarkEncode -benchmem -count=10 ./pkg > new.txt
benchstat old.txt new.txt

Use realistic sizes and distributions. Include benchstat output with a performance change; a single ns/op result is not evidence.

Profiles and trace

From tests:

bash
go test -bench=BenchmarkEncode \
-cpuprofile=cpu.prof -memprofile=heap.prof \
-blockprofile=block.prof -mutexprofile=mutex.prof
go tool pprof -http=:8080 cpu.prof
go tool pprof -top -cum -alloc_objects heap.prof

For services, expose net/http/pprof only on an authenticated internal listener. Use:

  • CPU profile for sampled on-CPU hot paths.
  • allocs for allocation sites and heap for live objects.
  • block and mutex profiles for contention.
  • goroutine profiles for growth and stuck stacks.
  • go tool trace for scheduler latency, goroutine transitions, syscalls, GC,

and network blocking that sampling profiles do not explain.

Escapes, inlining, and allocations

bash
go build -gcflags='-m=2' ./pkg/...

Escape and inlining diagnostics are leads. Confirm the allocation or call site in a benchmark or profile before changing ownership.

  • Pre-size slices and maps when the size is known:

make([]T, 0, n) and make(map[K]V, n).

  • Reuse slice capacity with s = s[:0] only when retained backing arrays do not

pin excessive memory.

  • Use strings.Builder for strings and bytes.Buffer for bytes when repeated

appends are measured. Do not copy a non-zero Builder; call Reset before reuse.

  • Prefer strconv.Append* or Format* over fmt.Sprintf in formatting hot

paths.

  • Interface conversions and interface calls can cause escapes or prevent

inlining and devirtualization. They do not always allocate; verify with -gcflags=-m=2 and -benchmem.

  • Generics do not guarantee faster code. Compare generated code and benchmarks

before replacing an interface API.

Pools and memory

sync.Pool is for temporary reusable objects that may be discarded at any time. It provides no retention guarantee. Reset objects before Put, validate state after Get, and benchmark pool contention plus retained capacity.

Use heap profiles to distinguish allocation rate from live heap. A large alloc_space profile with a small inuse_space profile indicates churn, not a leak. A goroutine retaining a reference can pin a large object graph.

Tune GOGC or GOMEMLIMIT only after allocation and GC profiles identify the trade-off. GOMEMLIMIT is a soft runtime memory limit, not an RSS cap.

Concurrency

  • Use block and mutex profiles before replacing locks with channels, atomics,

sharding, or lock-free structures.

  • Use atomics only for small independent state with documented memory-ordering

invariants.

  • Bound goroutine fan-out and queue capacity. Every goroutine needs an exit

through context cancellation, channel closure, or owned lifecycle.

  • Investigate rising goroutine counts with two goroutine profiles under stable

load; compare which stacks accumulate.

  • Avoid holding locks across I/O or callbacks. Keep critical sections small

without splitting invariants.

  • Treat cache-line padding and false-sharing fixes as hardware-specific

hypotheses; prove them with contention benchmarks.

Invoke runtime-process-debugging for child processes, pipes, shutdown, or scheduler/lifecycle stalls.

I/O and serialization

Use bufio.Reader/Writer for repeated small operations and batch small writes. Reuse byte buffers where ownership is clear. Preserve partial-write handling, flush errors, EOF, deadlines, cancellation, and ordering.

encoding/json, reflection, and struct-tag processing can be hot in serialization-heavy programs. Profile before adding generated codecs or a dependency; include compatibility, escaping, number handling, and error behavior in comparisons.

PGO and low-level code

Use a representative CPU profile as default.pgo and compare:

bash
go build -pgo=auto ./...

PGO can alter inlining and devirtualization, so re-run benchmarks and binary size checks on the deployment target.

Before unsafe, assembly, or architecture-specific code, confirm bounds checks, dispatch, cache misses, or instructions are material. The low-level version must outperform safe Go and document its invariant and portability.

Regression guards

Pin Go and GOMAXPROCS. Use focused benchmarks and allocation assertions for stable invariants. Keep wall time on shared runners advisory unless variance is controlled.

Related skills

  • go-conventions takes precedence for correctness and API design.
  • my-code-review checks a completed diff. When invoked from my-code-review, do not

invoke it again.

  • code-simplifier runs after the gain is proven.
  • runtime-process-debugging owns process and lifecycle stalls.

Correctness overrides performance.

← v1.0.1All versions