Learn Concurrency in Go


Concurrency in Go is one of its most powerful features, designed to make it easy to write concurrent programs. Go’s concurrency model is based on goroutines and channels, which provide a simple and efficient way to manage multiple tasks running in parallel.

Goroutines

A goroutine is a lightweight thread managed by the Go runtime. You can start a new goroutine using the go keyword followed by a function call. Goroutines run concurrently with other goroutines.

Example

package main

import (
    "fmt"
    "time"
)

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

func main() {
    go sayHello() // Start a new goroutine
    time.Sleep(1 * time.Second) // Wait for the goroutine to finish
}

Channels

Channels are Go’s way of communicating between goroutines. They provide a way to send and receive values of a specified type.

Creating Channels

ch := make(chan int) // Create a channel of type int

Sending and Receiving

ch <- 42 // Send value to channel
value := <-ch // Receive value from channel

Example

package main

import (
    "fmt"
)

func main() {
    ch := make(chan string)

    go func() {
        ch <- "Hello from goroutine"
    }()

    message := <-ch
    fmt.Println(message)
}

Buffered Channels

Buffered channels have a capacity and can hold a limited number of values without a corresponding receiver.

Creating Buffered Channels

ch := make(chan int, 2) // Channel with buffer size 2

Example

package main

import (
    "fmt"
)

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2

    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

Select Statement

The select statement is like a switch but for channels. It allows a goroutine to wait on multiple communication operations.

Example

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

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println("Received", msg1)
        case msg2 := <-ch2:
            fmt.Println("Received", msg2)
        }
    }
}

Worker Pools

Worker pools are a common pattern for concurrent processing. They allow you to limit the number of goroutines processing tasks concurrently.

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
    }
}

Synchronization

Go provides several mechanisms to synchronize goroutines, including the sync package with WaitGroup and Mutex.

WaitGroup 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 <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
}

Mutex Example

package main

import (
    "fmt"
    "sync"
)

type SafeCounter struct {
    v   map[string]int
    mux sync.Mutex
}

func (c *SafeCounter) Inc(key string) {
    c.mux.Lock()
    c.v[key]++
    c.mux.Unlock()
}

func (c *SafeCounter) Value(key string) int {
    c.mux.Lock()
    defer c.mux.Unlock()
    return c.v[key]
}

func main() {
    c := SafeCounter{v: make(map[string]int)}
    for i := 0; i < 1000; i++ {
        go c.Inc("somekey")
    }

    fmt.Println(c.Value("somekey"))
}

Summary

  • Goroutines: Lightweight threads managed by the Go runtime, allowing concurrent execution.
  • Channels: Used for communication between goroutines, ensuring safe data transfer.
  • Buffered Channels: Channels with a capacity to hold multiple values.
  • Select Statement: Allows waiting on multiple channel operations.
  • Worker Pools: Pattern for concurrent processing, managing a limited number of workers.
  • Synchronization: sync package provides mechanisms like WaitGroup and Mutex for goroutine synchronization.