Concurrency vs Parallelism
Before we dive into Golang’s concurrency model, it’s essential to distinguish between concurrency and parallelism. Concurrency is a property of systems that allows multiple tasks to be executed at the same time, but not necessarily simultaneously. Parallelism, on the other hand, is the simultaneous execution of multiple tasks.
Goroutines
Goroutines are lightweight threads managed by the Go runtime. They are the building blocks of concurrent programming in Go. To spawn a new goroutine, simply use the
go
keyword followed by a function invocation:Code :
package main import ( "fmt" "time" ) func printMessage(message string) { for i := 0; i < 5; i++ { fmt.Println(message) time.Sleep(1 * time.Second) } } func main() { go printMessage("Hello from goroutine") // Spawn a new goroutine printMessage("Hello from main function") // Run the same function in the main thread }
Output :
Hello from main function Hello from goroutine Hello from main function Hello from goroutine Hello from goroutine Hello from main function Hello from main function Hello from goroutine Hello from goroutine Hello from main function
In the example above, we create a new goroutine using the
go
keyword. The printMessage
function is executed concurrently in both the main function and the spawned goroutine.Channels
Channels are the primary means of communication between goroutines. They allow you to send and receive values between concurrently executing goroutines. Channels are strongly typed and can be created using the
make
function:
ch := make(chan int) // Creates a new channel of type int
Here’s an example demonstrating how to use channels for communication between goroutines:
Code :
package main import ( "fmt" "time" ) func sender(ch chan int) { for i := 0; i < 5; i++ { ch <- i // Send the value 'i' on the channel fmt.Printf("Sent: %d\n", i) time.Sleep(1 * time.Second) } close(ch) // Close the channel to signal the receiver that no more data will be sent } func receiver(ch chan int) { for { value, ok := <-ch // Receive a value from the channel if !ok { fmt.Println("Channel closed") break } fmt.Printf("Received: %d\n", value) } } func main() { ch := make(chan int) go sender(ch) // Spawn a goroutine to send values on the channel go receiver(ch) // Spawn a goroutine to receive values from the channel time.Sleep(7 * time.Second) // Sleep to give the goroutines time to finish execution }
Output :
Sent: 0 Received: 0 Sent: 1 Received: 1 Sent: 2 Received: 2 Sent: 3 Received: 3 Sent: 4 Received: 4 Channel closed
In this example, the
sender
goroutine sends integer values on the channel, while the receiver
goroutine receives and prints the values.Buffered and Unbuffered Channels
Channels in Go can be either buffered or unbuffered. An unbuffered channel has no capacity to store values, meaning that a send operation will block until a corresponding receive operation is ready to accept the value. Similarly, a receive operation will block until a value is available to be received.
A buffered channel, on the other hand, can store a fixed number of values without blocking. Send operations on a buffered channel will only block if the channel’s buffer is full, and receive operations will only block if the buffer is empty.
To create a buffered channel, you simply provide the buffer size as a second argument to the
make
function:bufferedCh := make(chan int, 10) // Create a buffered channel with a capacity of 10
Here’s an example illustrating the differences between buffered and unbuffered channels:
Code :
package main import ( "fmt" "time" ) func sender(ch chan int) { for i := 1; i <= 5; i++ { ch <- i fmt.Printf("Sent: %d\n", i) } close(ch) } func main() { unbufferedCh := make(chan int) bufferedCh := make(chan int, 5) fmt.Println("\nUnbuffered channel started") go sender(unbufferedCh) time.Sleep(1 * time.Second) fmt.Println("Unbuffered channel values:") for value := range unbufferedCh { fmt.Printf("Received: %d\n", value) } fmt.Println("\nBuffered channel started") go sender(bufferedCh) time.Sleep(1 * time.Second) fmt.Println("\nBuffered channel values:") for value := range bufferedCh { fmt.Printf("Received: %d\n", value) } }
Output :
Unbuffered channel started Unbuffered channel values: Received: 1 Sent: 1 Sent: 2 Received: 2 Received: 3 Sent: 3 Sent: 4 Received: 4 Received: 5 Sent: 5 Buffered channel started Sent: 1 Sent: 2 Sent: 3 Sent: 4 Sent: 5 Buffered channel values: Received: 1 Received: 2 Received: 3 Received: 4 Received: 5
In this example, we have two channels: an unbuffered channel and a buffered channel with a capacity of 5. When using the unbuffered channel, the sender and receiver operations will block until a corresponding operation is ready. With the buffered channel, the sender can send all values without blocking, as the channel has sufficient capacity to store them.
WaitGroup
A WaitGroup is a synchronization primitive provided by the
sync
package that helps you to wait for a collection of goroutines to finish executing. It is particularly useful when you have multiple goroutines working on different tasks, and you want to ensure they all complete before continuing in your main function.Here’s an example demonstrating the use of a WaitGroup:
Code :
package main import ( "fmt" "sync" ) func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // Signal that this worker has finished its task fmt.Printf("Worker %d started\n", id) // Perform some work here fmt.Printf("Worker %d finished\n", id) } func main() { var wg sync.WaitGroup numWorkers := 5 for i := 1; i <= numWorkers; i++ { wg.Add(1) // Increment the WaitGroup counter go worker(i, &wg) // Start a new goroutine } wg.Wait() // Wait for all goroutines to finish fmt.Println("All workers have finished") }
Output :
Worker 5 started Worker 5 finished Worker 1 started Worker 1 finished Worker 4 started Worker 4 finished Worker 2 started Worker 2 finished Worker 3 started Worker 3 finished All workers have finished
Select Statements
The
select
statement is a powerful control structure in Go that allows you to wait on multiple channel operations simultaneously. It's similar to a switch
statement, but for channels. The select
statement blocks until one of the cases can proceed, at which point it executes that case. If multiple cases are ready to proceed, one of them is chosen at random.
Here's an example demonstrating the use of a select
statement:
Code :
package main import ( "fmt" "time" ) func sender1(ch1 chan string) { for { ch1 <- "Message from sender1" time.Sleep(2 * time.Second) } } func sender2(ch2 chan string) { for { ch2 <- "Message from sender2" time.Sleep(3 * time.Second) } } func receiver(ch1, ch2 chan string) { for { select { case msg1 := <-ch1: fmt.Println(msg1) case msg2 := <-ch2: fmt.Println(msg2) } } } func main() { ch1 := make(chan string) ch2 := make(chan string) go sender1(ch1) // Spawn a goroutine to send messages on channel ch1 go sender2(ch2) // Spawn a goroutine to send messages on channel ch2 receiver(ch1, ch2) // Run the receiver function in the main thread }
Output :
Message from sender2 Message from sender1 Message from sender1 Message from sender2 Message from sender1 Message from sender1 Message from sender2 Message from sender1 Message from sender2 Message from sender1 Message from sender1 Message from sender2 . . . Infinite times.
In this example, we have two sender goroutines,
sender1
and sender2
, which send messages on channels ch1
and ch2
, respectively. The receiver
function uses a select
statement to wait for messages from both channels and prints the received messages. The select
statement allows us to easily handle the communication between multiple channels without having to deal with complex synchronization mechanisms.