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 thegokeyword. This initiates the execution ofprintNumbers()concurrently with themainfunction. - 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.WaitGroupto 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 channelchfor 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 fromchand print them. - The
main()function waits for thereceiveNumbersgoroutine to complete by receiving a value from thedonechannel.
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
chof typeintusingmake(chan int). - Inside a goroutine, we send the integer value
42to the channelchusing the<-operator. - In the main goroutine, we receive the value from the channel
chusing 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
chof typestringwith a capacity of3usingmake(chan string, 3). - Inside a goroutine, we send three string values (“one”, “two”, “three”) to the buffered channel
chusing the<-operator. - We then close the channel
chafter sending all values to signal that no more values will be sent. - In the main goroutine, we use a
forloop withrangeto iterate over the channelchand 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
ch1andch2. - Two goroutines are started with
go func()to send values toch1andch2after 2 seconds and 1 second, respectively. - We use a
selectstatement to receive data from whichever channel (ch1orch2) 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
chof typeintwith a capacity of3usingmake(chan int, 3). - Inside a goroutine, we send integer values
1,2, and3to the buffered channelchusing the<-operator in a loop. - After sending all values, we close the channel
chusing theclose(ch)statement to indicate no more values will be sent. - In the main goroutine, we use a
for ... rangeloop to iterate over the channelchand 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
chof typestring. - The
sendAndReceive()function sends the string “Hello” to the channelchusing<-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
<-chinmain()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
ch1andch2of typestring. - Two goroutines are started with
go func()to send values toch1andch2after 2 seconds and 1 second, respectively. - The
selectstatement waits for eitherch1orch2to be ready to receive a value. It will proceed with the case that is ready first. - In this example,
ch2is ready first because it receives a value after 1 second compared toch1after 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
chof typestring. - A goroutine is started with
go func()to send a value tochafter 2 seconds. - The
selectstatement waits for a receive operation onch. Ifchis not ready within a certain time frame, it proceeds to thedefaultcase. - In this example, since
chsends a value after 2 seconds, theselectstatement receives the value and prints"Received: Message". - If the send operation to
chtook longer than expected, thedefaultcase 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
chof typestring. - A goroutine is started with
go func()to send a value tochafter 2 seconds. - The
selectstatement waits for a receive operation onch. If the receive operation onchdoes not complete within 1 second, thetime.Afterchannel will become ready, and theselectstatement will proceed with the second case. - In this example, since the receive operation on
chtakes 2 seconds to receive a value, theselectstatement 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
ch1andch2of typestring. - Two goroutines are started with
go func()to send values toch1andch2after 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 neitherch1norch2is ready to receive a value. - The
selectstatement will choose the first case that is ready. Ifch1orch2receives 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
chof typestringwith capacity1usingmake(chan string, 1). - In the first
selectblock, we attempt to send the string"Message"toch. Sincechis buffered and has space, the send operation succeeds, and it prints"Sent: Message". - In the second
selectblock, we attempt to receive a value fromch. Sincechalready has a value ("Message"), the receive operation succeeds, and it prints"Received: Message". - If
chwas empty in the secondselectblock, thedefaultcase 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
counterto be incremented andmutexof typesync.Mutexto synchronize access tocounter. - The
incrementfunction locks the mutex withmutex.Lock(), increments thecounter, and then unlocks the mutex withmutex.Unlock(), ensuring that only one goroutine can modifycounterat a time. - In the
mainfunction, 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 ofcounterwill 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
workerfunction that takes anidand a pointer tosync.WaitGroup(wg). It deferswg.Done()to decrement the wait group counter when the function exits. - In the
mainfunction, we create async.WaitGroupvariablewg. - We start 5 goroutines (
workerfunction 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