Bill Kennedy Style Guide
Overview
Bill Kennedy is the author of "Go in Action" and founder of Ardan Labs. His teaching emphasizes mechanical sympathy: understanding how software interacts with hardware. His "Ultimate Go" course is legendary for deep-dive explanations.
Core Philosophy
"Integrity, readability, and simplicity—in that order."
"If you don't understand the data, you don't understand the problem."
"Mechanical sympathy: understanding how the hardware and runtime work."
Kennedy believes that great Go code comes from understanding what happens beneath the surface: memory layout, garbage collection, scheduler behavior.
Design Principles
Data-Oriented Design: Design around data transformations, not object hierarchies.
Mechanical Sympathy: Write code that works with the hardware, not against it.
Value Semantics First: Prefer values over pointers unless you have a reason.
Integrity First: Correctness beats performance, readability beats cleverness.
When Writing Code
Always
-
Understand the memory layout of your data structures
-
Know when copies happen and when references are used
-
Consider CPU cache behavior for hot paths
-
Profile before optimizing
-
Use value semantics by default
-
Understand escape analysis
Never
-
Optimize without profiling
-
Use pointers just to "avoid copies" without measuring
-
Create deep pointer chains (bad for cache)
-
Ignore alignment and padding
-
Assume you know what escapes to heap
Prefer
-
Contiguous data (slices) over pointer-heavy structures
-
Value receivers for small, immutable types
-
Stack allocation over heap when possible
-
Struct of arrays over array of structs for hot loops
-
Understanding over blind rules
Code Patterns
Data-Oriented Design
// BAD: Object-oriented thinking, pointer-heavy type Node struct { Value int Children []*Node // Pointers scattered in memory }
// GOOD: Data-oriented, cache-friendly type Tree struct { Values []int // Contiguous memory Children [][]int // Indices into Values }
// For hot loops, struct of arrays beats array of structs // BAD: Array of structs (AoS) type Particle struct { X, Y, Z float64 VX, VY, VZ float64 Mass float64 } particles := make([]Particle, 1000)
// GOOD: Struct of arrays (SoA) - better cache utilization type Particles struct { X, Y, Z []float64 VX, VY, VZ []float64 Mass []float64 } p := Particles{ X: make([]float64, 1000), Y: make([]float64, 1000), // ... }
// When updating just positions: for i := range p.X { p.X[i] += p.VX[i] // Sequential memory access p.Y[i] += p.VY[i] p.Z[i] += p.VZ[i] }
Value vs Pointer Semantics
// Value semantics: type is small, immutable logically type Time struct { sec int64 nsec int32 }
func (t Time) Add(d Duration) Time { return Time{sec: t.sec + int64(d), nsec: t.nsec} }
// Pointer semantics: type represents a resource or is large type File struct { fd int name string // ... }
func (f *File) Read(b []byte) (int, error) { // Modifies state, represents resource }
// RULE: Pick one semantic and be consistent for a type // If any method needs pointer, use pointer for all methods
Understanding Escape Analysis
// Stack allocation: fast, automatic cleanup func sumLocal() int { numbers := [4]int{1, 2, 3, 4} // Array on stack sum := 0 for _, n := range numbers { sum += n } return sum // numbers never escapes }
// Heap allocation: slower, needs GC func sumHeap() *int { sum := 0 for i := 0; i < 4; i++ { sum += i } return &sum // sum escapes to heap! }
// Check with: go build -gcflags="-m" // ./main.go:10:2: moved to heap: sum
// Slices and interfaces often cause escapes func process(data []byte) { // If data is used after function returns // or passed to interface{}, it may escape }
Memory Layout Awareness
// Struct padding wastes memory // BAD: Poor layout (24 bytes with padding) type BadLayout struct { a bool // 1 byte + 7 padding b int64 // 8 bytes c bool // 1 byte + 7 padding }
// GOOD: Optimized layout (16 bytes) type GoodLayout struct { b int64 // 8 bytes a bool // 1 byte c bool // 1 byte + 6 padding }
// Check with: unsafe.Sizeof() // Or use: go vet -fieldalignment
Slice Internals
// Slice header: (pointer, length, capacity) // Understanding this prevents bugs
func modify(s []int) { s[0] = 999 // Modifies original! s = append(s, 4) // May or may not affect original }
func main() { original := []int{1, 2, 3} modify(original) // original[0] is 999 // but append may have created new backing array }
// Safe pattern: return the slice func appendSafe(s []int, v int) []int { return append(s, v) }
original = appendSafe(original, 4)
Benchmarking Properly
func BenchmarkProcess(b *testing.B) { // Setup outside the loop data := generateTestData()
b.ResetTimer() // Don't count setup time
for i := 0; i < b.N; i++ {
result := Process(data)
// Prevent compiler from optimizing away
_ = result
}
}
// Compare implementations func BenchmarkProcessV1(b *testing.B) { ... } func BenchmarkProcessV2(b *testing.B) { ... }
// Run with: go test -bench=. -benchmem // BenchmarkProcessV1-8 1000000 1234 ns/op 256 B/op 3 allocs/op // BenchmarkProcessV2-8 2000000 567 ns/op 0 B/op 0 allocs/op
Goroutine Pool Pattern
type Pool struct { work chan func() sem chan struct{} }
func NewPool(size int) *Pool { p := &Pool{ work: make(chan func()), sem: make(chan struct{}, size), } return p }
func (p *Pool) Submit(task func()) { select { case p.work <- task: // Worker picked it up case p.sem <- struct{}{}: // Start new worker go p.worker(task) } }
func (p *Pool) worker(task func()) { defer func() { <-p.sem }()
for {
task()
task = <-p.work
}
}
Mental Model
Kennedy teaches by asking:
-
What's the data? Understand it before writing code.
-
Where does it live? Stack? Heap? How is it laid out?
-
How does it flow? What transformations happen?
-
What's the cost? Allocations, copies, cache misses?
Kennedy's Priorities
-
Integrity: Code must be correct
-
Readability: Code must be maintainable
-
Simplicity: Don't over-engineer
-
Performance: After the above are satisfied
In that order.