Goroutines, Channels and awaiting Asynchronously Operations in Go

Golang has fantastic support for actions that are supposed to happen concurrently (at the same time) without blocking the thread, they are called goroutines and are used by simply putting go in front of a function.

The functions prefixed with go will run "on their own" and the rest of your code will continue to run.

In order to gather results or returns from the functions, you commonly make use of a channel. Channels are the collecting "buckets" that will receive what your goroutines write to them.

This example is taken from the Golang Bootcamp: Concurrency and annotated a bit more.

package main

// note the function needs to be passed the channel
func sum(a []int, c chan int) {
    sum := 0
    // iterate over array
    for _, v := range a {
        // add value to sum
        sum += v
    }
    // send sum to c
    c <- sum
}

func main() {
    a := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    // send first half
    go sum(a[:len(a)/2], c)
    // send second half
    go sum(a[len(a)/2:], c)
    // receive from c to x and y
    x, y := <-c, <-c

}

Not that c, our channel, is initiated with a type. You can also specify your own types, but we'll look at that in a later example.

Channels and appending to Slices

When using channels, you can iterate over items as well and add them to an existing array using append. Note that by default, channels will be waited for, if you only use one go prefix in a loop, you'll basically have the same program as before:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func randomInt(min int, max int) int {
    var bytes int
    bytes = min + rand.Intn(max)
    return int(bytes)
}

func plusone(a int, c chan int) {
    fmt.Printf("now processing: %v\n", a)
    // different random delays, to show that this is synchronously executed
    time.Sleep(time.Second * time.Duration(randomInt(0, 2)))
    c <- a + 1
}

func main() {
    a := []int{7, 2, 8}

    c := make(chan int)

    var incrementedIntegers []int

    for _, element := range a {
        go plusone(element, c)
        // get result from channel
        y := <-c
        // append to existing array
        incrementedIntegers = append(incrementedIntegers, y)
    }

    fmt.Println(incrementedIntegers)
}

Try changing the for loop to:

    go plusone(a[0], c)
    go plusone(a[1], c)
    go plusone(a[2], c)
    // append to existing array
    incrementedIntegers = append(incrementedIntegers, <-c)
    incrementedIntegers = append(incrementedIntegers, <-c)
    incrementedIntegers = append(incrementedIntegers, <-c)

The expected output is still:

now processing: 8
now processing: 2
now processing: 7
[8 9 3]

This will make all your additions run at the same time, note that you need to receive from the channel as many times as you expect output.

Your fmt.Println statement will wait from all values being read from the channel.

Obviously it's not practical to type out every index of an array in order to make your program do a number of things at the same time, let's have a look at how we can run a dynamic number of things at once and wait for them to be finished.

Go Channels and Awaiting Asynchronous Operations

With Node.js we often rely on the async npm module, but in Go some similar functionality is already built in through sync.WaitGroups. We can pass WaitGroups a goroutine have them tell us when they're done and have written to their channel.

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func randomInt(min int, max int) int {
    var bytes int
    bytes = min + rand.Intn(max)
    return int(bytes)
}

func plusone(a int, c chan int, waitGroup *sync.WaitGroup) {
    defer waitGroup.Done()
    fmt.Printf("now processing: %v\n", a)
    // different random delays, to show that this is synchronously executed
    time.Sleep(time.Second * time.Duration(randomInt(0, 2)))
    c <- a + 1
}

func main() {
    a := []int{7, 2, 8}

    c := make(chan int)
    var waitGroup sync.WaitGroup
    var incrementedIntegers []int

    for _, element := range a {
        // add to waitGroup
        waitGroup.Add(1)
        go plusone(element, c, &amp;waitGroup)
    }

    // append to existing array
    for range a {
        incrementedIntegers = append(incrementedIntegers, <-c)
    }

    fmt.Println("Waiting for every Goroutine to be done")
    waitGroup.Wait()

    fmt.Println(incrementedIntegers)
}

When running the code, you should see that the calculations start at once, but the result has to be waited for, since we still have our delay in the plusOne function.

Summary

Go is a great language, built with a lot of real world problems in mind. It's a pleasure to develop with and explore their implementation of concurrency. I hope this post made them a bit more clear! Let me know what you think!

If you at any point read once from a channel that has been written to more times than one (this goes for any two numbers where the writing exceeds the reads), you'll receive a deadlock error. Just double check you're reading all the results:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc4200140ec)
    /usr/local/go/src/runtime/sema.go:56 +0x39
sync.(*WaitGroup).Wait(0xc4200140e0)
    /usr/local/go/src/sync/waitgroup.go:131 +0x72
main.main()
    /home/jonathan/projects/go/src/channels-tutorial/append-waitgroup.go:47 +0x208

goroutine 5 [chan send]:
main.plusone(0x7, 0xc42001e0c0, 0xc4200140e0)
    /home/jonathan/projects/go/src/channels-tutorial/append-waitgroup.go:21 +0x11d
created by main.main
    /home/jonathan/projects/go/src/channels-tutorial/append-waitgroup.go:34 +0xf9

goroutine 7 [chan send]:
main.plusone(0x8, 0xc42001e0c0, 0xc4200140e0)
    /home/jonathan/projects/go/src/channels-tutorial/append-waitgroup.go:21 +0x11d
created by main.main
    /home/jonathan/projects/go/src/channels-tutorial/append-waitgroup.go:34 +0xf9
exit status 2
Tagged with: #go #golang

Thank you for reading! If you have any comments, additions or questions, please tweet or toot them at me!