Demystifying: "Do not communicate by sharing memory; instead, share memory by communicating." Golang idiom.
Golang's approach to effectively addressing locking and race condition
We are going to understand the statement with a simple example that contrasts two approaches to concurrency.
The statement is about the preferred way to handle concurrency in Go.
“Do not communicate by sharing memory” -> This is the traditional way where multiple threads/goroutines access the same shared memory and use locks to protect access. This is error-prone and can lead to race conditions and deadlocks.
“Instead, share memory by communicating” -> This is the Go way. Instead of having multiple goroutines accessing the same memory, we use channels to pass data between goroutines. Only one goroutine has access to the data at any given time. This is inspired by the Communicating Sequential Processes (CSP) model.
Let’s see an example of both.
Example: We want to increment a counter from multiple goroutines.
Approach 1: Sharing memory (with a mutex) - This is what we are advised against.
Approach 2: Using channels (sharing by communicating) - This is the Go way.
We’ll write two programs that increment a counter 1000 times using 10 goroutines.
Simple Explanation: Don’t Share, Pass Messages Instead
Think of it like talking vs. fighting over a toy:
Example 1: The Wrong Way (Sharing Memory)
package main
import (
"fmt"
"time"
)
// BAD: Two kids fighting over the same toy
func main() {
toy := "Teddy Bear" // One toy in the middle
// Kid 1 wants the toy
go func() {
for i := 0; i < 3; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println("Kid 1 playing with:", toy) // Direct access
}
}()
// Kid 2 also wants the toy
go func() {
for i := 0; i < 3; i++ {
time.Sleep(150 * time.Millisecond)
fmt.Println("Kid 2 playing with:", toy) // Direct access
}
}()
time.Sleep(1 * time.Second)
}Problem: Both goroutines are grabbing the same toy variable. This can cause:
Race conditions (they might play at the same time)
Unexpected behavior
Need for locks (making it complicated)
Example 2: The Right Way (Communicating)
package main
import (
"fmt"
)
// GOOD: One toy, passed between kids properly
func main() {
// Channel acts as a "toy passing system"
toyChannel := make(chan string)
// Kid 1: Waits for toy, plays, then passes it back
go func() {
for i := 0; i < 3; i++ {
toy := <-toyChannel // Wait to receive toy
fmt.Println("Kid 1 playing with:", toy)
toyChannel <- toy // Pass toy back
}
}()
// Kid 2: Same behavior
go func() {
for i := 0; i < 3; i++ {
toy := <-toyChannel
fmt.Println("Kid 2 playing with:", toy)
toyChannel <- toy
}
}()
// Start by putting toy in the channel
toyChannel <- "Teddy Bear"
// Let them play
time.Sleep(1 * time.Second)
}Even Simpler: Counter Example
Bad: Everyone touches the counter
var counter = 0 // Shared memory everyone touches
func main() {
// 5 people trying to increment same counter
for i := 0; i < 5; i++ {
go func() {
counter++ // DANGER: Race condition!
}()
}
}Good: One counter owner, others send requests
func main() {
// Channel for increment requests
increment := make(chan int)
// ONE counter owner
go func() {
counter := 0 // Private, not shared
for amount := range increment {
counter += amount
fmt.Println("Counter:", counter)
}
}()
// Send requests through channel
for i := 0; i < 5; i++ {
increment <- 1 // "Please add 1 to counter"
}
time.Sleep(100 * time.Millisecond)
}Real-Life Analogy
Shared Memory Approach (Bad)
Imagine a shared whiteboard in an office:
Everyone writes on it simultaneously
People erase each other’s work
Need rules like “Raise hand before writing”
Chaos ensues
Communicating Approach (Good)
Imagine a whiteboard with one marker:
Only one person has the marker at a time
Others request the marker when they need it
Clear who has control
No conflicts
Complete Simple Program
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("=== SHARING MEMORY (BAD) ===")
badWay()
time.Sleep(100 * time.Millisecond)
fmt.Println("\n=== COMMUNICATING (GOOD) ===")
goodWay()
}
func badWay() {
sharedNumber := 0
// Two goroutines fighting over sharedNumber
go func() {
for i := 0; i < 3; i++ {
time.Sleep(50 * time.Millisecond)
sharedNumber++ // Direct access - dangerous!
fmt.Println("Goroutine 1 set to:", sharedNumber)
}
}()
go func() {
for i := 0; i < 3; i++ {
time.Sleep(75 * time.Millisecond)
sharedNumber = sharedNumber + 2 // Direct access - dangerous!
fmt.Println("Goroutine 2 set to:", sharedNumber)
}
}()
time.Sleep(300 * time.Millisecond)
fmt.Println("Final (unpredictable):", sharedNumber)
}
func goodWay() {
// Channel for sending number updates
updates := make(chan int)
// ONE goroutine owns the number
go func() {
privateNumber := 0 // Private, safe
for change := range updates {
privateNumber += change
fmt.Println("Number is now:", privateNumber)
}
}()
// Other goroutines send requests
go func() {
for i := 0; i < 3; i++ {
time.Sleep(50 * time.Millisecond)
updates <- 1 // "Add 1 please"
}
}()
go func() {
for i := 0; i < 3; i++ {
time.Sleep(75 * time.Millisecond)
updates <- 2 // "Add 2 please"
}
}()
time.Sleep(300 * time.Millisecond)
close(updates)
}Bottomline:
Old way (C#/Java style):
“Here’s a shared variable, everyone can use it”
“Be careful! Use locks when you touch it”
“Hope no one forgets the locks!”
Go way:
“Here’s a message box (channel)”
“If you want to change the data, send a message”
“One dedicated worker will handle all messages”
“No fighting, no locks needed”
If you enjoyed this deep dive…
I write weekly about:
Go performance and runtime behavior
Kubernetes-native service design
Expert-level engineering lessons from real systems
Subscribe if you want more posts like this.


