Chapter 5: Concurrency in Go

Goroutines in Go

Overview and Basics

Goroutines are a key feature of Go’s concurrency model. They are functions or methods that can be executed concurrently with other goroutines within the same address space. Goroutines are lightweight compared to threads managed by the operating system, allowing Go programs to efficiently utilize available resources without the overhead of traditional threading mechanisms.

Example 1: Basic Goroutine

go

package main

import (
"fmt"
"time"
)

// Function that executes as a goroutine
func printNumbers() {
for i := 1; i <= 5; i++ {
fmt.Printf("%d ", i)
time.Sleep(time.Second) // Simulate some work with sleep
}
}

func main() {
// Start a goroutine using the `go` keyword
go printNumbers()

// Main function continues to execute concurrently with `printNumbers`
for i := 1; i <= 5; i++ {
fmt.Printf("Main: %d ", i)
time.Sleep(time.Second)
}
}

In this example:

  • We define a function printNumbers() that prints numbers from 1 to 5 with a sleep between each print to simulate some work.
  • In the main() function, we start printNumbers() as a goroutine using the go keyword. This initiates the execution of printNumbers() concurrently with the main function.
  • The main() function continues to execute concurrently with printNumbers(), allowing both to run concurrently and print their respective outputs.

Example 2: Concurrent Execution

go

package main

import (
"fmt"
"sync"
)

// Function to print a message multiple times
func printMessage(message string, wg *sync.WaitGroup) {
defer wg.Done() // Signal the WaitGroup that this goroutine is done

for i := 1; i <= 3; i++ {
fmt.Println(message)
}
}

func main() {
var wg sync.WaitGroup

// Add 2 to the WaitGroup counter for two goroutines
wg.Add(2)

// Start two goroutines to print messages concurrently
go printMessage("Hello", &wg)
go printMessage("Go", &wg)

// Wait until all goroutines are done
wg.Wait()
}

In this example:

  • We define a function printMessage() that prints a given message multiple times.
  • In the main() function, we use a sync.WaitGroup to synchronize the completion of goroutines. We add 2 to the WaitGroup counter before starting two goroutines with go printMessage(...).
  • Each goroutine executes concurrently, printing their respective messages (“Hello” and “Go”) three times.
  • main() waits for all goroutines to complete using wg.Wait(), ensuring that it doesn’t exit before all goroutines are finished.

Example 3: Channels for Communication

go

package main

import (
"fmt"
)

// Function to send numbers to a channel
func sendNumbers(ch chan<- int) {
for i := 1; i <= 5; i++ {
ch <- i // Send `i` to channel `ch`
}
close(ch) // Close the channel to signal no more values will be sent
}

// Function to receive numbers from a channel
func receiveNumbers(ch <-chan int, done chan<- bool) {
for num := range ch {
fmt.Printf("Received: %d\n", num)
}
done <- true // Signal done on the `done` channel
}

func main() {
// Create an unbuffered channel for int values
ch := make(chan int)
done := make(chan bool)

// Start a goroutine to send numbers to the channel
go sendNumbers(ch)

// Start another goroutine to receive numbers from the channel
go receiveNumbers(ch, done)

// Wait for the receive goroutine to complete
<-done
}

In this example:

  • We define sendNumbers() and receiveNumbers() functions to send numbers to and receive numbers from a channel, respectively.
  • In main(), we create an unbuffered channel ch for communication between goroutines.
  • We start a goroutine sendNumbers(ch) to send numbers 1 to 5 to the channel ch.
  • Another goroutine receiveNumbers(ch, done) is started to receive numbers from ch and print them.
  • The main() function waits for the receiveNumbers goroutine to complete by receiving a value from the done channel.

Goroutines are a powerful feature of Go, enabling concurrent execution in a straightforward manner. They allow Go programs to efficiently utilize available resources and handle concurrent tasks such as I/O operations, parallel processing, and concurrent data access.

Channels in Go

Overview and Basics

Channels are typed conduits through which Go routines communicate by sending and receiving values of a specified element type. They provide a way for goroutines to synchronize execution and exchange data in a safe and controlled manner. Channels are designed to facilitate communication and coordination between goroutines, making concurrent programming easier and less error-prone.

Example 1: Unbuffered Channels

go

package main

import (
"fmt"
)

func main() {
// Create an unbuffered channel of integers
ch := make(chan int)

// Goroutine to send data to the channel
go func() {
ch <- 42 // Send the value 42 to the channel
}()

// Receive data from the channel (blocks until data is available)
value := <-ch
fmt.Println("Received:", value)
}

In this example:

  • We create an unbuffered channel ch of type int using make(chan int).
  • Inside a goroutine, we send the integer value 42 to the channel ch using the <- operator.
  • In the main goroutine, we receive the value from the channel ch using the <- operator and store it in the variable value.
  • The program will output Received: 42, demonstrating how data is exchanged between goroutines via a channel.

Example 2: Buffered Channels

go

package main

import (
"fmt"
)

func main() {
// Create a buffered channel of strings with capacity 3
ch := make(chan string, 3)

// Goroutine to send data to the buffered channel
go func() {
ch <- "one"
ch <- "two"
ch <- "three"
close(ch) // Close the channel after sending all values
}()

// Iterate over the channel and receive all values
for msg := range ch {
fmt.Println("Received:", msg)
}
}

In this example:

  • We create a buffered channel ch of type string with a capacity of 3 using make(chan string, 3).
  • Inside a goroutine, we send three string values (“one”, “two”, “three”) to the buffered channel ch using the <- operator.
  • We then close the channel ch after sending all values to signal that no more values will be sent.
  • In the main goroutine, we use a for loop with range to iterate over the channel ch and receive all values until the channel is closed.
  • The program will output:makefile
  • Received: one Received: two Received: three

Example 3: Select Statement with Channels

go

package main

import (
"fmt"
"time"
)

func main() {
// Create two channels
ch1 := make(chan string)
ch2 := make(chan string)

// Goroutine to send data to ch1 after 2 seconds
go func() {
time.Sleep(2 * time.Second)
ch1 <- "Channel 1"
}()

// Goroutine to send data to ch2 after 1 second
go func() {
time.Sleep(1 * time.Second)
ch2 <- "Channel 2"
}()

// Use select statement to receive from whichever channel is ready first
select {
case msg1 := <-ch1:
fmt.Println("Received from ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("Received from ch2:", msg2)
}
}

In this example:

  • We create two channels ch1 and ch2.
  • Two goroutines are started with go func() to send values to ch1 and ch2 after 2 seconds and 1 second, respectively.
  • We use a select statement to receive data from whichever channel (ch1 or ch2) is ready first.
  • The program will output either “Received from ch1: Channel 1” or “Received from ch2: Channel 2”, depending on which goroutine sends its value first.

Example 4: Closing Channels and Range

go

package main

import (
"fmt"
)

func main() {
// Create a buffered channel of integers with capacity 3
ch := make(chan int, 3)

// Goroutine to send data to the buffered channel
go func() {
for i := 1; i <= 3; i++ {
ch <- i // Send values 1, 2, 3 to the channel
}
close(ch) // Close the channel after sending all values
}()

// Iterate over the channel and receive all values until closed
for num := range ch {
fmt.Println("Received:", num)
}
}

In this example:

  • We create a buffered channel ch of type int with a capacity of 3 using make(chan int, 3).
  • Inside a goroutine, we send integer values 1, 2, and 3 to the buffered channel ch using the <- operator in a loop.
  • After sending all values, we close the channel ch using the close(ch) statement to indicate no more values will be sent.
  • In the main goroutine, we use a for ... range loop to iterate over the channel ch and receive all values until the channel is closed.
  • The program will output:makefile
  • Received: 1 Received: 2 Received: 3

Example 5: Bidirectional Channels

go

package main

import (
"fmt"
)

func sendAndReceive(ch chan string) {
ch <- "Hello" // Send a value to the channel
fmt.Println(<-ch) // Receive a value from the channel and print it
}

func main() {
// Create a bidirectional channel of strings
ch := make(chan string)

// Start a goroutine to send and receive data through the channel
go sendAndReceive(ch)

// Block and wait for the goroutine to finish
<-ch
}

In this example:

  • We create a bidirectional channel ch of type string.
  • The sendAndReceive() function sends the string “Hello” to the channel ch using <- operator and then immediately receives and prints the value from the channel using <-ch.
  • In the main() function, we start a goroutine to execute sendAndReceive(ch), which performs both send and receive operations on the channel ch.
  • The program blocks on <-ch in main() until the sendAndReceive() goroutine completes.

Channels in Go are powerful constructs for managing concurrent communication between goroutines, ensuring safe data sharing and synchronization without explicit locking mechanisms. They facilitate effective concurrency patterns and enable scalable and efficient concurrent programming in Go applications.

Select Statement in Go

Overview and Basics

The select statement in Go is used to choose which of multiple possible send or receive operations will proceed. It waits for multiple channels to be ready to proceed with their respective operations. If multiple channels are ready, it chooses one randomly. The select statement is essential for building concurrent and non-blocking operations in Go programs.

Example 1: Basic Select Statement

go

package main

import (
"fmt"
"time"
)

func main() {
ch1 := make(chan string)
ch2 := make(chan string)

go func() {
time.Sleep(2 * time.Second)
ch1 <- "Message from ch1"
}()

go func() {
time.Sleep(1 * time.Second)
ch2 <- "Message from ch2"
}()

select {
case msg1 := <-ch1:
fmt.Println("Received from ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("Received from ch2:", msg2)
}
}

Explanation:

  • We create two channels ch1 and ch2 of type string.
  • Two goroutines are started with go func() to send values to ch1 and ch2 after 2 seconds and 1 second, respectively.
  • The select statement waits for either ch1 or ch2 to be ready to receive a value. It will proceed with the case that is ready first.
  • In this example, ch2 is ready first because it receives a value after 1 second compared to ch1 after 2 seconds.
  • The program will output: Received from ch2: Message from ch2.

Example 2: Select with Default Case

go

package main

import (
"fmt"
"time"
)

func main() {
ch := make(chan string)

go func() {
time.Sleep(2 * time.Second)
ch <- "Message"
}()

select {
case msg := <-ch:
fmt.Println("Received:", msg)
default:
fmt.Println("No message received")
}
}

Explanation:

  • We create a channel ch of type string.
  • A goroutine is started with go func() to send a value to ch after 2 seconds.
  • The select statement waits for a receive operation on ch. If ch is not ready within a certain time frame, it proceeds to the default case.
  • In this example, since ch sends a value after 2 seconds, the select statement receives the value and prints "Received: Message".
  • If the send operation to ch took longer than expected, the default case would execute, printing "No message received".

Example 3: Select with Timeout

go

package main

import (
"fmt"
"time"
)

func main() {
ch := make(chan string)

go func() {
time.Sleep(2 * time.Second)
ch <- "Message"
}()

select {
case msg := <-ch:
fmt.Println("Received:", msg)
case <-time.After(1 * time.Second):
fmt.Println("Timeout occurred")
}
}

Explanation:

  • We create a channel ch of type string.
  • A goroutine is started with go func() to send a value to ch after 2 seconds.
  • The select statement waits for a receive operation on ch. If the receive operation on ch does not complete within 1 second, the time.After channel will become ready, and the select statement will proceed with the second case.
  • In this example, since the receive operation on ch takes 2 seconds to receive a value, the select statement will print "Timeout occurred" after 1 second.

Example 4: Select with Multiple Cases

go

package main

import (
"fmt"
"time"
)

func main() {
ch1 := make(chan string)
ch2 := make(chan string)

go func() {
time.Sleep(2 * time.Second)
ch1 <- "Message from ch1"
}()

go func() {
time.Sleep(1 * time.Second)
ch2 <- "Message from ch2"
}()

select {
case msg1 := <-ch1:
fmt.Println("Received from ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("Received from ch2:", msg2)
case <-time.After(3 * time.Second):
fmt.Println("Timeout occurred")
}
}

Explanation:

  • Similar to the first example, we create two channels ch1 and ch2 of type string.
  • Two goroutines are started with go func() to send values to ch1 and ch2 after 2 seconds and 1 second, respectively.
  • In this example, we add a third case using time.After(3 * time.Second). This case becomes ready after 3 seconds if neither ch1 nor ch2 is ready to receive a value.
  • The select statement will choose the first case that is ready. If ch1 or ch2 receives a value within 3 seconds, it prints the received message. Otherwise, it prints "Timeout occurred".

Example 5: Non-Blocking Send and Receive with select

go

package main

import (
"fmt"
)

func main() {
ch := make(chan string, 1)

select {
case ch <- "Message":
fmt.Println("Sent:", "Message")
default:
fmt.Println("Channel is full, cannot send")
}

select {
case msg := <-ch:
fmt.Println("Received:", msg)
default:
fmt.Println("No message received")
}
}

Explanation:

  • We create a buffered channel ch of type string with capacity 1 using make(chan string, 1).
  • In the first select block, we attempt to send the string "Message" to ch. Since ch is buffered and has space, the send operation succeeds, and it prints "Sent: Message".
  • In the second select block, we attempt to receive a value from ch. Since ch already has a value ("Message"), the receive operation succeeds, and it prints "Received: Message".
  • If ch was empty in the second select block, the default case would have executed, printing "No message received".

Conclusion

The select statement in Go is a powerful mechanism for implementing concurrent operations and managing multiple channels effectively. It enables non-blocking operations, timeout handling, and multiplexing of communication operations, making it essential for building robust and efficient concurrent applications in Go. Understanding how to use select allows developers to leverage Go’s concurrency model to handle complex communication and synchronization scenarios gracefully.

Mutexes (Mutual Exclusion)

Overview and Basics

Mutexes (short for mutual exclusion) in Go provide a way to synchronize access to shared resources by ensuring that only one goroutine can access the resource at any given time. They prevent data races and ensure safe concurrent access.

Example 1: Using Mutexes for Synchronization

go

package main

import (
"fmt"
"sync"
"time"
)

var counter int
var mutex sync.Mutex

func increment() {
mutex.Lock()
counter++
mutex.Unlock()
}

func main() {
for i := 0; i < 10; i++ {
go increment()
}

time.Sleep(1 * time.Second)
fmt.Println("Final counter:", counter)
}

Explanation:

  • We import "sync" package to use Mutex.
  • We define a global variable counter to be incremented and mutex of type sync.Mutex to synchronize access to counter.
  • The increment function locks the mutex with mutex.Lock(), increments the counter, and then unlocks the mutex with mutex.Unlock(), ensuring that only one goroutine can modify counter at a time.
  • In the main function, we start 10 goroutines to concurrently execute increment.
  • After a delay (time.Sleep(1 * time.Second)), we print the final value of counter. Due to the mutual exclusion provided by mutex, the final value of counter will be 10, demonstrating safe concurrent access.

WaitGroups

Overview and Basics

Wait groups in Go provide a way to wait for a collection of goroutines to finish executing before proceeding. They are useful for coordinating the execution of multiple goroutines and waiting for them to complete.

Example 2: Using WaitGroups for Coordination

go

package main

import (
"fmt"
"sync"
"time"
)

func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Decrements the wait group counter when function exits
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}

func main() {
var wg sync.WaitGroup

for i := 1; i <= 5; i++ {
wg.Add(1) // Increment the wait group counter
go worker(i, &wg)
}

wg.Wait() // Waits until the wait group counter goes to zero
fmt.Println("All workers done")
}

Explanation:

  • We import "sync" package to use WaitGroup.
  • We define the worker function that takes an id and a pointer to sync.WaitGroup (wg). It defers wg.Done() to decrement the wait group counter when the function exits.
  • In the main function, we create a sync.WaitGroup variable wg.
  • We start 5 goroutines (worker function calls) using a loop. For each goroutine, we increment the wait group counter using wg.Add(1).
  • wg.Wait() blocks until the wait group counter returns to zero, meaning all goroutines have called wg.Done(), indicating they have finished.
  • After all workers are done, "All workers done" is printed.

Conclusion

Mutexes and wait groups are essential synchronization primitives in Go for managing shared resources and coordinating concurrent tasks. Understanding their usage allows developers to write safe and efficient concurrent programs, leveraging Go’s powerful concurrency features effectively. By using mutexes for exclusive access and wait groups for coordination, developers can ensure thread-safe access to shared data and orderly execution of concurrent tasks in Go applications.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *