Go Idioms

Don't Communicate by Sharing Memory; Share Memory by Communicating

This idiom means you should avoid using mutexes and shared variables to coordinate goroutines; instead, pass data directly between goroutines using channels.

This idiom means you should avoid using mutexes and shared variables to coordinate goroutines; instead, pass data directly between goroutines using channels. By treating channels as the primary synchronization mechanism, you eliminate race conditions and make concurrency logic explicit and easier to reason about.

The core principle is that if two goroutines need to access the same data, they shouldn't lock a variable and read/write it. One goroutine should send the data through a channel, and the other should receive it. This ensures data ownership is transferred safely without the complexity of locking hierarchies.

Here is a practical example comparing the "wrong" way (shared memory with mutexes) to the "right" way (communicating via channels):

// ❌ Anti-pattern: Shared memory with mutexes
// Hard to scale, prone to deadlocks, and logic is scattered.
type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

// ✅ Idiom: Share memory by communicating
// Data flows explicitly; no locks needed.
func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        // Process data locally, then send result
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // Start workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Send jobs
    for j := 1; j <= 10; j++ {
        jobs <- j
    }
    close(jobs)

    // Collect results
    for a := 1; a <= 10; a++ {
        <-results
    }
}

In the channel-based approach, the worker function owns the data it processes. It receives a job, computes the result, and sends it back. No other goroutine touches the internal state of the worker, so no mutex is required. This pattern scales naturally: you can add more workers or change the processing logic without worrying about lock contention.

When you must share a complex data structure, pass a pointer to that structure through a channel rather than accessing it directly from multiple places. This effectively serializes access to the data.

// Passing ownership via channel
type Config struct {
    Setting string
}

func updateConfig(cfg *Config, newSetting string) {
    cfg.Setting = newSetting
}

func main() {
    config := &Config{Setting: "default"}
    updates := make(chan func(*Config))

    go func() {
        for update := range updates {
            update(config) // Only one goroutine touches config at a time
        }
    }()

    updates <- func(c *Config) { c.Setting = "new" }
    close(updates)
}

Use channels for synchronization and data transfer. Only use mutexes when you absolutely cannot avoid shared mutable state, such as in low-level cache implementations or when interfacing with C libraries. For 95% of Go concurrency, channels provide a safer, more readable design.