DEV Community

Daniel Puig Gerarde
Daniel Puig Gerarde

Posted on

Concurrency in Go using Goroutines and Channels.

"Go serves both siblings, DevOps and SRE, from its fast build times and lean syntax to its security and reliability support. Go’s concurrency and networking features also make it ideal for tools that manage cloud deployment—readily supporting automation while scaling for speed and code maintainability as development infrastructure grows over time." Development Operations & Site Reliability Engineering

Introduction

Concurrency is an important aspect of modern software development that allows multiple tasks to be executed independently, enhancing the overall performance and responsiveness of an application. In Go, a statically-typed compiled language designed by Google, concurrency is one of the built-in features that developers absolutely love. One of the key components that make this possible is the Goroutine.

What is a Goroutine?

A Goroutine is a lightweight thread managed by the Go runtime. It allows you to run functions concurrently with other functions. Goroutines are one of the key elements that allow Go programs to easily implement parallel and concurrent processing. Unlike traditional threads, Goroutines are cheaper to create, and their stack sizes grow and shrink dynamically, making them more efficient.

package main

import (
    "fmt"
    "time"
)

func printMessage(msg string) {
    for i := 0; i < 10; i++ {
        fmt.Println(msg)
        time.Sleep(time.Millisecond * 100)
    }
}

func main() {
    go printMessage("Black")
    printMessage("Yellow")
}
Enter fullscreen mode Exit fullscreen mode

Output

Yellow
Black
Black
Yellow
Yellow
Black
Black
Yellow
Yellow
Black
Black
Yellow
Yellow
Black
Black
Yellow
Yellow
Black
Black
Yellow
Enter fullscreen mode Exit fullscreen mode

In this example, the printMessage function is called twice: once as a Goroutine and once as a normal function call. Both will execute concurrently.

Synchronization Using Channels

While Goroutines make it easy to implement concurrency, they also present challenges, especially when it comes to coordinating tasks or sharing data. Go provides a mechanism called 'channels' for safely communicating between Goroutines.

Here's an example that uses a channel to synchronize two Goroutines:

package main

import "fmt"

func printMessage(msg string, ch chan string) {
    fmt.Println(msg)
    ch <- "Done"
}

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

    go printMessage("Hello", ch)
    go printMessage("World", ch)

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

}
Enter fullscreen mode Exit fullscreen mode

Output

World
Done
Hello
Done
Enter fullscreen mode Exit fullscreen mode

In this example, each Goroutine sends a "Done" message on the channel ch after completing its task. The main function waits to receive two "Done" messages before terminating.

Advantages of Using Goroutines

  1. Resource Efficiency: Goroutines are incredibly lightweight, requiring only a few kilobytes of stack memory.
  2. Ease of Use: With the simple go keyword, you can convert most functions into Goroutines.
  3. Built-in Support: The Go runtime natively supports Goroutines, eliminating the need for third-party libraries for task scheduling or context switching.

Goroutines vs Threads

  1. Stack Size: Threads typically have a fixed stack size, usually around 1-2MB, whereas Goroutines start with a much smaller stack that can dynamically grow and shrink.
  2. Creation Cost: Goroutines are much cheaper to create in terms of memory and CPU time compared to threads.
  3. Scheduling: Goroutines are cooperatively scheduled by the Go runtime, which simplifies the design compared to preemptively scheduled threads.

Examples:

Using sync.WaitGroup for Synchronization

Instead of using channels for synchronization, you can use sync.WaitGroup. A WaitGroup waits for a collection of Goroutines to finish executing.

package main

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

func worker(id int, wg *sync.WaitGroup) {
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
    wg.Done()
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
}
Enter fullscreen mode Exit fullscreen mode

Parallel Summation using Goroutines

In this example, we split an array into two halves and sum them concurrently.

package main

import "fmt"

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

func main() {
    data := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    c := make(chan int)

    go sum(data[:len(data)/2], c)
    go sum(data[len(data)/2:], c)

    x, y := <-c, <-c

    fmt.Printf("Sum1: %d, Sum2: %d, Total Sum: %d\n", x, y, x+y)
}
Enter fullscreen mode Exit fullscreen mode

Using select with Channels

The select statement allows you to wait for multiple channel operations, similar to how switch works for value types.

package main

import (
    "fmt"
    "time"
)

func server1(ch chan string) {
    time.Sleep(time.Second * 2)
    ch <- "Response from server 1"
}

func server2(ch chan string) {
    time.Sleep(time.Second * 1)
    ch <- "Response from server 2"
}

func main() {
    output1 := make(chan string)
    output2 := make(chan string)

    go server1(output1)
    go server2(output2)

    select {
    case msg1 := <-output1:
        fmt.Println(msg1)
    case msg2 := <-output2:
        fmt.Println(msg2)
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, two "servers" send a message on their respective channels. We use select to wait for the first response and print it.

Timer and Ticker

Timers and tickers are other important features in Go that can be used for scheduling.

package main

import (
    "fmt"
    "time"
)

func main() {
    timer := time.NewTimer(time.Second * 2)
    ticker := time.NewTicker(time.Second)

    go func() {
        for t := range ticker.C {
            fmt.Println("Tick at", t)
        }
    }()

    <-timer.C
    ticker.Stop()
    fmt.Println("Timer expired")
}
Enter fullscreen mode Exit fullscreen mode

In this example, the ticker ticks every second, and the timer expires after two seconds. When the timer expires, the ticker is stopped.

These examples demonstrate different facets of Go's concurrency model, helping you understand how versatile and useful Goroutines can be.

Goroutines are a powerful feature in Go for implementing concurrent tasks. They are simple to use, efficient, and well-integrated into the language and its standard library. Channels further enhance their usability by providing a safe way to share data between concurrently running Goroutines. As you dive deeper into Go, you'll find Goroutines and channels to be indispensable tools in your development toolkit.

Top comments (0)