Learn Concurrency in Go
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.