Concurrency in Go: Goroutines, Channels, and Concurrency Patterns

gophers_kisumu

Gophers Kisumu

Posted on June 17, 2024

Concurrency in Go: Goroutines, Channels, and Concurrency Patterns

Introduction to Goroutines

Concurrency in Go is a core feature that allows programs to perform multiple tasks simultaneously. One of the key tools for achieving concurrency in Go is the goroutine. Goroutines are lightweight threads managed by the Go runtime, enabling efficient execution of concurrent tasks.

Creating and Managing Goroutines

To create a goroutine, you simply use the go keyword followed by a function call. Here’s a basic example:

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    fmt.Println("Hello, Goroutine!")
}

func main() {
    go sayHello()
    time.Sleep(1 * time.Second) // Give the goroutine time to complete
}
Enter fullscreen mode Exit fullscreen mode

In this example, sayHello is executed as a goroutine, allowing the main function to continue running concurrently. The time.Sleep call ensures the program doesn’t terminate before the goroutine finishes executing.

Channels

Channels are a powerful feature in Go that allow goroutines to communicate with each other and synchronize their execution. Channels can be used to send and receive values between goroutines.

Understanding Channels

A channel is created using the make function:

ch := make(chan int)
Enter fullscreen mode Exit fullscreen mode

This creates a channel that can send and receive int values.

Sending and Receiving Data

To send data to a channel, you use the <- operator:

ch <- 42
Enter fullscreen mode Exit fullscreen mode

To receive data from a channel, you also use the <- operator:

value := <-ch
Enter fullscreen mode Exit fullscreen mode

Here’s a complete example demonstrating sending and receiving data:

package main

import (
    "fmt"
)

func sendData(ch chan int) {
    ch <- 42
}

func main() {
    ch := make(chan int)
    go sendData(ch)
    value := <-ch
    fmt.Println(value)
}
Enter fullscreen mode Exit fullscreen mode

Buffered vs Unbuffered Channels

Channels can be either buffered or unbuffered. An unbuffered channel requires both send and receive operations to be ready before any communication can occur. A buffered channel allows sending and receiving to be decoupled.

// Buffered channel with capacity of 2
ch := make(chan int, 2)

ch <- 1
ch <- 2

fmt.Println(<-ch)
fmt.Println(<-ch)
Enter fullscreen mode Exit fullscreen mode

Buffered channels are useful when you want to allow some amount of data to be sent without requiring immediate receiving.

Concurrency Patterns

Select Statement

The select statement allows a goroutine to wait on multiple communication operations.

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"
    }()

    select {
    case msg1 := <-ch1:
        fmt.Println("Received", msg1)
    case msg2 := <-ch2:
        fmt.Println("Received", msg2)
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the select statement waits for either channel to send a message and then proceeds accordingly.

Worker Pools

A worker pool is a pattern used to manage a pool of goroutines that perform tasks from a shared queue.

package main

import (
    "fmt"
    "sync"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        fmt.Printf("Worker %d processing job %d\n", id, job)
        results <- job * 2
    }
}

func main() {
    const numJobs = 5
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)
    var wg sync.WaitGroup

    for w := 1; w <= 3; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    wg.Wait()
    close(results)

    for result := range results {
        fmt.Println("Result:", result)
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, a set of workers process jobs concurrently, distributing the workload among multiple goroutines.

Pipeline Pattern

The pipeline pattern connects multiple stages of processing, where the output of one stage is the input of the next.

package main

import (
    "fmt"
)

func gen(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func sq(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

func main() {
    nums := gen(2, 3, 4)
    squares := sq(nums)

    for n := range squares {
        fmt.Println(n)
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the gen function generates numbers and sends them to the sq function, which squares them, forming a processing pipeline.

Conclusion

Concurrency in Go, facilitated by goroutines and channels, provides a powerful yet simple way to handle multiple tasks simultaneously. Understanding and utilizing concurrency patterns such as the select statement, worker pools, and pipelines can significantly enhance the efficiency and responsiveness of Go applications.

💖 💪 🙅 🚩
gophers_kisumu
Gophers Kisumu

Posted on June 17, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related