A.3 Common Patterns and Idioms

This section summarizes the most common Go idioms and patterns that every Go developer should know. These patterns represent idiomatic Go code.

Go Idioms

1. Error Handling

// ✅ IDIOMATIC: Check errors immediately
result, err := doSomething()
if err != nil {
    return err
}

// ✅ IDIOMATIC: Early return
if err != nil {
    return fmt.Errorf("context: %w", err)
}

Why this is idiomatic

2. Defer for Cleanup

// ✅ IDIOMATIC: Always use defer for cleanup
file, err := os.Open("file.txt")
if err != nil {
    return err
}
defer file.Close()  // Always executed

Why defer is idiomatic

3. Interface Satisfaction

// ✅ IDIOMATIC: Interfaces are satisfied implicitly
type Writer interface {
    Write([]byte) (int, error)
}

type MyWriter struct{}

func (w *MyWriter) Write(data []byte) (int, error) {
    // MyWriter implements Writer automatically
    return len(data), nil
}

Why implicit satisfaction is idiomatic

4. Zero Values

// ✅ IDIOMATIC: Use zero values
var buffer bytes.Buffer  // Ready to use
var mu sync.Mutex        // Ready to use
var wg sync.WaitGroup    // Ready to use

Why zero values are idiomatic

5. Short Variable Declarations

// ✅ IDIOMATIC: Use := for local variables
x := 42
name := "Alice"

// ✅ IDIOMATIC: Multiple assignments
x, y := 10, 20

Why := is idiomatic

6. Named Returns

// ✅ IDIOMATIC: Named returns for clarity
func divide(a, b float64) (result float64, err error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    result = a / b
    return  // Naked return
}

Why named returns are idiomatic

Common Patterns

1. Table-Driven Tests

// ✅ PATTERN: Table-driven tests
func TestAdd(t *testing.T) {
    tests := []struct {
        name string
        a, b int
        want int
    }{
        {"positive", 2, 3, 5},
        {"negative", -2, -3, -5},
        {"zero", 0, 0, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
            }
        })
    }
}

Why table-driven tests

2. Options Pattern

// ✅ PATTERN: Options for configuration
type Server struct {
    host string
    port int
}

type Option func(*Server)

func WithHost(host string) Option {
    return func(s *Server) {
        s.host = host
    }
}

func NewServer(opts ...Option) *Server {
    s := &Server{host: "localhost", port: 8080}
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// Usage
server := NewServer(WithHost("example.com"), WithPort(9000))

Why options pattern

3. Functional Options

// ✅ PATTERN: Functional options (similar to options pattern)
type Config struct {
    timeout time.Duration
    retries int
}

func WithTimeout(d time.Duration) func(*Config) {
    return func(c *Config) {
        c.timeout = d
    }
}

func NewConfig(opts ...func(*Config)) *Config {
    c := &Config{timeout: 5 * time.Second, retries: 3}
    for _, opt := range opts {
        opt(c)
    }
    return c
}

4. Worker Pool

// ✅ PATTERN: Worker pool
func processWithWorkers(jobs []Job, numWorkers int) {
    jobChan := make(chan Job, len(jobs))
    var wg sync.WaitGroup

    // Start workers
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobChan {
                processJob(job)
            }
        }()
    }

    // Send jobs
    for _, job := range jobs {
        jobChan <- job
    }
    close(jobChan)

    wg.Wait()
}

Why worker pools

5. Context Pattern

// ✅ PATTERN: Context for cancellation
func processWithContext(ctx context.Context, items []Item) error {
    for _, item := range items {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            if err := processItem(ctx, item); err != nil {
                return err
            }
        }
    }
    return nil
}

Why context pattern

6. Error Wrapping

// ✅ PATTERN: Wrap errors with context
func processFile(filename string) error {
    data, err := ioutil.ReadFile(filename)
    if err != nil {
        return fmt.Errorf("reading file %s: %w", filename, err)
    }

    if err := validate(data); err != nil {
        return fmt.Errorf("validating %s: %w", filename, err)
    }

    return nil
}

Why error wrapping

7. Interface Segregation

// ✅ PATTERN: Small, focused interfaces
type Reader interface {
    Read([]byte) (int, error)
}

type Writer interface {
    Write([]byte) (int, error)
}

// Not: ReadWriter interface with both methods
// Better: Compose when needed
type ReadWriter interface {
    Reader
    Writer
}

Why small interfaces

Best Practices Summary

1. Error Handling

2. Concurrency

3. Code Organization

4. Performance

5. Testing

6. Documentation

7. Naming

8. Memory Management

These idioms and patterns represent the collective wisdom of the Go community. Following them makes your code more readable, maintainable, and idiomatic.