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
gopackage 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 startprintNumbers()
as a goroutine using thego
keyword. This initiates the execution ofprintNumbers()
concurrently with themain
function. - The
main()
function continues to execute concurrently withprintNumbers()
, allowing both to run concurrently and print their respective outputs.
Example 2: Concurrent Execution
gopackage 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 async.WaitGroup
to synchronize the completion of goroutines. We add 2 to the WaitGroup counter before starting two goroutines withgo printMessage(...)
. - Each goroutine executes concurrently, printing their respective messages (“Hello” and “Go”) three times.
main()
waits for all goroutines to complete usingwg.Wait()
, ensuring that it doesn’t exit before all goroutines are finished.
Example 3: Channels for Communication
gopackage 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()
andreceiveNumbers()
functions to send numbers to and receive numbers from a channel, respectively. - In
main()
, we create an unbuffered channelch
for communication between goroutines. - We start a goroutine
sendNumbers(ch)
to send numbers 1 to 5 to the channelch
. - Another goroutine
receiveNumbers(ch, done)
is started to receive numbers fromch
and print them. - The
main()
function waits for thereceiveNumbers
goroutine to complete by receiving a value from thedone
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
gopackage 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 typeint
usingmake(chan int)
. - Inside a goroutine, we send the integer value
42
to the channelch
using the<-
operator. - In the main goroutine, we receive the value from the channel
ch
using the<-
operator and store it in the variablevalue
. - The program will output
Received: 42
, demonstrating how data is exchanged between goroutines via a channel.
Example 2: Buffered Channels
gopackage 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 typestring
with a capacity of3
usingmake(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 withrange
to iterate over the channelch
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
gopackage 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
andch2
. - Two goroutines are started with
go func()
to send values toch1
andch2
after 2 seconds and 1 second, respectively. - We use a
select
statement to receive data from whichever channel (ch1
orch2
) 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
gopackage 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 typeint
with a capacity of3
usingmake(chan int, 3)
. - Inside a goroutine, we send integer values
1
,2
, and3
to the buffered channelch
using the<-
operator in a loop. - After sending all values, we close the channel
ch
using theclose(ch)
statement to indicate no more values will be sent. - In the main goroutine, we use a
for ... range
loop to iterate over the channelch
and receive all values until the channel is closed. - The program will output:makefile
Received: 1 Received: 2 Received: 3
Example 5: Bidirectional Channels
gopackage 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 typestring
. - The
sendAndReceive()
function sends the string “Hello” to the channelch
using<-
operator and then immediately receives and prints the value from the channel using<-ch
. - In the
main()
function, we start a goroutine to executesendAndReceive(ch)
, which performs both send and receive operations on the channelch
. - The program blocks on
<-ch
inmain()
until thesendAndReceive()
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
gopackage 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
andch2
of typestring
. - Two goroutines are started with
go func()
to send values toch1
andch2
after 2 seconds and 1 second, respectively. - The
select
statement waits for eitherch1
orch2
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 toch1
after 2 seconds. - The program will output:
Received from ch2: Message from ch2
.
Example 2: Select with Default Case
gopackage 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 typestring
. - A goroutine is started with
go func()
to send a value toch
after 2 seconds. - The
select
statement waits for a receive operation onch
. Ifch
is not ready within a certain time frame, it proceeds to thedefault
case. - In this example, since
ch
sends a value after 2 seconds, theselect
statement receives the value and prints"Received: Message"
. - If the send operation to
ch
took longer than expected, thedefault
case would execute, printing"No message received"
.
Example 3: Select with Timeout
gopackage 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 typestring
. - A goroutine is started with
go func()
to send a value toch
after 2 seconds. - The
select
statement waits for a receive operation onch
. If the receive operation onch
does not complete within 1 second, thetime.After
channel will become ready, and theselect
statement will proceed with the second case. - In this example, since the receive operation on
ch
takes 2 seconds to receive a value, theselect
statement will print"Timeout occurred"
after 1 second.
Example 4: Select with Multiple Cases
gopackage 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
andch2
of typestring
. - Two goroutines are started with
go func()
to send values toch1
andch2
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 neitherch1
norch2
is ready to receive a value. - The
select
statement will choose the first case that is ready. Ifch1
orch2
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
gopackage 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 typestring
with capacity1
usingmake(chan string, 1)
. - In the first
select
block, we attempt to send the string"Message"
toch
. Sincech
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 fromch
. Sincech
already has a value ("Message"
), the receive operation succeeds, and it prints"Received: Message"
. - If
ch
was empty in the secondselect
block, thedefault
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
gopackage 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 useMutex
. - We define a global variable
counter
to be incremented andmutex
of typesync.Mutex
to synchronize access tocounter
. - The
increment
function locks the mutex withmutex.Lock()
, increments thecounter
, and then unlocks the mutex withmutex.Unlock()
, ensuring that only one goroutine can modifycounter
at a time. - In the
main
function, we start 10 goroutines to concurrently executeincrement
. - After a delay (
time.Sleep(1 * time.Second)
), we print the final value ofcounter
. Due to the mutual exclusion provided bymutex
, the final value ofcounter
will be10
, 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
gopackage 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 useWaitGroup
. - We define the
worker
function that takes anid
and a pointer tosync.WaitGroup
(wg
). It deferswg.Done()
to decrement the wait group counter when the function exits. - In the
main
function, we create async.WaitGroup
variablewg
. - We start 5 goroutines (
worker
function calls) using a loop. For each goroutine, we increment the wait group counter usingwg.Add(1)
. wg.Wait()
blocks until the wait group counter returns to zero, meaning all goroutines have calledwg.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.
Leave a Reply