Advanced Golang Workflows
This reference consolidates advanced Go development workflows that are used less frequently.
Core workflows (used frequently) are in separate workflow files:
- Build (workflows/Build.md)
- Test (workflows/Test.md)
- Deps (workflows/Deps.md)
- Debug (workflows/Debug.md)
- Lint (workflows/Lint.md)
Benchmark - Performance Benchmarking
Run Go benchmarks to measure and optimize performance.
When to Use
- “benchmark go code”
- “measure performance”
- “run benchmarks”
- “profile performance”
- “compare benchmark results”
Quick Commands
Basic Benchmarking
# Run all benchmarks
go test -bench=.
# Run benchmarks in specific package
go test -bench=. ./internal/handlers
# Run specific benchmark
go test -bench=BenchmarkMyFunction
# Run benchmarks matching pattern
go test -bench=BenchmarkUser.*
With Memory Statistics
# Show memory allocations
go test -bench=. -benchmem
# Example output:
# BenchmarkFunction-8 1000000 1234 ns/op 512 B/op 5 allocs/op
# ^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^^^^
# iterations ns/operation bytes/op allocations/op
Benchmark Time Control
# Run for specific duration (default 1s)
go test -bench=. -benchtime=10s
# Run specific number of iterations
go test -bench=. -benchtime=1000x
# Quick benchmark (shorter duration)
go test -bench=. -benchtime=100ms
Writing Benchmarks
Basic Benchmark
func BenchmarkMyFunction(b *testing.B) {
// Setup (not timed)
input := generateInput()
// Reset timer to exclude setup
b.ResetTimer()
// Run function b.N times
for i := 0; i < b.N; i++ {
myFunction(input)
}
}
Benchmark with Table Tests
func BenchmarkParse(b *testing.B) {
benchmarks := []struct {
name string
input string
}{
{"small", "small input"},
{"medium", "medium sized input string"},
{"large", strings.Repeat("large ", 1000)},
}
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
parse(bm.input)
}
})
}
}
Parallel Benchmarks
func BenchmarkParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// Code to benchmark
doWork()
}
})
}
Reporting Custom Metrics
func BenchmarkCustomMetrics(b *testing.B) {
var totalBytes int64
for i := 0; i < b.N; i++ {
data := processData()
totalBytes += int64(len(data))
}
// Report custom metric
b.ReportMetric(float64(totalBytes)/float64(b.N), "bytes/op")
}
Profiling with Benchmarks
CPU Profiling
# Generate CPU profile
go test -bench=. -cpuprofile=cpu.prof
# Analyze CPU profile
go tool pprof cpu.prof
# Interactive pprof commands:
# top10 - Show top 10 functions
# list Func - Show source for function
# web - Generate graph (requires graphviz)
# pdf - Generate PDF (requires graphviz)
# Generate CPU profile visualization
go tool pprof -http=:8080 cpu.prof
Memory Profiling
# Generate memory profile
go test -bench=. -memprofile=mem.prof
# Analyze memory profile
go tool pprof mem.prof
# Show allocations
go tool pprof -alloc_space mem.prof
# Show in-use memory
go tool pprof -inuse_space mem.prof
Block Profiling
# Profile blocking operations
go test -bench=. -blockprofile=block.prof
# Analyze
go tool pprof block.prof
Mutex Profiling
# Profile mutex contention
go test -bench=. -mutexprofile=mutex.prof
# Analyze
go tool pprof mutex.prof
Comparing Benchmarks
Using benchstat
# Install benchstat
go install golang.org/x/perf/cmd/benchstat@latest
# Run baseline benchmark
go test -bench=. -count=10 > old.txt
# Make changes and run new benchmark
go test -bench=. -count=10 > new.txt
# Compare results
benchstat old.txt new.txt
# Example output:
# name old time/op new time/op delta
# Function-8 123ns ± 2% 98ns ± 1% -20.33% (p=0.000 n=10+10)
Manual Comparison
# Run benchmark multiple times for stability
go test -bench=BenchmarkMyFunc -count=5
# Save to file
go test -bench=. -benchmem > benchmark.txt
# Compare manually
# name time/op mem/op allocs/op
# Before: 1234ns 512B 5
# After: 987ns 256B 3
# Delta: -20% -50% -40%
Advanced Techniques
Sub-Benchmarks
func BenchmarkComplexOperation(b *testing.B) {
sizes := []int{10, 100, 1000, 10000}
for _, size := range sizes {
b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
data := make([]int, size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
processSlice(data)
}
})
}
}
Stopping and Starting Timer
func BenchmarkWithSetup(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer()
// Expensive setup not timed
data := generateExpensiveData()
b.StartTimer()
// This is timed
process(data)
}
}
Setting Bytes Processed
func BenchmarkProcess(b *testing.B) {
data := make([]byte, 1024*1024) // 1MB
b.SetBytes(int64(len(data)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
process(data)
}
// Will report MB/s
}
Common Patterns
Benchmark HTTP Handler
func BenchmarkHandler(b *testing.B) {
handler := setupHandler()
req := httptest.NewRequest("GET", "/users", nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
}
}
Benchmark with Pool
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func BenchmarkWithPool(b *testing.B) {
for i := 0; i < b.N; i++ {
buf := bufferPool.Get().(*bytes.Buffer)
buf.Reset()
// Use buffer
processWithBuffer(buf)
bufferPool.Put(buf)
}
}
Benchmark Allocations
func BenchmarkAllocations(b *testing.B) {
b.ReportAllocs() // Equivalent to -benchmem
for i := 0; i < b.N; i++ {
// Code that allocates
_ = make([]int, 100)
}
}
Makefile Example
.PHONY: bench
bench:
go test -bench=. -benchmem ./...
.PHONY: bench-cpu
bench-cpu:
go test -bench=. -cpuprofile=cpu.prof
go tool pprof -http=:8080 cpu.prof
.PHONY: bench-mem
bench-mem:
go test -bench=. -memprofile=mem.prof
go tool pprof -http=:8080 mem.prof
.PHONY: bench-compare
bench-compare:
@echo "Running baseline benchmarks..."
go test -bench=. -count=10 -benchmem > bench-old.txt
@echo "Make your changes, then run: make bench-compare-new"
.PHONY: bench-compare-new
bench-compare-new:
@echo "Running new benchmarks..."
go test -bench=. -count=10 -benchmem > bench-new.txt
benchstat bench-old.txt bench-new.txt
Best Practices
- Run multiple iterations: Use
-count=5or higher for stable results - Use benchstat: For statistical comparison of results
- Reset timer after setup: Exclude setup time with
b.ResetTimer() - Report allocations: Always use
-benchmemto see memory impact - Benchmark real scenarios: Use realistic data and workloads
- Run on consistent hardware: Same machine, same load for comparisons
- Profile before optimizing: Find bottlenecks with pprof
- Benchmark alternatives: Compare different implementations
- Document baseline: Save benchmark results in version control
- Use sub-benchmarks: Test different input sizes and scenarios
Interpreting Results
Time/Operation
- Lower is better
- Represents average time per operation
- Compare before/after optimization
Allocations/Operation
- Fewer is better
- Each allocation has overhead
- Reducing allocations improves performance
Bytes/Operation
- Fewer is better
- Memory allocations cause GC pressure
- Optimize hot paths to reduce allocations
MB/s (when using SetBytes)
- Higher is better
- Throughput measurement
- Useful for I/O operations
Common Flags
| Flag | Purpose |
|---|---|
-bench=. |
Run all benchmarks |
-benchmem |
Show memory stats |
-benchtime=Xs |
Run for X seconds |
-benchtime=Nx |
Run N iterations |
-count=N |
Run benchmarks N times |
-cpuprofile=file |
Write CPU profile |
-memprofile=file |
Write memory profile |
-blockprofile=file |
Write block profile |
-mutexprofile=file |
Write mutex profile |
Resources
Generate - Code Generation
Code generation with go generate, mocking, and other generation tools.
When to Use
- “generate go code”
- “run go generate”
- “create mocks”
- “generate from protobuf”
- “use stringer”
Quick Commands
go generate
# Run generators in current package
go generate
# Run in all packages
go generate ./...
# Verbose output
go generate -v ./...
# Dry run
go generate -n ./...
# Run specific generator
go generate -run mockgen ./...
Common Generators
Stringer (Enum String Methods)
//go:generate stringer -type=Status
type Status int
const (
Pending Status = iota
Active
Completed
)
// Generates: status_string.go with String() method
# Install
go install golang.org/x/tools/cmd/stringer@latest
# Generate
go generate ./...
# Usage
s := Active
fmt.Println(s.String()) // "Active"
mockgen (Mock Interfaces)
//go:generate mockgen -destination=mocks/mock_database.go -package=mocks . Database
type Database interface {
Get(id string) (*User, error)
Save(user *User) error
}
# Install
go install github.com/golang/mock/mockgen@latest
# Generate mocks
go generate ./...
# Use in tests
mockDB := mocks.NewMockDatabase(ctrl)
mockDB.EXPECT().Get("123").Return(user, nil)
protoc (Protocol Buffers)
// user.proto
syntax = "proto3";
package api;
message User {
string id = 1;
string name = 2;
}
//go:generate protoc --go_out=. --go_opt=paths=source_relative user.proto
# Install
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
# Generate
go generate ./...
Embed (Static Files)
//go:embed templates/*.html
var templates embed.FS
//go:embed static/*
var static embed.FS
func main() {
data, _ := templates.ReadFile("templates/index.html")
}
Advanced Generators
sqlc (SQL to Go)
# sqlc.yaml
version: "2"
sql:
- schema: "schema.sql"
queries: "queries.sql"
engine: "postgresql"
gen:
go:
package: "db"
out: "db"
//go:generate sqlc generate
Wire (Dependency Injection)
//go:build wireinject
//go:generate wire
func InitializeApp() (*App, error) {
wire.Build(
NewDatabase,
NewUserService,
NewApp,
)
return nil, nil
}
oapi-codegen (OpenAPI)
//go:generate oapi-codegen -package api -generate types,server openapi.yaml > api.gen.go
Generator Directives
Syntax
//go:generate command arg1 arg2
Examples
// Generate stringer
//go:generate stringer -type=Color
// Multiple commands
//go:generate mockgen -destination=mocks/mock.go . Interface
//go:generate gofmt -w mocks/mock.go
// With environment variables
//go:generate sh -c "VERSION=$(git describe) envsubst < version.tmpl > version.go"
// Conditional generation
//go:generate echo "Generating..."
Makefile Integration
.PHONY: generate
generate:
go generate ./...
gofmt -w .
.PHONY: generate-mocks
generate-mocks:
go generate -run mockgen ./...
.PHONY: generate-proto
generate-proto:
protoc --go_out=. --go_opt=paths=source_relative api/*.proto
.PHONY: generate-check
generate-check:
go generate ./...
git diff --exit-code
.PHONY: clean-generated
clean-generated:
find . -name "*.gen.go" -delete
find . -name "*_string.go" -delete
Best Practices
- Commit generated code: Makes builds reproducible
- Run before building: Ensure generated code is current
- Use specific versions: Pin generator tool versions
- Document generators: Comment what each directive does
- Format generated code: Run gofmt after generation
- Check in CI: Verify generated code is up-to-date
- Separate generated files: Use .gen.go suffix
CI/CD Integration
GitHub Actions
- name: Install generators
run: |
go install golang.org/x/tools/cmd/stringer@latest
go install github.com/golang/mock/mockgen@latest
- name: Run generators
run: go generate ./...
- name: Check for changes
run: |
if [[ `git status --porcelain` ]]; then
echo "Generated code is out of date"
git diff
exit 1
fi
Common Patterns
Version Embedding
//go:generate sh -c "echo 'package main\n\nconst Version = \"'$(git describe --tags)'\"' > version.go"
func main() {
fmt.Println("Version:", Version)
}
Interface Verification
//go:generate go run check_interfaces.go
// Verify implementation
var _ http.Handler = (*MyHandler)(nil)
Template Expansion
//go:generate go run gen_template.go
// gen_template.go
package main
import "text/template"
const tmpl = `// Code generated. DO NOT EDIT.
package {{.Package}}
type {{.Name}} struct {}`
func main() {
// Generate code from template
}
Resources
Profile - CPU and Memory Profiling
Advanced profiling and performance analysis for Go applications.
When to Use
- “profile go application”
- “find performance bottlenecks”
- “analyze memory usage”
- “trace execution”
- “optimize performance”
Types of Profiling
CPU Profiling
# During tests
go test -cpuprofile=cpu.prof -bench=.
# In application
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
# Capture profile
curl http://localhost:6060/debug/pprof/profile?seconds=30 > cpu.prof
# Analyze
go tool pprof cpu.prof
Memory Profiling
# Heap profile
go test -memprofile=mem.prof -bench=.
# From running app
curl http://localhost:6060/debug/pprof/heap > heap.prof
# Analyze
go tool pprof mem.prof
# Allocation profile
go tool pprof -alloc_space mem.prof
Block Profiling
# Profile blocking operations
import "runtime"
runtime.SetBlockProfileRate(1)
# Capture
curl http://localhost:6060/debug/pprof/block > block.prof
# Analyze
go tool pprof block.prof
Mutex Profiling
# Profile mutex contention
import "runtime"
runtime.SetMutexProfileFraction(1)
# Capture
curl http://localhost:6060/debug/pprof/mutex > mutex.prof
# Analyze
go tool pprof mutex.prof
pprof Commands
Interactive Mode
# Top functions by CPU
top
top10
# Show specific function
list functionName
# View as graph (requires graphviz)
web
# View as PDF
pdf
# Show call graph
traces
# Show cumulative time
top -cum
# Filter by function
top grep main
Web UI
# Start web UI
go tool pprof -http=:8080 cpu.prof
# Browse to http://localhost:8080
# Interactive flame graph, top view, source view
Execution Tracing
Generate Trace
# During tests
go test -trace=trace.out
# In application
import "runtime/trace"
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
# Or via HTTP
curl http://localhost:6060/debug/pprof/trace?seconds=5 > trace.out
Analyze Trace
# Open trace viewer
go tool trace trace.out
# View in browser - shows:
# - Goroutine execution
# - Network blocking
# - Synchronization blocking
# - System calls
# - GC events
Profiling in Code
Basic CPU Profile
import (
"os"
"runtime/pprof"
)
func main() {
f, _ := os.Create("cpu.prof")
defer f.Close()
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Your code here
}
Heap Profile
import (
"os"
"runtime/pprof"
)
func writeHeapProfile() {
f, _ := os.Create("heap.prof")
defer f.Close()
pprof.WriteHeapProfile(f)
}
HTTP pprof Server
import (
"net/http"
_ "net/http/pprof"
)
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Your application code
}
// Access profiles at:
// http://localhost:6060/debug/pprof/
Analyzing Results
CPU Profile Interpretation
- flat: Time spent in function itself
- cum: Cumulative time (including callees)
- %: Percentage of total time
Memory Profile Interpretation
- alloc_space: Total allocated bytes
- alloc_objects: Total allocated objects
- inuse_space: Currently in-use bytes
- inuse_objects: Currently in-use objects
Finding Hot Paths
# Top CPU consumers
go tool pprof -top cpu.prof
# Top memory allocators
go tool pprof -top mem.prof
# Call graph
go tool pprof -web cpu.prof
Benchmarking with Profiling
# CPU + memory profile
go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof -benchmem
# Compare before/after
benchstat old.txt new.txt
Continuous Profiling
Datadog Example
import "gopkg.in/DataDog/dd-trace-go.v1/profiler"
err := profiler.Start(
profiler.WithService("myapp"),
profiler.WithEnv("production"),
)
defer profiler.Stop()
Common Optimizations
Reduce Allocations
// Before: Creates new slice each time
func process(data []int) []int {
result := make([]int, 0)
for _, v := range data {
result = append(result, v*2)
}
return result
}
// After: Preallocate with capacity
func process(data []int) []int {
result := make([]int, 0, len(data))
for _, v := range data {
result = append(result, v*2)
}
return result
}
Use sync.Pool
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func process() {
buf := bufferPool.Get().(*bytes.Buffer)
defer bufferPool.Put(buf)
buf.Reset()
// Use buffer
}
Avoid String Concatenation in Loops
// Slow
var s string
for _, item := range items {
s += item // Creates new string each time
}
// Fast
var b strings.Builder
for _, item := range items {
b.WriteString(item)
}
s := b.String()
Makefile Example
.PHONY: profile-cpu
profile-cpu:
go test -bench=. -cpuprofile=cpu.prof
go tool pprof -http=:8080 cpu.prof
.PHONY: profile-mem
profile-mem:
go test -bench=. -memprofile=mem.prof
go tool pprof -http=:8080 mem.prof
.PHONY: trace
trace:
go test -trace=trace.out
go tool trace trace.out
.PHONY: profile-serve
profile-serve:
@echo "Starting pprof server on :6060"
@echo "CPU: curl http://localhost:6060/debug/pprof/profile?seconds=30 > cpu.prof"
@echo "Heap: curl http://localhost:6060/debug/pprof/heap > heap.prof"
go run -tags pprof main.go
Best Practices
- Profile in production-like environment: Real data, real load
- Focus on hot paths: Optimize the 20% that matters
- Measure before and after: Use benchstat for comparisons
- Profile different aspects: CPU, memory, blocking, contention
- Use flame graphs: Visual representation is powerful
- Sample for sufficient time: 30+ seconds for CPU profiles
- Check GC impact: High GC time indicates memory issues
- Monitor in production: Continuous profiling catches regressions