Goroutines
1 February 2025 / 7 min read
1. Goroutines
Goroutines are the foundation of concurrency in Go. They are lightweight threads managed by the Go runtime (not the operating system!). They are highly efficient and scalable.
Key Details:
- Lightweight: Goroutines have a small stack size (starting at 2KB, growing as needed) compared to OS threads (typically 1MB or more).
- Scheduling: The Go runtime uses an M:N scheduler, which multiplexes (M) goroutines onto (N) OS threads. This allows thousands or even millions of goroutines to run concurrently.
- Concurrency vs. Parallelism: Concurrency is about structuring a program to handle multiple tasks at once, while parallelism is about executing multiple tasks simultaneously. Goroutines enable concurrency, but parallelism depends on the number of CPU cores.
Example:
package main
import (
"fmt"
"time"
)
func printNumbers() {
for i := 1; i <= 5; i++ {
fmt.Println(i)
time.Sleep(500 * time.Millisecond)
}
}
func main() {
go printNumbers() // Start a goroutine
go printNumbers() // Start another goroutine
// Wait for goroutines to finish (naive approach)
time.Sleep(3 * time.Second)
}
2. Channels
Channels are the primary way goroutines communicate and synchronize. They allow safe data exchange between goroutines.
Key Details:
- Thread-Safe: Channels handle synchronization internally, so you don’t need to worry about race conditions when using them.
- Blocking: By default, channels are unbuffered, meaning sending and receiving block until the other side is ready.
- Directionality: Channels can be restricted to sending or receiving only (e.g.,
chan<- int
for send-only,<-chan int
for receive-only).
Example:
package main
import (
"fmt"
"time"
)
func worker(id int, ch chan string) {
for i := 1; i <= 3; i++ {
ch <- fmt.Sprintf("Worker %d: Task %d", id, i)
time.Sleep(500 * time.Millisecond)
}
}
func main() {
ch := make(chan string)
for i := 1; i <= 2; i++ {
go worker(i, ch)
}
for i := 1; i <= 6; i++ {
fmt.Println(<-ch)
}
}
3. Buffered Channels
Buffered channels allow sending multiple values without an immediate receiver. They have a fixed capacity, and sending blocks only when the buffer is full.
Key Details:
- Capacity: The buffer size is specified when creating the channel (e.g.,
make(chan int, 10)
). - Non-Blocking: Sending to a buffered channel is non-blocking as long as the buffer is not full. Similarly, receiving is non-blocking as long as the buffer is not empty.
Example:
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}
4. Select Statement
The select
statement allows you to wait on multiple channel operations. It is similar to a switch
statement but for channels.
Key Details:
- Non-Deterministic: If multiple cases are ready, one is chosen at random.
- Default Case: The
default
case is executed immediately if no other case is ready, makingselect
non-blocking.
Example:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "Hello"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "World"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
}
5. Synchronization
WaitGroup:
A sync.WaitGroup
waits for a collection of goroutines to finish. It uses a counter to track the number of active goroutines.
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 <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("All workers done")
}
Mutex:
A sync.Mutex
ensures exclusive access to shared resources. It prevents race conditions by allowing only one goroutine to access the resource at a time.
Key Details:
- Lock/Unlock: Use
Lock()
to acquire the mutex andUnlock()
to release it. - Shared Resources: Use a mutex to protect shared resources like global variables, slices, or maps.
Example:
package main
import (
"fmt"
"sync"
)
var counter int
var mutex sync.Mutex
func increment(wg *sync.WaitGroup) {
defer wg.Done()
mutex.Lock()
counter++
mutex.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)
}
6. Context
The context
package is used to manage the lifecycle of goroutines, such as cancellation, timeouts, and deadlines.
Key Details:
- Cancellation: Use
context.WithCancel
to cancel a goroutine. - Timeout: Use
context.WithTimeout
to set a deadline for a goroutine. - Deadline: Use
context.WithDeadline
to specify an exact time for cancellation.
Example:
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Worker cancelled")
return
default:
fmt.Println("Working...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
go worker(ctx)
time.Sleep(3 * time.Second)
}
7. Patterns
Fan-In:
Combining multiple input channels into a single channel.
Example:
package main
import (
"fmt"
"time"
)
func producer(ch chan<- int, start int) {
for i := start; i < start+5; i++ {
ch <- i
time.Sleep(100 * time.Millisecond)
}
}
func fanIn(ch1, ch2 <-chan int, out chan<- int) {
for {
select {
case msg := <-ch1:
out <- msg
case msg := <-ch2:
out <- msg
}
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
out := make(chan int)
go producer(ch1, 1)
go producer(ch2, 10)
go fanIn(ch1, ch2, out)
for i := 0; i < 10; i++ {
fmt.Println(<-out)
}
}
Fan-Out:
Distributing work from a single channel to multiple worker goroutines.
Example:
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d started job %d\n", id, job)
time.Sleep(time.Second)
fmt.Printf("Worker %d finished job %d\n", id, job)
results <- job * 2
}
}
func main() {
jobs := make(chan int, 10)
results := make(chan int, 10)
var wg sync.WaitGroup
// Start 3 workers
for w := 1; w <= 3; w++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
worker(id, jobs, results)
}(w)
}
// Send 5 jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
wg.Wait()
close(results)
for result := range results {
fmt.Println("Result:", result)
}
}
8. Best Practices
1. Avoid Goroutine Leaks
Always ensure goroutines terminate properly. Use context
or channels to signal when a goroutine should exit.
2. Use Buffered Channels Wisely
Buffered channels can help decouple producers and consumers, but they can also hide issues like deadlocks. Use them with care.
3. Prefer select
for Multiplexing
Use select
to handle multiple channels, especially when dealing with timeouts or cancellations.
4. Use Context for Cancellation
Always use context
to manage the lifecycle of goroutines, especially in long-running tasks.
5. Backpressure
Backpressure is a strategy to handle situations where producers are faster than consumers. Use buffered channels or explicit signaling to control the flow of data.
Example of Backpressure:
package main
import (
"fmt"
"time"
)
func producer(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
fmt.Println("Produced:", i)
}
close(ch)
}
func consumer(ch <-chan int, done chan<- bool) {
for i := range ch {
fmt.Println("Consumed:", i)
time.Sleep(500 * time.Millisecond) // Simulate slow consumer
}
done <- true
}
func main() {
ch := make(chan int, 3) // Buffered channel for backpressure
done := make(chan bool)
go producer(ch)
go consumer(ch, done)
<-done
}
9. Common Pitfalls
1. Deadlocks
Deadlocks occur when goroutines are waiting on each other indefinitely. Always ensure channels are properly closed and goroutines can exit.
2. Race Conditions
Race conditions occur when multiple goroutines access shared resources without synchronization. Use sync.Mutex
or channels to protect shared resources.
3. Goroutine Leaks
Goroutine leaks happen when goroutines are started but never terminated. Always use context
or channels to ensure goroutines exit.