Concurrency in Go is one of its most powerful features, designed to make it easy to write concurrent programs. Go’s concurrency model is based on goroutines and channels, which provide a simple and efficient way to manage multiple tasks running in parallel.
Goroutines
A goroutine is a lightweight thread managed by the Go runtime. You can start a new goroutine using the go
keyword followed by a function call. Goroutines run concurrently with other goroutines.
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) // Wait for the goroutine to finish
}
Channels
Channels are Go’s way of communicating between goroutines. They provide a way to send and receive values of a specified type.
Creating Channels
ch := make(chan int) // Create a channel of type int
Sending and Receiving
ch <- 42 // Send value to channel
value := <-ch // Receive value from channel
Example
package main
import (
"fmt"
)
func main() {
ch := make(chan string)
go func() {
ch <- "Hello from goroutine"
}()
message := <-ch
fmt.Println(message)
}
Buffered Channels
Buffered channels have a capacity and can hold a limited number of values without a corresponding receiver.
Creating Buffered Channels
ch := make(chan int, 2) // Channel with buffer size 2
Example
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}
Select Statement
The select
statement is like a switch
but for channels. It allows a goroutine to wait on multiple communication operations.
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", msg1)
case msg2 := <-ch2:
fmt.Println("Received", msg2)
}
}
}
Worker Pools
Worker pools are a common pattern for concurrent processing. They allow you to limit the number of goroutines processing tasks concurrently.
Example
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("Worker %d started job %d\n", id, j)
time.Sleep(time.Second)
fmt.Printf("Worker %d finished job %d\n", id, j)
results <- j * 2
}
}
func main() {
const numJobs = 5
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
for w := 1; w < 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
for a := 1; a <= numJobs; a++ {
<-results
}
}
Synchronization
Go provides several mechanisms to synchronize goroutines, including the sync
package with WaitGroup
and Mutex
.
WaitGroup Example
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
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)
go worker(i, &wg)
}
wg.Wait()
}
Mutex Example
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
v map[string]int
mux sync.Mutex
}
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
c.v[key]++
c.mux.Unlock()
}
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
defer c.mux.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
fmt.Println(c.Value("somekey"))
}
Summary
- Goroutines: Lightweight threads managed by the Go runtime, allowing concurrent execution.
- Channels: Used for communication between goroutines, ensuring safe data transfer.
- Buffered Channels: Channels with a capacity to hold multiple values.
- Select Statement: Allows waiting on multiple channel operations.
- Worker Pools: Pattern for concurrent processing, managing a limited number of workers.
- Synchronization:
sync
package provides mechanisms likeWaitGroup
andMutex
for goroutine synchronization.