DEV Community

ajay-8192
ajay-8192

Posted on

Go Concurrency

Go Concurrency Explained in Detail

Go has a built-in feature to handle concurrency, which allows it to run multiple functions simultaneously and efficiently. Concurrency isn't parallelism. Parallelism refers to the simultaneous execution of multiple tasks, whereas concurrency refers to the ability to deal with multiple tasks at once. Go Concurrency is a common topic of discussion. Let's look at the mechanisms that support Go Concurrency.


Goroutines

Goroutines are functions or methods that run concurrently with other functions or methods. Goroutines are lightweight threads managed by the Go runtime. They're similar to threads, but they're much cheaper to create and manage. You can create thousands of goroutines without significant overhead.

To create a goroutine, simply add the go keyword before the function call:

package main

import (
    "fmt"
    "time"
)

func hello() {
    fmt.Println("Hello from goroutine!")
}

func main() {
    go hello() // This will run hello() as a goroutine
    fmt.Println("Hello from main function!")
    time.Sleep(1 * time.Second) // Wait for the goroutine to finish
}
Enter fullscreen mode Exit fullscreen mode

In this example, hello() will run concurrently with the main() function. The time.Sleep() is added to give the goroutine enough time to execute before the main function exits. Without time.Sleep(), the main function might exit before the hello() goroutine has a chance to print its message.


Channels

Channels are the conduits through which goroutines communicate. They allow goroutines to send and receive values of a specified type. Channels are a fundamental part of Go's concurrency model, enabling safe and synchronized communication between concurrent processes.

Declaring and Initializing Channels

You can declare a channel using the chan keyword, followed by the type of data it will carry:

var myChannel chan int // Declares a channel that will carry integers
Enter fullscreen mode Exit fullscreen mode

To initialize a channel, you use the make function:

myChannel := make(chan int) // Initializes an unbuffered channel
Enter fullscreen mode Exit fullscreen mode

Sending and Receiving Values

You send values into a channel using the <- operator:

myChannel <- 10 // Sends the integer 10 into myChannel
Enter fullscreen mode Exit fullscreen mode

You receive values from a channel using the same <- operator, but this time it's on the left side of the assignment:

value := <-myChannel // Receives a value from myChannel and assigns it to 'value'
Enter fullscreen mode Exit fullscreen mode

Here's an example demonstrating channel communication:

package main

import (
    "fmt"
)

func sendData(ch chan string) {
    ch <- "Go Concurrency is awesome!"
}

func main() {
    myChannel := make(chan string) // Create a channel of type string
    go sendData(myChannel)         // Start a goroutine to send data
    message := <-myChannel         // Receive data from the channel
    fmt.Println(message)
}
Enter fullscreen mode Exit fullscreen mode

In this example, sendData sends a string to myChannel, and the main function receives that string and prints it.

Buffered Channels

Channels can be buffered, meaning they have a capacity to hold a certain number of values before blocking. When a channel is unbuffered (capacity 0), sending to it will block until another goroutine receives from it, and receiving from it will block until another goroutine sends to it.

You can create a buffered channel by providing a capacity to the make function:

bufferedChannel := make(chan int, 3) // Creates a buffered channel with a capacity of 3
Enter fullscreen mode Exit fullscreen mode

Here's an example of a buffered channel:

package main

import (
    "fmt"
)

func main() {
    ch := make(chan int, 2) // Buffered channel with capacity 2

    ch <- 1
    ch <- 2
    // ch <- 3 // This would block because the buffer is full

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

Select Statement

The select statement is used to wait on multiple channel operations. It allows a goroutine to block until one of multiple send/receive operations is ready. It's similar to a switch statement but specifically for channels.

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "message from channel 1"
    }()

    go func() {
        time.Sleep(500 * time.Millisecond)
        ch2 <- "message from channel 2"
    }()

    for i := 0; i < 2; i++ { // We expect two messages
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, select will wait for either ch1 or ch2 to send a message. Whichever message arrives first will be printed.


Mutexes (Mutual Exclusion)

While channels are the preferred way to communicate and synchronize in Go, there are situations where you might need to protect shared resources from concurrent access. This is where sync.Mutex comes in. A mutex ensures that only one goroutine can access a critical section of code at a time, preventing race conditions.

package main

import (
    "fmt"
    "sync"
    "time"
)

var (
    counter int
    mutex   sync.Mutex
)

func increment() {
    mutex.Lock()   // Acquire the lock
    counter++
    mutex.Unlock() // Release the lock
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            increment()
        }()
    }

    wg.Wait() // Wait for all goroutines to finish
    fmt.Println("Final counter:", counter)
}
Enter fullscreen mode Exit fullscreen mode

In this example, mutex.Lock() ensures that only one goroutine can increment counter at a time, preventing a race condition where the final counter value might be incorrect without the mutex. sync.WaitGroup is used to wait for all goroutines to complete before printing the final counter.


Conclusion

Go's concurrency model, built around goroutines and channels, provides powerful and elegant ways to write concurrent programs. By understanding these core concepts, you can build highly performant and scalable applications in Go. While mutexes are available for shared memory access, the Go idiom often favors communication through channels to achieve concurrency safely and efficiently. Embrace goroutines and channels, and you'll unlock the true potential of Go for concurrent programming!

Top comments (0)