Introduction
Concurrency is one of the core features of Go, allowing you to write programs that can perform multiple tasks simultaneously. Go’s concurrency model is based on goroutines and channels, which provide a simple yet powerful way to manage concurrent execution. In this chapter, you will learn the basics of Go’s concurrency model, including goroutines, channels, and synchronization techniques.
Goroutines
A goroutine is a lightweight thread managed by the Go runtime. Goroutines allow you to run functions concurrently with other goroutines.
Example: Creating a Goroutine
Example:
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Hello, World!")
}
func main() {
go sayHello() // Start a new goroutine
time.Sleep(1 * time.Second) // Give the goroutine time to run
}
In this example, the sayHello
function runs concurrently with the main
function.
Channels
Channels provide a way for goroutines to communicate with each other and synchronize their execution. A channel is a conduit through which you can send and receive values of a specific type.
Creating and Using Channels
Example:
package main
import (
"fmt"
)
func main() {
ch := make(chan string)
go func() {
ch <- "Hello, World!" // Send a value to the channel
}()
msg := <-ch // Receive a value from the channel
fmt.Println(msg)
}
In this example, a goroutine sends a message to the ch
channel, and the main function receives and prints the message.
Buffered Channels
Buffered channels allow you to specify the capacity of the channel. They block only when the buffer is full or empty.
Example:
package main
import (
"fmt"
)
func main() {
ch := make(chan string, 2)
ch <- "Hello"
ch <- "World"
fmt.Println(<-ch)
fmt.Println(<-ch)
}
In this example, the channel has a buffer size of 2, allowing two values to be sent to the channel without blocking.
Channel Synchronization
Channels can also be used to synchronize the execution of goroutines.
Example:
package main
import (
"fmt"
)
func worker(done chan bool) {
fmt.Println("Working...")
done <- true // Signal that the work is done
}
func main() {
done := make(chan bool)
go worker(done)
<-done // Wait for the worker to finish
fmt.Println("Done")
}
In this example, the main function waits for the worker goroutine to finish by receiving a value from the done
channel.
Select Statement
The select
statement allows a goroutine to wait on multiple communication operations. It blocks until one of its cases can proceed, then it executes that case.
Example:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "One"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "Two"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("Received from ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("Received from ch2:", msg2)
}
}
}
In this example, the select
statement waits for messages from either ch1
or ch2
channels and prints the received message.
Mutexes
The sync
package provides mutual exclusion locks, or mutexes, to protect shared data from being accessed concurrently by multiple goroutines.
Example:
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func increment(wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock()
counter++
mu.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Counter:", counter) // Output: Counter: 1000
}
In this example, a mutex is used to ensure that only one goroutine increments the counter at a time.
Conclusion
Concurrency in Go is made simple and effective through goroutines and channels. By understanding how to create and manage goroutines, communicate between them using channels, and synchronize their execution using mutexes and the select
statement, you can write robust and efficient concurrent programs. This powerful concurrency model is one of the key features that makes Go an excellent choice for building scalable and high-performance applications.