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.