Don't Communicate by Sharing Memory, Share Memory by Communicating: A Go Approach

ยท 486 words ยท 3 minute read

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 using make(chan int).
  • The Writer sends the value 42 to the channel using dataChan <- 42.
  • The Reader receives the value from the channel using value := <-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 , Facebook , Telegram and GitHub .

Share: