Concurrency Patterns in Golang

Concurrency Patterns in Golang

Introduction

I have been hearing a lot lately about concurrency and precisely how concurrency manifests in its uniqueness, elegance, and simplicity when it comes to Golang, although I only knew the very basics of Go, I've decided to take a shot to learn concurrency patterns that Go provides.

One of the main characteristics of Go (besides its great mascot) is concurrency, which is intended to make it simple to develop concurrent applications that can effectively leverage many CPUs and manage a lot of concurrent requests or connections.
These are some crucial concepts and tools for concurrency in Go:

Goroutines

Goroutines: Goroutines are lightweight threads that are managed by the Go runtime, and they are used to execute concurrent tasks. Goroutines are cheap to create and have a small memory footprint, so you can use them liberally to parallelize tasks and handle many concurrent requests or connections. You can start a new goroutine using the go keyword before a function call:

func main() {
    go myFunc() // Start a new goroutine to execute myFunc concurrently
    // ...
}

func myFunc() {
    // ...
}

Channels

Channels: Channels are used for communication and synchronization between goroutines. Channels provide a way to pass data between goroutines and coordinate their execution. Channels can be used to implement various concurrency patterns, such as producer-consumer. You can create a channel using the make function:

func main() {
     // create a channel with make function
    ch := make(chan int)
    go func() {
        // some code to send data through channel
        ch <- 42
    }()
    data := <-ch
    // use data received from channel
}

WaitGroup

WaitGroup: WaitGroup is used to wait for a group of goroutines to complete before continuing. WaitGroup provides a way to synchronize the execution of multiple goroutines and ensure that all of them have completed their tasks before proceeding. You can create a WaitGroup using the sync package:

var wg sync.WaitGroup // Create a WaitGroup variable

func main() {
    for i := 0; i < 5; i++ {
        wg.Add(1) // Increment the WaitGroup counter for each goroutine
        go myFunc(&wg)
    }
    wg.Wait() // Wait for all goroutines to complete
}

func myFunc(wg *sync.WaitGroup) {
    defer wg.Done() // Decrement the WaitGroup counter when the goroutine completes
    // ...
}

Select statement

Select statement: The select statement is used to wait for multiple channels to receive or send data, and it allows you to choose the first channel that is ready. The select statement can be used to implement non-blocking communication and timeouts. Here is an example of using the select statement to receive data from multiple channels:

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go produce(ch1)
    go produce(ch2)
    for i := 0; i < 2; i++ {
        select {
        case x := <-ch1:
            fmt.Println("Received from ch1:", x)
        case x := <-ch2:
            fmt.Println("Received from ch2:", x)
        }
    }
}

func produce(ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}

Mutex

Mutex: Mutex is a mutual exclusion lock used to protect shared resources from concurrent access. Mutex ensures that only one goroutine can access a shared resource at a time, preventing race conditions and data races. You can create a Mutex using the sync package:

var mu sync.Mutex // Create a Mutex variable
func myFunc() {
    mu.Lock() // Acquire the lock before accessing the shared resource
// ...
mu.Unlock() // Release the lock after accessing the shared resource
 }

Atomic

Atomicity pattern, Atomic: To ensure Atomicity and Atomic operations, The atomic package provides low-level atomic operations for managing shared memory. Atomic operations can be used to perform read-modify-write operations on shared variables, without the risk of race conditions.

type Counter struct {
    value int32 // must use int32 for atomic operations
}

func (c *Counter) Increment() {
    atomic.AddInt32(&c.value, 1) // increment value atomically
}

func (c *Counter) Value() int32 {
    return atomic.LoadInt32(&c.value) // read value atomically
}

func main() {
    var wg sync.WaitGroup
    var counter Counter
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }
    wg.Wait()
    fmt.Println(counter.Value())
}

These are some of the key tools and concepts in Go for concurrency.

Conclusion

Golang provides a rich set of standard library functions and packages for concurrency. With these tools, you can write efficient, scalable concurrent-prone programs in Go.