Go proverbs and simplicity
Rob Pike's core belief: Simplicity is the ultimate sophistication. The best code is the code that isn't there. When in doubt, leave it out.
"Complexity is multiplicative: fixing a problem by making one part of the system more complex slowly but surely adds complexity to other parts."
Every added feature, abstraction, or clever trick has a cost. That cost compounds. The goal is not to build the most powerful system, but the simplest system that works.
From "Notes on Programming in C":
Bottlenecks occur in surprising places. Don't guess. Don't optimize without data.
Not this:
// "I bet this loop is slow, let me optimize it"
// [spends 3 hours optimizing code that runs once at startup]
This:
// Profile first
// pprof shows 80% of time in database calls
// Optimize database calls
"Measure. Don't tune for speed until you've measured, and even then don't unless one part of the code overwhelms the rest."
Intuition is unreliable. Profilers don't lie. Measure before you touch anything.
And N is usually small.
Not this:
// Using red-black tree for 20 items
tree := redblack.New()
This:
// Linear search is fine for 20 items
for _, item := range items {
if item.ID == target {
return item
}
}
They're harder to implement, harder to debug, and the constant factors are often large. Simple algorithms with simple data structures are easier to get right.
"If you've chosen the right data structures and organized things well, the algorithms will almost always be self-evident."
Get the data structures right. The code follows.
The most important proverb. If someone has to puzzle over your code, you've failed.
Not this:
// Clever one-liner
return a[i], a[j] = a[j], a[i], len(a) > i && len(a) > j
This:
// Clear
if i >= len(a) || j >= len(a) {
return false
}
a[i], a[j] = a[j], a[i]
return true
Small interfaces are powerful. io.Reader has one method. It's used everywhere.
Not this:
type DataStore interface {
Get(key string) ([]byte, error)
Put(key string, value []byte) error
Delete(key string) error
List(prefix string) ([]string, error)
Watch(key string) (<-chan Event, error)
Transaction(func(Txn) error) error
Backup(path string) error
Restore(path string) error
// ... 15 more methods
}
This:
type Reader interface {
Read(key string) ([]byte, error)
}
type Writer interface {
Write(key string, value []byte) error
}
// Compose when needed
type ReadWriter interface {
Reader
Writer
}
A type's zero value should be immediately usable without initialization.
Not this:
type Buffer struct {
data []byte
}
func NewBuffer() *Buffer {
return &Buffer{data: make([]byte, 0, 64)}
}
// User must remember to call NewBuffer()
This:
type Buffer struct {
data []byte
}
func (b *Buffer) Write(p []byte) {
b.data = append(b.data, p...) // Works even when b.data is nil
}
// var b Buffer; b.Write(data) just works
Errors are not exceptions. They're values you program with.
Not this:
// Just checking, not handling
if err != nil {
return err
}
This:
// Errors are values - you can work with them
type errWriter struct {
w io.Writer
err error
}
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return // Skip if already errored
}
_, ew.err = ew.w.Write(buf)
}
Add context. Make errors actionable. Help the person debugging at 3am.
Not this:
return err
This:
return fmt.Errorf("loading config from %s: %w", path, err)
Don't import a library for one function. Copy the 10 lines you need.
Not this:
import "github.com/somelib/utils" // For one function
result := utils.Max(a, b)
This:
// Just write it
func max(a, b int) int {
if a > b {
return a
}
return b
}
Use channels to pass data between goroutines. Don't share state with mutexes unless you must.
Not this:
var mu sync.Mutex
var data map[string]int
func update(key string, val int) {
mu.Lock()
data[key] = val
mu.Unlock()
}
This:
type update struct {
key string
val int
}
func worker(updates <-chan update, data map[string]int) {
for u := range updates {
data[u.key] = u.val
}
}
Concurrency is about structure. Parallelism is about execution. You can have concurrency without parallelism (single core). Design for concurrency; parallelism may follow.
When you call C from Go, you leave Go's safe world. Memory safety, garbage collection, goroutine scheduling—all bets are off. Avoid cgo if possible.
Reflection is powerful but obscure. It makes code harder to understand and slower to execute. Use it only when there's no other way.
Don't build monoliths. Build small pieces that compose.
// Small, focused types
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type Closer interface { Close() error }
// Compose as needed
type ReadWriteCloser interface {
Reader
Writer
Closer
}
Components should be independent. Changing one shouldn't require changing another.
Functions should accept the smallest interface they need and return concrete types.
// Accept interface
func Process(r io.Reader) (*Result, error) {
// Works with files, buffers, network connections...
}
// Return concrete
func NewProcessor() *Processor {
return &Processor{}
}
Before committing code, ask:
Apply these checks:
Use a different skill when:
data-first (kernel coding style, 8-char tabs, specific conventions)optimization (profiling, cache behavior, data-oriented design)clarity (general clarity principles)distributed (statelessness, idempotency, failure handling)composition (Unix philosophy, stdin/stdout composition)Pike is the Go-first skill and for systems code emphasizing small interfaces and composition.
"Simplicity is complicated, but the clarity it provides is worth the effort." — Rob Pike