Goroutines & Channels
Explore Go's concurrency primitives: goroutines for lightweight threads, channels for communication, and sync utilities.
Goroutines
Goroutines are lightweight threads of execution managed by the Go runtime, started simply by prefixing a function call with the 'go' keyword. They are extremely cheap to create, using only a few kilobytes of stack space that grows and shrinks as needed, allowing you to run thousands or even millions concurrently. The Go runtime multiplexes goroutines onto a small number of OS threads using a work-stealing scheduler. The main goroutine must stay alive for other goroutines to continue executing, so synchronization is essential.
package main
import (
"fmt"
"time"
)
func printNumbers(label string) {
for i := 1; i <= 5; i++ {
fmt.Printf("%s: %d\n", label, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// Start goroutines
go printNumbers("goroutine-1")
go printNumbers("goroutine-2")
// Anonymous goroutine
go func(msg string) {
fmt.Println(msg)
}("hello from anonymous goroutine")
// Wait for goroutines to finish
time.Sleep(1 * time.Second)
fmt.Println("main done")
}
Channels
Channels are Go's primary mechanism for communication between goroutines, following the principle of sharing memory by communicating rather than communicating by sharing memory. Channels are typed conduits through which you send and receive values using the '<-' operator. Unbuffered channels block the sender until a receiver is ready and vice versa, providing built-in synchronization. Buffered channels have a capacity and only block when the buffer is full or empty.
package main
import "fmt"
func sum(nums []int, ch chan int) {
total := 0
for _, n := range nums {
total += n
}
ch <- total // Send result to channel
}
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
ch := make(chan int)
// Split work between two goroutines
mid := len(numbers) / 2
go sum(numbers[:mid], ch)
go sum(numbers[mid:], ch)
// Receive results
result1 := <-ch
result2 := <-ch
fmt.Println("Total:", result1+result2)
// Buffered channel
buffered := make(chan string, 2)
buffered <- "hello"
buffered <- "world"
fmt.Println(<-buffered)
fmt.Println(<-buffered)
// Close and range over channel
jobs := make(chan int, 5)
for i := 1; i <= 5; i++ {
jobs <- i
}
close(jobs)
for job := range jobs {
fmt.Println("Job:", job)
}
}
Select Statement
The 'select' statement lets a goroutine wait on multiple channel operations simultaneously, proceeding with whichever one is ready first. If multiple cases are ready, one is chosen at random, ensuring fair scheduling. A 'default' case makes the select non-blocking, which is useful for polling or implementing try-send patterns. Select is commonly used with time.After for implementing timeouts and with context.Done for cancellation.
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(200 * time.Millisecond)
ch1 <- "result from ch1"
}()
go func() {
time.Sleep(100 * time.Millisecond)
ch2 <- "result from ch2"
}()
// Wait for first result
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
// Timeout pattern
slow := make(chan string)
go func() {
time.Sleep(2 * time.Second)
slow <- "done"
}()
select {
case result := <-slow:
fmt.Println(result)
case <-time.After(500 * time.Millisecond):
fmt.Println("Operation timed out")
}
}
WaitGroup and Mutexes
The sync.WaitGroup type provides a way to wait for a collection of goroutines to finish, using a counter that is incremented with Add, decremented with Done, and awaited with Wait. This is more robust than sleeping for a fixed duration. The sync.Mutex type provides mutual exclusion for protecting shared state from concurrent access. Always lock and unlock in the same function, and use 'defer mu.Unlock()' immediately after locking to ensure the mutex is released even if a panic occurs.
package main
import (
"fmt"
"sync"
)
func main() {
// WaitGroup example
var wg sync.WaitGroup
results := make([]int, 5)
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
results[id] = id * id
fmt.Printf("Worker %d done\n", id)
}(i)
}
wg.Wait()
fmt.Println("All workers done. Results:", results)
// Mutex example - safe counter
var mu sync.Mutex
counter := 0
var wg2 sync.WaitGroup
for i := 0; i < 1000; i++ {
wg2.Add(1)
go func() {
defer wg2.Done()
mu.Lock()
defer mu.Unlock()
counter++
}()
}
wg2.Wait()
fmt.Println("Counter:", counter) // Always 1000
}