Rob Pike Style Guide
Overview
Rob Pike co-created Go at Google with Ken Thompson and Robert Griesemer. He also created Plan 9, Acme, sam, and co-invented UTF-8. His central thesis: simplicity is the ultimate sophistication, and most software is far too complex.
Core Philosophy
"Simplicity is complicated."
"Don't communicate by sharing memory; share memory by communicating."
"Clear is better than clever."
Pike believes that complexity is the enemy, and Go was designed as an antidote to the bloat of C++ and Java. Every feature in Go earned its place by being essential.
Design Principles
Simplicity Above All: If you can remove something without breaking functionality, remove it.
Composition Over Inheritance: Embed types, implement interfaces implicitly.
Concurrency as First-Class: Goroutines and channels, not threads and locks.
Orthogonality: Features should be independent and composable.
When Writing Code
Always
-
Use gofmt — no exceptions, no debates
-
Keep functions short and focused
-
Use interfaces for abstraction, keep them small
-
Handle errors explicitly at the call site
-
Use goroutines freely, they're cheap
-
Communicate via channels, not shared memory
-
Name things clearly—userCount not uc
Never
-
Fight gofmt
-
Create deep inheritance hierarchies (Go doesn't have them anyway)
-
Use interface{} without good reason
-
Ignore errors with _
-
Use panic for normal error handling
-
Create goroutines without knowing how they'll stop
Prefer
-
Small interfaces (1-2 methods ideal)
-
Returning errors over panicking
-
Channels over mutexes for coordination
-
Composition over embedding over "inheritance"
-
Standard library over third-party when possible
-
Table-driven tests
Code Patterns
Composition via Embedding
// NOT inheritance — composition type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
// Compose interfaces type ReadWriter interface { Reader Writer }
// Embed structs for composition type CountingWriter struct { io.Writer // Embedded — gets all Writer methods count int64 }
func (cw *CountingWriter) Write(p []byte) (int, error) { n, err := cw.Writer.Write(p) // Delegate to embedded cw.count += int64(n) return n, err }
Concurrency: Share by Communicating
// BAD: Sharing memory, communicating by locking type Counter struct { mu sync.Mutex value int }
func (c *Counter) Inc() { c.mu.Lock() c.value++ c.mu.Unlock() }
// GOOD: Communicate via channels func Counter() (inc func(), value func() int) { ch := make(chan int) go func() { count := 0 for delta := range ch { if delta == 0 { ch <- count // Request for value } else { count += delta } } }() return func() { ch <- 1 }, func() int { ch <- 0; return <-ch } }
// Even better: channel as work queue func worker(jobs <-chan Job, results chan<- Result) { for job := range jobs { results <- process(job) } }
func main() { jobs := make(chan Job, 100) results := make(chan Result, 100)
// Start workers
for i := 0; i < 4; i++ {
go worker(jobs, results)
}
// Send jobs, collect results...
}
Small Interfaces
// BAD: Large interface type Repository interface { Create(user User) error Read(id string) (User, error) Update(user User) error Delete(id string) error List() ([]User, error) Search(query string) ([]User, error) // ... and 20 more methods }
// GOOD: Small, focused interfaces type UserReader interface { Read(id string) (User, error) }
type UserWriter interface { Write(user User) error }
type UserDeleter interface { Delete(id string) error }
// Compose when needed type UserStore interface { UserReader UserWriter }
// Functions accept minimal interface func ProcessUser(r UserReader, id string) error { user, err := r.Read(id) // ... }
Error Handling
// Errors are values — handle them func readConfig(path string) (*Config, error) { f, err := os.Open(path) if err != nil { return nil, fmt.Errorf("open config: %w", err) } defer f.Close()
var cfg Config
if err := json.NewDecoder(f).Decode(&cfg); err != nil {
return nil, fmt.Errorf("decode config: %w", err)
}
return &cfg, nil
}
// Sentinel errors for checking var ErrNotFound = errors.New("not found")
func Find(id string) (*Item, error) { item, ok := store[id] if !ok { return nil, ErrNotFound } return item, nil }
// Caller can check: if errors.Is(err, ErrNotFound) { // handle not found }
Make the Zero Value Useful
// BAD: Requires initialization type Buffer struct { data []byte }
func NewBuffer() *Buffer { return &Buffer{data: make([]byte, 0, 1024)} }
// GOOD: Zero value works type Buffer struct { data []byte }
func (b *Buffer) Write(p []byte) (int, error) { b.data = append(b.data, p...) // nil slice append works! return len(p), nil }
// Can use immediately: var buf Buffer buf.Write([]byte("hello"))
// sync.Mutex zero value is unlocked // sync.WaitGroup zero value is ready // etc.
Mental Model
Pike approaches software by asking:
-
Is this necessary? Remove anything that isn't essential
-
Is this simple? Can someone understand it in 30 seconds?
-
Is this orthogonal? Does it compose with other features?
-
How does it fail? Design for failure cases explicitly
The Go Way
-
No generics (until Go 1.18) — and that was intentional restraint
-
No exceptions — errors are values
-
No inheritance — composition only
-
No operator overloading — + always means numeric addition
-
No implicit conversions — explicit is better
Each "missing" feature is a deliberate choice for simplicity.