Swiftorial Logo
Home
Swift Lessons
Matchups
CodeSnaps
Tutorials
Career
Resources

Advanced Go - Concurrency Patterns

Introduction to Concurrency in Go

Go is designed with concurrency in mind. It provides goroutines and channels to handle concurrent programming. Concurrency is not parallelism, but it enables the structuring of programs to handle multiple tasks at once. Let's explore some common concurrency patterns in Go.

1. Goroutines

A goroutine is a lightweight thread managed by the Go runtime. Goroutines are used to perform tasks concurrently. They are cheaper than threads in terms of memory and resources.

Example:

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}
                
Output:
hello
world
hello
world
hello
world
hello
world
hello
world
                    

2. Channels

Channels are used to send and receive data between goroutines. They provide a way for goroutines to communicate with each other and synchronize their execution.

Example:

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c

    fmt.Println(x, y, x+y)
}
                
Output:
17 -5 12
                    

3. Buffered Channels

Buffered channels have a capacity and do not block until the buffer is full. They allow sending data to the channel without blocking the sender until the receiver is ready to receive.

Example:

package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}
                
Output:
1
2
                    

4. Select Statement

The select statement is used to wait on multiple channel operations. It blocks until one of its cases can proceed, then executes that case.

Example:

package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}
                
Output:
0
1
1
2
3
5
8
13
21
34
quit
                    

5. Worker Pools

Worker pools are a common concurrency pattern that allows you to limit the number of goroutines running at the same time, thus controlling the resource usage.

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
    }
}
                
Output:
worker 1 started job 1
worker 2 started job 2
worker 3 started job 3
worker 1 finished job 1
worker 1 started job 4
worker 2 finished job 2
worker 2 started job 5
worker 3 finished job 3
worker 1 finished job 4
worker 2 finished job 5