Go - Design Guidelines
Concept: Go prioritizes code readability and maintainability by reducing code volume
Why it matters: Fewer lines = fewer bugs; every line has a probability of containing an error.
package main
import "fmt"
// fewer lines means fewer places for bugs to hide
func sum(nums []int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() { fmt.Println(sum([]int{1, 2, 3})) }
Gotcha: Adding abstraction to "feel cleaner" increases lines and hides cost.
Concept: Thin layers of abstraction.
Why it matters: Every abstraction layer adds indirection cost; Go keeps layers shallow and explicit.
package main
import "fmt"
// one thin layer: wraps a concern without hiding cost
type Multiplier struct{ factor int }
func (m Multiplier) Apply(n int) int { return n * m.factor }
func main() {
double := Multiplier{factor: 2}
fmt.Println(double.Apply(5)) // cost is obvious — no hidden alloc
}
Gotcha: Wrapping a wrapper to "future-proof" adds hidden cost with zero current benefit.
Concept: Mechanical Sympathy.
Why it matters: Code that mirrors hardware layout (cache lines, memory access) runs faster without algorithmic change.
package main
import "fmt"
// sequential access matches how CPU prefetcher loads cache lines
func sumSlice(data []int64) int64 {
var total int64
for _, v := range data { // stride-1 access — every element in cache line used
total += v
}
return total
}
func main() {
data := make([]int64, 1024)
fmt.Println(sumSlice(data))
}
Gotcha: Using []interface{} instead of []int64 breaks cache-line locality — 8× slower.
Concept: Every programming decision comes with a cost.
Why it matters: Knowing the cost of a decision (allocation, indirection, GC pressure) prevents accidental performance problems.
package main
import "fmt"
// value return: stack allocation, zero GC cost
func newPointValue() [2]int { return [2]int{1, 2} }
// pointer return: heap allocation, GC must track it
func newPointPtr() *[2]int { return &[2]int{1, 2} }
func main() {
v := newPointValue() // cheap — stays on stack
p := newPointPtr() // costs a heap alloc — only use when sharing is needed
fmt.Println(v, p)
}
Gotcha: Reflexively returning pointers "for efficiency" actually adds GC overhead for small values.
Concept: Reading code is as important as writing it.
Why it matters: Code is read 10× more than written; optimizing for the reader reduces bugs and onboarding time.
package main
import "fmt"
// named return makes intent readable without a comment
func divide(a, b float64) (result float64, err error) {
if b == 0 {
err = fmt.Errorf("divide: zero divisor")
return // naked return — only acceptable when function is very short
}
result = a / b
return
}
func main() {
r, err := divide(10, 2)
fmt.Println(r, err)
}
Gotcha: Named returns in long functions cause confusion — the return value is non-obvious at the call site.
Concept: Mental models are essential for debugging and maintaining complex codebases.
Why it matters: A developer without a mental model spends hours re-reading code to understand state; good structure builds the model automatically.
package main
import "fmt"
// consistent type + single responsibility = easy mental model
type Account struct {
id int
balance float64
}
func (a *Account) Deposit(amount float64) { a.balance += amount }
func (a *Account) Balance() float64 { return a.balance }
func main() {
acc := Account{id: 1}
acc.Deposit(100)
fmt.Println(acc.Balance()) // reader instantly knows what state changed
}
Gotcha: Spreading state across multiple global variables destroys the mental model instantly.
Concept: As projects grow beyond 10,000 lines of code, maintaining a shared mental model within a team becomes increasingly important.
Why it matters: Past ~10k lines, no single developer holds the full model — the code structure must carry it.
package main
// package-level doc comment = shared mental model encoded in code
// Package inventory manages stock levels and reservation logic.
// It owns no network I/O; callers supply the persistence layer.
package main
import "fmt"
type Inventory struct{ items map[string]int }
func NewInventory() *Inventory { return &Inventory{items: make(map[string]int)} }
func (inv *Inventory) Add(sku string, qty int) { inv.items[sku] += qty }
func (inv *Inventory) Stock(sku string) int { return inv.items[sku] }
func main() {
inv := NewInventory()
inv.Add("widget", 10)
fmt.Println(inv.Stock("widget"))
}
Gotcha: Skipping package-level doc comments forces every reader to reconstruct the mental model from scratch.
Concept: Debuggers should be used as a last resort.
Why it matters: Relying on a debugger means your mental model failed; writing code you can reason about eliminates most debugging.
package main
import "fmt"
// instrument with fmt/log first — understand state without a debugger
func processOrder(id int, qty int) (total float64, err error) {
fmt.Printf("processOrder: id=%d qty=%d\n", id, qty) // trace the state
if qty <= 0 {
return 0, fmt.Errorf("processOrder: invalid qty %d", qty)
}
total = float64(qty) * 9.99
fmt.Printf("processOrder: total=%.2f\n", total)
return total, nil
}
func main() {
t, err := processOrder(42, 3)
fmt.Println(t, err)
}
Gotcha: Removing trace logs after debugging means you'll add them back the next time — keep structured logging.
Concept: Productivity vs. Performance — Historically, the industry prioritized productivity over performance.
Why it matters: Understanding this history explains why Go was designed differently — it refuses to accept the false tradeoff.
package main
import (
"fmt"
"strings"
)
// Go standard library is both productive AND performant — strings.Builder example
func buildReport(lines []string) string {
var b strings.Builder // productive: easy to use
b.Grow(len(lines) * 40) // performant: single pre-allocation
for _, l := range lines {
b.WriteString(l)
b.WriteByte('\n')
}
return b.String()
}
func main() {
fmt.Print(buildReport([]string{"line1", "line2", "line3"}))
}
Gotcha: Using += on strings in a loop is productive to write but O(n²) to run — use Builder.
Concept: "Fast Enough" Philosophy.
Why it matters: Optimizing beyond requirements wastes engineering time and reduces readability with no user-visible benefit.
package main
import "fmt"
// fast enough: map lookup is O(1) amortized — no need for a custom hash table
func findUser(users map[int]string, id int) (string, bool) {
name, ok := users[id]
return name, ok
}
func main() {
users := map[int]string{1: "Harish", 2: "Alice"}
name, ok := findUser(users, 1)
fmt.Println(name, ok)
}
Gotcha: Replacing a map with a sorted slice + binary search for "performance" before profiling proves the bottleneck.
Concept: Correctness — the primary focus should be on correctness, not performance.
Why it matters: Incorrect fast code is worse than correct slow code — you can't optimize what doesn't work.
package main
import "fmt"
// correct first: boundary conditions handled before any optimization
func safeDivide(a, b int) (int, bool) {
if b == 0 {
return 0, false // correctness: handle the edge case explicitly
}
return a / b, true
}
func main() {
if r, ok := safeDivide(10, 0); ok {
fmt.Println(r)
} else {
fmt.Println("cannot divide")
}
}
Gotcha: Adding a fast-path optimization before writing the correct slow-path creates two bugs to fix later.
Concept: Correctness encompasses code integrity, readability, and simplicity.
Why it matters: These three properties together define whether code can be trusted, understood, and maintained.
package main
import "fmt"
// integrity: pure function — same input always produces same output
// readability: names explain intent without comments
// simplicity: does exactly one thing
func celsiusToFahrenheit(c float64) float64 {
return c*9/5 + 32
}
func main() {
fmt.Println(celsiusToFahrenheit(100)) // 212
fmt.Println(celsiusToFahrenheit(0)) // 32
}
Gotcha: Merging two unrelated concerns into one function destroys all three properties simultaneously.
Concept: Refactoring is an essential part of the development process.
Why it matters: First drafts solve the problem; refactoring makes the solution understandable and maintainable long-term.
package main
import "fmt"
// before refactoring: everything inline, hard to test
func processV1(data []int) int {
total := 0
for _, v := range data {
if v > 0 {
total += v
}
}
return total
}
// after refactoring: extracted predicate — each part independently testable
func isPositive(n int) bool { return n > 0 }
func processV2(data []int) int {
total := 0
for _, v := range data {
if isPositive(v) {
total += v
}
}
return total
}
func main() { fmt.Println(processV2([]int{-1, 2, 3, -4, 5})) }
Gotcha: Refactoring without tests means you can't verify the behavior was preserved.
Concept: "Drafting" Code — iterate and not strive for perfection in the initial stages.
Why it matters: Drafting removes the cognitive block of writing "perfect" code; iteration always produces better results than one-shot perfection.
package main
import "fmt"
// draft 1: just make it work
func filterV1(nums []int, min int) []int {
var out []int
for _, n := range nums {
if n >= min {
out = append(out, n)
}
}
return out
}
// draft 2: generalized after seeing repeated pattern
func filter(nums []int, keep func(int) bool) []int {
var out []int
for _, n := range nums {
if keep(n) {
out = append(out, n)
}
}
return out
}
func main() {
data := []int{1, 5, 2, 8, 3}
fmt.Println(filterV1(data, 3))
fmt.Println(filter(data, func(n int) bool { return n >= 3 }))
}
Gotcha: Never merging drafts back — leaving dead V1 code alongside V2 creates confusion.
Concept: Integrity — code reliability at both micro level (individual read/write operations) and macro level (data transformation accuracy).
Why it matters: A single corrupted read or write cascades into data loss or silent wrong results at the macro level.
package main
import (
"fmt"
"sync"
)
// micro integrity: every write to shared state is protected
type SafeCounter struct {
mu sync.Mutex
value int
}
func (c *SafeCounter) Inc() {
c.mu.Lock()
c.value++ // atomic at the micro level — no torn write
c.mu.Unlock()
}
func (c *SafeCounter) Get() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
c := SafeCounter{}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() { defer wg.Done(); c.Inc() }()
}
wg.Wait()
fmt.Println(c.Get()) // always 100 — macro result is correct
}
Gotcha: Locking on write but not on read still produces a data race — both operations need the lock.
Concept: Writing less code reduces the potential for introducing bugs.
Why it matters: Each line of code has a nonzero probability of containing a bug; the only bug-free code is code that doesn't exist.
package main
import (
"fmt"
"sort"
)
// use the standard library instead of reimplementing — less code, fewer bugs
func topN(nums []int, n int) []int {
sort.Sort(sort.Reverse(sort.IntSlice(nums))) // stdlib does the work
if n > len(nums) {
n = len(nums)
}
return nums[:n]
}
func main() {
fmt.Println(topN([]int{3, 1, 4, 1, 5, 9, 2, 6}, 3)) // [9 6 5]
}
Gotcha: Re-implementing sorting/searching from scratch to "avoid dependencies" creates untested code paths.
Concept: Thorough error handling ensures the software can gracefully handle unexpected situations.
Why it matters: Unhandled errors turn recoverable situations into crashes or silent data corruption.
package main
import (
"errors"
"fmt"
)
var ErrInvalidInput = errors.New("invalid input")
// every error path explicitly handled — no silent failures
func sqrt(n float64) (float64, error) {
if n < 0 {
return 0, fmt.Errorf("sqrt: %w: got %v", ErrInvalidInput, n)
}
// Newton's method (simplified)
x := n
for i := 0; i < 10; i++ {
x = (x + n/x) / 2
}
return x, nil
}
func main() {
if r, err := sqrt(-1); err != nil {
fmt.Println("error:", err)
fmt.Println("is invalid input:", errors.Is(err, ErrInvalidInput))
} else {
fmt.Println(r)
}
}
Gotcha: Using _ = someFunc() discards errors — the function may have partially mutated state.
Concept: Readability — the cost of code operations should be readily apparent (Transparency).
Why it matters: Hidden costs (allocations, locks, I/O) make performance unpredictable and debugging hard.
package main
import "fmt"
// cost is visible at the call site — caller knows what they're paying
func loadUsers() ([]string, error) { // name signals I/O + error possible
return []string{"alice", "bob"}, nil // in production: DB call
}
func main() {
// reader sees: this might fail, this allocates a slice
users, err := loadUsers()
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(users)
}
Gotcha: Naming a DB-calling function getUsers() hides the I/O cost — loadUsers or fetchUsers signals it.
Concept: The average developer on the team should possess a comprehensive mental model of the codebase.
Why it matters: Code only the author understands is a liability — it can't be reviewed, debugged, or extended by the team.
package main
import "fmt"
// self-documenting: variable names and structure carry the model
type OrderStatus int
const (
StatusPending OrderStatus = iota
StatusConfirmed // iota makes the sequence obvious to any reader
StatusShipped
StatusDelivered
)
func (s OrderStatus) String() string {
return [...]string{"pending", "confirmed", "shipped", "delivered"}[s]
}
func main() {
status := StatusConfirmed
fmt.Println(status) // "confirmed" — no magic numbers in the codebase
}
Gotcha: Using raw int constants (0, 1, 2, 3) for status values forces every reader to look up the mapping.
Concept: Simplicity — achieved through refactoring, aims to hide complexity without sacrificing readability.
Why it matters: Simplicity is the result of removing everything that isn't necessary while keeping everything that is.
package main
import (
"fmt"
"strings"
)
// complex version hidden behind a simple interface — reader sees only the contract
type Pipeline struct{ steps []func(string) string }
func (p *Pipeline) Add(step func(string) string) { p.steps = append(p.steps, step) }
func (p *Pipeline) Run(input string) string {
for _, step := range p.steps {
input = step(input)
}
return input
}
func main() {
p := Pipeline{}
p.Add(strings.TrimSpace)
p.Add(strings.ToLower)
p.Add(func(s string) string { return strings.ReplaceAll(s, " ", "_") })
fmt.Println(p.Run(" Hello World ")) // hello_world
}
Gotcha: Hiding complexity in a way that prevents the caller from understanding error conditions is not simplicity — it's opacity.
Concept: External latencies — delays caused by network communication, disk I/O, and system calls (measured in milliseconds).
Why it matters: External latencies dominate wall-clock time in most services — optimizing CPU while ignoring I/O is wasted effort.
package main
import (
"fmt"
"time"
)
// simulate and measure external latency — make it visible
func callExternalService(url string) (string, time.Duration) {
start := time.Now()
time.Sleep(50 * time.Millisecond) // simulates network round-trip
return "response", time.Since(start)
}
func main() {
result, latency := callExternalService("https://api.example.com")
fmt.Printf("result=%s latency=%v\n", result, latency)
// if latency > 100ms, investigate the network path, not the Go code
}
Gotcha: Profiling CPU when the bottleneck is a 100ms database query — pprof shows the wait, not the work.
Concept: Internal latencies — garbage collection, memory allocation, and synchronization mechanisms (measured in microseconds).
Why it matters: Internal latencies accumulate across millions of operations; reducing allocations directly reduces GC pauses.
package main
import (
"fmt"
"runtime"
)
// reusing a buffer eliminates per-call allocation — reduces GC pressure
type Processor struct{ buf []byte }
func NewProcessor() *Processor {
return &Processor{buf: make([]byte, 0, 4096)} // pre-allocate once
}
func (p *Processor) Process(data []byte) []byte {
p.buf = p.buf[:0] // reset length, keep capacity — zero allocation
p.buf = append(p.buf, data...)
return p.buf
}
func main() {
var before, after runtime.MemStats
runtime.ReadMemStats(&before)
proc := NewProcessor()
for i := 0; i < 10000; i++ {
proc.Process([]byte("hello world"))
}
runtime.ReadMemStats(&after)
fmt.Printf("allocs: %d\n", after.Mallocs-before.Mallocs) // near zero
}
Gotcha: Creating a new []byte inside a hot loop causes one allocation per iteration — multiplied by millions of requests.
Concept: Data access patterns significantly influence performance — optimizing data storage and retrieval strategies.
Why it matters: The same algorithm runs 10× faster with cache-friendly data layout versus pointer-chasing structures.
package main
import "fmt"
// struct of arrays (cache-friendly) vs array of structs (cache-unfriendly) for hot fields
// cache-friendly: access all X values sequentially — fits in cache lines
type ParticlesSOA struct {
x []float64 // all X in one contiguous block
y []float64
}
// cache-unfriendly for X-only loops: X and Y interleaved — Y wastes cache space
type ParticleAOS struct {
x, y float64
}
func sumX_SOA(p ParticlesSOA) float64 {
total := 0.0
for _, x := range p.x { // stride-1, all cache hits
total += x
}
return total
}
func main() {
p := ParticlesSOA{
x: []float64{1, 2, 3, 4},
y: []float64{5, 6, 7, 8},
}
fmt.Println(sumX_SOA(p))
}
Gotcha: Adding rarely-used fields to a hot struct pollutes cache lines even when those fields aren't accessed.
Concept: Algorithm efficiency plays a role particularly within tight loops — readability should not be sacrificed for marginal algorithmic gains.
Why it matters: A 5% algorithmic gain that makes the code unreadable creates maintenance debt worth more than the gain.
package main
import "fmt"
// O(n) linear scan is "fast enough" for small n — readable wins over micro-optimized
func contains(haystack []string, needle string) bool {
for _, s := range haystack { // clear, correct, fast enough for n < 1000
if s == needle {
return true
}
}
return false
}
func main() {
fruits := []string{"apple", "banana", "cherry"}
fmt.Println(contains(fruits, "banana")) // true
fmt.Println(contains(fruits, "mango")) // false
}
Gotcha: Replacing a readable linear scan with a binary search (requiring a sorted slice) before profiling proves it matters.