Don't Communicate by Sharing Memory, Share Memory by Communicating: A Go Approach
This concept, a core principle in Go’s concurrency model, can be confusing at first glance. Let’s break it down with practical examples in Go, highlighting the difference between unsafe memory sharing and safe communication via channels.
The Problem with Shared Memory ๐
Imagine two goroutines (lightweight threads) in your Go program, Reader
and Writer
, that need to access the same data:
var data int // Shared variable (unsafe)
func Reader() {
fmt.Println("Reader:", data)
}
func Writer() {
data = 42 // Modifying shared data
}
func main() {
go Reader()
go Writer()
time.Sleep(1 * time.Second) // Wait for goroutines to finish
}
This code seems straightforward, but it has a major flaw. Since both Reader
and Writer
access the same data
variable directly, there’s no guarantee of data consistency. The output can be unpredictable depending on the execution order of the goroutines. This is a classic example of a race condition.
The Power of Channels ๐
Go promotes safe communication via channels, which act as a message-passing mechanism between goroutines. Here’s an improved version using a channel:
var dataChan = make(chan int)
func Reader() {
value := <-dataChan // Receive data from channel
fmt.Println("Reader:", value)
}
func Writer() {
dataChan <- 42 // Send data to channel
}
func main() {
go Reader()
go Writer()
time.Sleep(1 * time.Second)
}
In this version:
- We create a channel
dataChan
usingmake(chan int)
. - The
Writer
sends the value 42 to the channel usingdataChan <- 42
. - The
Reader
receives the value from the channel usingvalue := <-dataChan
.
Channels provide a safe and synchronized way for goroutines to communicate. Only one goroutine can access the channel at a time, preventing race conditions.
Key Points:
- Don’t communicate by sharing memory: Avoid directly modifying shared variables between goroutines.
- Share memory by communicating: Use channels to safely pass data between goroutines.
- Channels guarantee synchronization: Each goroutine interacts with the channel independently, ensuring data consistency.
Bonus Example ๐
Let’s see how channels can be used for more complex scenarios, like calculating the factorial of a number:
func Factorial(n int, result chan int) {
if n == 0 {
result <- 1
return
}
temp := n * Factorial(n-1, result)
result <- temp
}
func main() {
resultChan := make(chan int)
go Factorial(5, resultChan)
factorial := <-resultChan
fmt.Println("Factorial of 5:", factorial)
}
Here, the Factorial
function uses a channel to send the final result back to the main
function, demonstrating how channels can be used for more elaborate communication patterns in Go concurrency.
By adopting the “share memory by communicating” approach, you can write safe, concurrent Go applications while leveraging the performance benefits of goroutines.
I hope this post helps you. If you know a person who can benefit from this information, send them a link of this post. If you want to get notified about new posts, follow me on YouTube , Twitter (x) , LinkedIn , and GitHub .