返回 Skills
cxuu/golang-skills· Apache-2.0 内容可用

go-performance

Use when optimizing Go code, investigating slow performance, or writing performance-critical sections. Also use when a user mentions slow Go code, string concatenation in loops, or asks about benchmarking, even if the user doesn't explicitly mention performance patterns. Does not cover concurrent performance patterns (see go-concurrency).

安装

与 skills.sh 相同的 Command / Prompt 安装方式


name: go-performance description: Use when optimizing Go code, investigating slow performance, or writing performance-critical sections. Also use when a user mentions slow Go code, string concatenation in loops, or asks about benchmarking, even if the user doesn't explicitly mention performance patterns. Does not cover concurrent performance patterns (see go-concurrency). allowed-tools: Bash(bash:*)

Go Performance Patterns

Resource Routing

  • scripts/bench-compare.sh - Run when comparing benchmark results, saving baselines, or producing JSON benchmark metadata.
  • references/BENCHMARKS.md - Read when writing benchmarks, using benchstat, or profiling with pprof.
  • references/STRING-OPTIMIZATION.md - Read when optimizing string conversion, concatenation, or byte/string boundaries.

Performance-specific guidelines apply only to the hot path. Don't prematurely optimize—focus these patterns where they matter most.


Prefer strconv over fmt

When converting primitives to/from strings, strconv is faster than fmt:

s := strconv.Itoa(rand.Int()) // ~2x faster than fmt.Sprint()
ApproachSpeedAllocations
fmt.Sprint143 ns/op2 allocs/op
strconv.Itoa64.2 ns/op1 allocs/op

Avoid Repeated String-to-Byte Conversions

Convert a fixed string to []byte once outside the loop:

data := []byte("Hello world")
for b.Loop() { // Go 1.24+; use b.N loops only for older Go
    w.Write(data) // ~7x faster than []byte("...") each iteration
}

Prefer Specifying Container Capacity

Specify container capacity where possible to allocate memory up front. This minimizes subsequent allocations from copying and resizing as elements are added.

Map Capacity Hints

Provide capacity hints when initializing maps with make():

m := make(map[string]os.DirEntry, len(files))

Note: Unlike slices, map capacity hints do not guarantee complete preemptive allocation—they approximate the number of hashmap buckets required.

Slice Capacity

Provide capacity hints when initializing slices with make(), particularly when appending:

data := make([]int, 0, size)

Unlike maps, slice capacity is not a hint—the compiler allocates exactly that much memory. Subsequent append() operations incur zero allocations until capacity is reached.

ApproachTime (100M iterations)
No capacity2.48s
With capacity0.21s

The capacity version is ~12x faster due to zero reallocations during append.


Pass Values

Don't pass pointers as function arguments just to save a few bytes. If a function refers to its argument x only as *x throughout, then the argument shouldn't be a pointer.

func process(s string) { // not *string — strings are small fixed-size headers
    fmt.Println(s)
}

Common pass-by-value types: string, io.Reader, small structs.

Exceptions:

  • Large structs where copying is expensive
  • Small structs that might grow in the future

String Concatenation

Choose the right strategy based on complexity:

MethodBest For
+Few strings, simple concat
fmt.SprintfFormatted output with mixed types
strings.BuilderLoop/piecemeal construction
strings.JoinJoining a slice
Backtick literalConstant multi-line text

Benchmarking and Profiling

Always measure before and after optimizing. Use Go's built-in benchmark framework and profiling tools.

go test -bench=. -benchmem -count=10 ./...

Validation: After applying optimizations, run bash scripts/bench-compare.sh to measure the actual impact. Only keep optimizations with measurable improvement.


Quick Reference

PatternBadGoodImprovement
Int to stringfmt.Sprint(n)strconv.Itoa(n)~2x faster
Repeated []byte[]byte("str") in loopConvert once outside~7x faster
Map initializationmake(map[K]V)make(map[K]V, size)Fewer allocs
Slice initializationmake([]T, 0)make([]T, 0, cap)~12x faster
Small fixed-size args*string, *io.Readerstring, io.ReaderNo indirection
Simple string joins1 + " " + s2(already good)Use + for few strings
Loop string buildRepeated +=strings.BuilderO(n) vs O(n²)

Related Skills

  • Data structures: See go-data-structures when choosing between slices, maps, and arrays, or understanding allocation semantics
  • Declaration patterns: See go-declarations when using make with capacity hints or initializing maps and slices
  • Concurrency: See go-concurrency when parallelizing work across goroutines or using sync.Pool for buffer reuse
  • Style principles: See go-style-core when deciding whether an optimization is worth the readability cost

附带文件

references/BENCHMARKS.md
# Benchmark Methodology

## Contents

- [Writing Benchmarks](#writing-benchmarks)
- [Running Benchmarks](#running-benchmarks)
- [Interpreting Results](#interpreting-results)
- [Using benchstat for Comparison](#using-benchstat-for-comparison)
- [Benchmark Examples from Performance Patterns](#benchmark-examples-from-performance-patterns)
- [Profiling with pprof](#profiling-with-pprof)
- [Common Mistakes](#common-mistakes)

## Writing Benchmarks

Go benchmarks use the `testing.B` type and live in `_test.go` files. Function
names must start with `Benchmark`. On Go 1.24+, prefer `b.Loop()`.

```go
func BenchmarkStrconv(b *testing.B) {
    for b.Loop() {
        s := strconv.Itoa(rand.Int())
        _ = s
    }
}

func BenchmarkFmtSprint(b *testing.B) {
    for b.Loop() {
        s := fmt.Sprint(rand.Int())
        _ = s
    }
}
```

Key rules:
- Use `b.Loop()` on Go 1.24+; use `for i := 0; i < b.N; i++` only when
  maintaining older Go versions
- Assign results to a variable (or `_`) to prevent the compiler from
  optimizing away the call
- Use `b.ResetTimer()` after expensive setup that shouldn't be measured
- Use `b.ReportAllocs()` or the `-benchmem` flag for allocation tracking

### Sub-benchmarks

```go
func BenchmarkConvert(b *testing.B) {
    for _, size := range []int{10, 100, 1000} {
        b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
            data := make([]byte, size)
            b.ResetTimer()
            for b.Loop() {
                _ = string(data)
            }
        })
    }
}
```

---

## Running Benchmarks

```bash
# Run all benchmarks in a package
go test -bench=. ./...

# Run specific benchmark with memory stats
go test -bench=BenchmarkStrconv -benchmem ./...

# Run with count for statistical significance
go test -bench=. -benchmem -count=10 ./...
```

The `-benchmem` flag reports allocations per operation. The `-count` flag runs
each benchmark N times for statistical significance.

---

## Interpreting Results

```
BenchmarkStrconv-8     18705042    64.2 ns/op    16 B/op    1 allocs/op
BenchmarkFmtSprint-8    8249536   143.0 ns/op    16 B/op    2 allocs/op
```

| Field | Meaning |
|-------|---------|
| `-8` | GOMAXPROCS |
| `18705042` | Number of iterations |
| `64.2 ns/op` | Time per operation |
| `16 B/op` | Bytes allocated per operation |
| `1 allocs/op` | Heap allocations per operation |

---

## Using benchstat for Comparison

`benchstat` compares benchmark results statistically. Install it and save
benchmark output to files:

```bash
# Install benchstat
go install golang.org/x/perf/cmd/benchstat@latest

# Run benchmarks and save results
go test -bench=. -benchmem -count=10 ./... > old.txt

# Make changes, then run again
go test -bench=. -benchmem -count=10 ./... > new.txt

# Compare results
benchstat old.txt new.txt
```

### Interpreting benchstat Output

```
name          old time/op    new time/op    delta
Strconv-8     64.2ns ± 2%    61.8ns ± 1%   -3.74%  (p=0.001 n=10+10)
```

- **delta**: Percentage change (negative = faster)
- **p-value**: Statistical significance (p < 0.05 is significant)
- **n**: Number of valid samples used

Tips:
- Always use `-count=10` or higher for reliable results
- A small p-value confirms the change is real, not noise
- If benchstat shows `~` (tilde), the difference is not statistically
  significant

---

## Benchmark Examples from Performance Patterns

### strconv vs fmt

| Approach | Speed | Allocations |
|----------|-------|-------------|
| `fmt.Sprint` | 143 ns/op | 2 allocs/op |
| `strconv.Itoa` | 64.2 ns/op | 1 allocs/op |

### Repeated Byte Conversions

```go
func BenchmarkRepeatedConversion(b *testing.B) {
    var buf bytes.Buffer
    for b.Loop() {
        buf.Write([]byte("Hello world"))
    }
}

func BenchmarkSingleConversion(b *testing.B) {
    var buf bytes.Buffer
    data := []byte("Hello world")
    for b.Loop() {
        buf.Write(data)
    }
}
```

| Approach | Speed |
|----------|-------|
| Repeated conversion | 22.2 ns/op |
| Single conversion | 3.25 ns/op |

### Slice Capacity

```go
func BenchmarkNoCapacity(b *testing.B) {
    for b.Loop() {
        data := make([]int, 0)
        for k := 0; k < 1000; k++ {
            data = append(data, k)
        }
    }
}

func BenchmarkWithCapacity(b *testing.B) {
    for b.Loop() {
        data := make([]int, 0, 1000)
        for k := 0; k < 1000; k++ {
            data = append(data, k)
        }
    }
}
```

| Approach | Time (100M iterations) |
|----------|------------------------|
| No capacity | 2.48s |
| With capacity | 0.21s |

---

## Profiling with pprof

Use `pprof` to identify bottlenecks before optimizing. Benchmarks measure
improvement; pprof finds where to improve.

### CPU Profiling

```bash
# Generate a CPU profile from benchmarks
go test -bench=BenchmarkHotPath -cpuprofile=cpu.prof ./...

# Analyze with pprof
go tool pprof cpu.prof
```

Common pprof commands:

```
(pprof) top10          # Top 10 functions by CPU time
(pprof) list funcName  # Annotated source for a function
(pprof) web            # Interactive graph in browser
```

### Memory Profiling

```bash
# Generate a memory profile
go test -bench=BenchmarkHotPath -memprofile=mem.prof ./...

# Analyze allocations
go tool pprof -alloc_space mem.prof
```

### HTTP Profiling for Running Services

```go
import _ "net/http/pprof"

func main() {
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    // ... application code ...
}
```

Access profiles at `http://localhost:6060/debug/pprof/`.

### Profiling Workflow

1. **Benchmark** the suspected hot path
2. **Profile** with pprof to confirm where time is spent
3. **Optimize** using patterns from this skill
4. **Re-benchmark** to verify improvement with benchstat
5. **Re-profile** to check for new bottlenecks

---

## Common Mistakes

### Ignoring the benchmark loop

The testing framework adjusts iteration counts to get stable timing. Using a
fixed iteration count produces meaningless results:

```go
// Bad: Ignores the benchmark loop, so the framework can't calibrate
func BenchmarkFixed(b *testing.B) {
    for i := 0; i < 1000; i++ {
        doWork()
    }
}

// Good: Use b.Loop on Go 1.24+
func BenchmarkCorrect(b *testing.B) {
    for b.Loop() {
        doWork()
    }
}
```

### Not preventing compiler elision

If the result of a function call is unused, the compiler may optimize the call
away entirely. Assign results to a package-level variable:

```go
// Bad: Compiler may optimize away the call
func BenchmarkElided(b *testing.B) {
    for b.Loop() {
        expensiveFunc()
    }
}

// Good: Assign to package-level var to prevent elision
var benchResult int

func BenchmarkKept(b *testing.B) {
    var r int
    for b.Loop() {
        r = expensiveFunc()
    }
    benchResult = r
}
```
references/STRING-OPTIMIZATION.md
# String Optimization Patterns

## strconv vs fmt

When converting primitives to/from strings, `strconv` is faster than `fmt`
because `fmt` uses reflection and handles arbitrary types.

Benchmark snippets use `b.Loop()`, available in Go 1.24 and newer. For older
Go versions, use `for i := 0; i < b.N; i++`.

**Bad:**

```go
for b.Loop() {
    s := fmt.Sprint(rand.Int())
}
```

**Good:**

```go
for b.Loop() {
    s := strconv.Itoa(rand.Int())
}
```

**Benchmark comparison:**

| Approach | Speed | Allocations |
|----------|-------|-------------|
| `fmt.Sprint` | 143 ns/op | 2 allocs/op |
| `strconv.Itoa` | 64.2 ns/op | 1 allocs/op |

Common conversions:

| Task | `fmt` | `strconv` |
|------|-------|-----------|
| Int → string | `fmt.Sprint(n)` | `strconv.Itoa(n)` |
| Int64 → string | `fmt.Sprint(n)` | `strconv.FormatInt(n, 10)` |
| Float → string | `fmt.Sprint(f)` | `strconv.FormatFloat(f, 'f', -1, 64)` |
| String → int | — | `strconv.Atoi(s)` |
| Bool → string | `fmt.Sprint(b)` | `strconv.FormatBool(b)` |

---

## Repeated String-to-Byte Conversions

Do not create byte slices from a fixed string repeatedly. Instead, perform the
conversion once and capture the result.

**Bad:**

```go
for b.Loop() {
    w.Write([]byte("Hello world"))
}
```

**Good:**

```go
data := []byte("Hello world")
for b.Loop() {
    w.Write(data)
}
```

**Benchmark comparison:**

| Approach | Speed |
|----------|-------|
| Repeated conversion | 22.2 ns/op |
| Single conversion | 3.25 ns/op |

The good version is **~7x faster** because it avoids allocating a new byte slice
on each iteration.

---

## String Concatenation

Choose the right string building strategy based on complexity.

### Use `+` for Simple Cases

```go
key := "projectid: " + p
```

The `+` operator is efficient for a small, fixed number of strings. The compiler
can often optimize adjacent string literals.

### Use `fmt.Sprintf` for Formatting

```go
// Good: clear formatting
str := fmt.Sprintf("%s [%s:%d]-> %s", src, qos, mtu, dst)

// Bad: + with manual conversions
str := src.String() + " [" + qos.String() + ":" + strconv.Itoa(mtu) + "]-> " + dst.String()
```

When writing to an `io.Writer`, use `fmt.Fprintf` directly instead of building a
temporary string with `fmt.Sprintf`.

### Use `strings.Builder` for Piecemeal Construction

`strings.Builder` takes amortized linear time, whereas repeated `+` or
`fmt.Sprintf` take quadratic time when building a large string:

```go
b := new(strings.Builder)
for i, d := range digitsOfPi {
    fmt.Fprintf(b, "the %d digit of pi is: %d\n", i, d)
}
str := b.String()
```

### Use Backticks for Constant Multi-line Strings

```go
// Good: raw string literal
usage := `Usage:

custom_tool [args]`

// Bad: concatenation with escape sequences
usage := "" +
    "Usage:\n" +
    "\n" +
    "custom_tool [args]"
```

### Strategy Summary

| Method | Best For | Performance |
|--------|----------|-------------|
| `+` | Few strings, simple concat | O(n) for small n |
| `fmt.Sprintf` | Formatted output | Slower, but clearer |
| `strings.Builder` | Loop/piecemeal construction | Amortized O(n) |
| `strings.Join` | Joining a slice | O(n) |
| Backtick literal | Constant multi-line text | Zero cost |
scripts/bench-compare.sh
#!/usr/bin/env bash
set -euo pipefail

VERSION="1.1.0"
SCRIPT_NAME="$(basename "$0")"

usage() {
    cat <<EOF
$SCRIPT_NAME v$VERSION — Run Go benchmarks with optional comparison

USAGE
    bash $SCRIPT_NAME [options] [package]

DESCRIPTION
    Wrapper around 'go test -bench' that runs benchmarks multiple times and
    optionally compares results against a saved baseline using benchstat.

    Results can be saved to a file for future comparison. If benchstat is
    installed and a baseline is provided, a statistical comparison is shown.

EXIT CODES
    0    Benchmarks ran successfully
    1    go test failed (compilation error, test failure, no benchmarks found)
    2    Usage error (missing arguments, bad flags, file exists without --force)

OPTIONS
    -h, --help           Show this help message
    -v, --version        Show version
    -n, --count N        Number of benchmark iterations (default: 5)
    -b, --baseline FILE  Compare results against this baseline file
    -s, --save FILE      Save benchmark results to this file
    -f, --filter REGEX   Benchmark filter regex (default: ".")
    --json               Output metadata as JSON (human output goes to stderr)
    --benchmem           Include memory allocation stats (default: on)
    --no-benchmem        Disable memory allocation stats
    --force              Allow --save to overwrite existing files
    --limit N            Max benchmark result lines to include (default: 0 = all)

ARGUMENTS
    package              Go package to benchmark (default: ./...)

EXAMPLES
    bash $SCRIPT_NAME
    bash $SCRIPT_NAME -n 10 ./pkg/parser
    bash $SCRIPT_NAME --save baseline.txt ./...
    bash $SCRIPT_NAME --baseline baseline.txt --save current.txt ./...
    bash $SCRIPT_NAME --filter BenchmarkSort -n 3
    bash $SCRIPT_NAME --json --limit 5 ./...
    bash $SCRIPT_NAME --save results.txt --force ./...
EOF
}

json_escape() {
    local s="$1"
    s="${s//\\/\\\\}"
    s="${s//\"/\\\"}"
    s="${s//$'\t'/\\t}"
    s="${s//$'\r'/}"
    s="${s//$'\n'/\\n}"
    printf '%s' "$s"
}

# Print human-readable output: stdout in text mode, stderr in JSON mode.
log() {
    if $JSON_OUTPUT; then
        echo "$@" >&2
    else
        echo "$@"
    fi
}

COUNT=5
BASELINE=""
SAVE=""
FILTER="."
PACKAGE=""
JSON_OUTPUT=false
BENCHMEM=true
FORCE=false
LIMIT=0

while [[ $# -gt 0 ]]; do
    case "$1" in
        -h|--help)      usage; exit 0 ;;
        -v|--version)   echo "$SCRIPT_NAME v$VERSION"; exit 0 ;;
        -n|--count)     COUNT="${2:?error: --count requires a number}"; shift 2 ;;
        -b|--baseline)  BASELINE="${2:?error: --baseline requires a file path}"; shift 2 ;;
        -s|--save)      SAVE="${2:?error: --save requires a file path}"; shift 2 ;;
        -f|--filter)    FILTER="${2:?error: --filter requires a regex}"; shift 2 ;;
        --json)         JSON_OUTPUT=true; shift ;;
        --benchmem)     BENCHMEM=true; shift ;;
        --no-benchmem)  BENCHMEM=false; shift ;;
        --force)        FORCE=true; shift ;;
        --limit)        LIMIT="${2:?error: --limit requires a number}"; shift 2 ;;
        -*)             echo "error: unknown option: $1" >&2; usage >&2; exit 2 ;;
        *)              PACKAGE="$1"; shift ;;
    esac
done

PACKAGE="${PACKAGE:-./...}"

if ! command -v go &>/dev/null; then
    echo "error: 'go' command not found in PATH" >&2
    exit 2
fi

if ! [[ "$COUNT" =~ ^[1-9][0-9]*$ ]]; then
    echo "error: --count must be a positive integer, got: $COUNT" >&2
    exit 2
fi

if ! [[ "$LIMIT" =~ ^[0-9]+$ ]]; then
    echo "error: --limit must be a non-negative integer, got: $LIMIT" >&2
    exit 2
fi

if [[ -n "$BASELINE" && ! -f "$BASELINE" ]]; then
    echo "error: baseline file not found: $BASELINE" >&2
    exit 2
fi

if [[ -n "$SAVE" && -f "$SAVE" ]] && ! $FORCE; then
    echo "error: save target already exists: $SAVE (use --force to overwrite)" >&2
    exit 2
fi

HAS_BENCHSTAT=false
if command -v benchstat &>/dev/null; then
    HAS_BENCHSTAT=true
fi

BENCH_ARGS=(-bench "$FILTER" -count "$COUNT" -run '^$')
if $BENCHMEM; then
    BENCH_ARGS+=(-benchmem)
fi

TMPFILE=$(mktemp "${TMPDIR:-/tmp}/bench-XXXXXX.txt")
trap 'rm -f "$TMPFILE"' EXIT

log "Running benchmarks: go test ${BENCH_ARGS[*]} $PACKAGE"
log "Iterations: $COUNT"
log ""

GO_EXIT=0
if $JSON_OUTPUT; then
    go test "${BENCH_ARGS[@]}" "$PACKAGE" 2>&1 | tee "$TMPFILE" >&2 || GO_EXIT=$?
else
    go test "${BENCH_ARGS[@]}" "$PACKAGE" 2>&1 | tee "$TMPFILE" || GO_EXIT=$?
fi

BENCH_COUNT=$(grep -cE '^Benchmark' "$TMPFILE" || true)

TRUNCATED=false
if [[ $LIMIT -gt 0 && $BENCH_COUNT -gt $LIMIT ]]; then
    TRUNCATED=true
fi

if ! $JSON_OUTPUT && $TRUNCATED; then
    log ""
    log "Note: $BENCH_COUNT benchmark results found, showing first $LIMIT (--limit $LIMIT)"
fi

if [[ -n "$SAVE" ]]; then
    cp "$TMPFILE" "$SAVE"
    log ""
    log "Results saved to: $SAVE"
fi

if [[ -n "$BASELINE" ]]; then
    log ""
    log "=== Comparison with baseline: $BASELINE ==="
    log ""
    if $HAS_BENCHSTAT; then
        if $JSON_OUTPUT; then
            benchstat "$BASELINE" "$TMPFILE" >&2 || true
        else
            benchstat "$BASELINE" "$TMPFILE" || true
        fi
    else
        log "note: install benchstat for statistical comparison:"
        log "  go install golang.org/x/perf/cmd/benchstat@latest"
        log ""
        log "--- Baseline ---"
        if $JSON_OUTPUT; then
            grep -E '^Benchmark' "$BASELINE" >&2 || true
        else
            grep -E '^Benchmark' "$BASELINE" || true
        fi
        log ""
        log "--- Current ---"
        if $JSON_OUTPUT; then
            grep -E '^Benchmark' "$TMPFILE" >&2 || true
        else
            grep -E '^Benchmark' "$TMPFILE" || true
        fi
    fi
fi

FINAL_EXIT=0
if [[ $GO_EXIT -ne 0 ]]; then
    FINAL_EXIT=1
    if ! $JSON_OUTPUT; then
        log ""
        log "error: go test exited with code $GO_EXIT"
    fi
elif [[ $BENCH_COUNT -eq 0 ]]; then
    FINAL_EXIT=1
    if ! $JSON_OUTPUT; then
        log ""
        log "error: no benchmarks found matching filter: $FILTER"
    fi
fi

if $JSON_OUTPUT; then
    BENCH_OUTPUT=$(<"$TMPFILE")
    if $TRUNCATED; then
        limited=""
        bench_seen=0
        while IFS= read -r line; do
            if [[ "$line" =~ ^Benchmark ]]; then
                bench_seen=$((bench_seen + 1))
                if [[ $bench_seen -le $LIMIT ]]; then
                    limited+="$line"$'\n'
                fi
            else
                limited+="$line"$'\n'
            fi
        done < "$TMPFILE"
        BENCH_OUTPUT="$limited"
    fi

    escaped_package=$(json_escape "$PACKAGE")
    escaped_filter=$(json_escape "$FILTER")
    escaped_baseline=$(json_escape "$BASELINE")
    escaped_save=$(json_escape "$SAVE")
    escaped_output=$(json_escape "$BENCH_OUTPUT")

    printf '{"count":%d,' "$COUNT"
    printf '"package":"%s",' "$escaped_package"
    printf '"filter":"%s",' "$escaped_filter"
    printf '"benchmarks_found":%d,' "$BENCH_COUNT"
    printf '"baseline":"%s",' "$escaped_baseline"
    printf '"save":"%s",' "$escaped_save"
    printf '"exit_code":%d,' "$GO_EXIT"
    printf '"output":"%s"' "$escaped_output"
    if $TRUNCATED; then
        printf ',"truncated":true'
    fi
    printf '}\n'
fi

exit $FINAL_EXIT