Learn › Go Programming

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
}

← Error Handling