Go Concurrency

Go has a concurrency system that allows you to run program units in parallel. At the most basic level, this is done by prefixing a function call with the go command. To demonstrate this, I set up a simple example as per”

func doLoop(strPrefix string){

    for i := 0; i < 10; i ++ {
        fmt.Println(strPrefix + ": ", i)
    }
}

func main(){
    fmt.Println("Program starting")

    go doLoop("new thread")
    doLoop("main thread")
}

However, I found that the concurrent thread never output anything. I can only conclude the main thread completed before the concurrent thread got started. I slightly tweaked this to include a 100ms sleep between outputs (as per the example on tour.golang.org), and the desired result can be demonstrated:

func doLoop(strPrefix string){

    for i := 0; i < 10; i ++ {
        fmt.Println(strPrefix + ": ", i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main(){
    fmt.Println("Program starting")

    go doLoop("new thread")
    doLoop("main thread")
}

Output: Program starting main thread: 0 new thread: 0 main thread: 1 new thread: 1 main thread: 2 new thread: 2 main thread: 3 new thread: 3 main thread: 4 new thread: 4 main thread: 5 new thread: 5 main thread: 6 new thread: 6 main thread: 7 new thread: 7 main thread: 8 new thread: 8 main thread: 9 new thread: 9

Which, as we can see is running both routines simultaneously. Not a very useful example, but just demonstrated both threads running.

The problem with the above example, once the concurrent process has started - we have no way to communicate with the process. It would be more beneficial if we could open up a communication channel between threads. Luckily, this is possible with channels.

First, a channel is created with the make function where you pass in the commands chan and the data type used to communicate (int, string, etc).

numChannel := make(chan int)

Then, to send and receive values across the channel, you use the channel operator (<-).

To send a value down the channel, you start with the channel variable, then the channel operator and finally the value you wish to send across the channel (numChannel <- 1 – to send 1 across the channel).

To receive a value, you put the channel variable immediately after the channel operator (v := <-numChannel – assign the value received into v).

For a basic example of using a channel to return a value (one that doesn’t really require concurrency):

package main

import (
    "fmt"
)

func main(){
    fmt.Println("Program starting")

    numChannel := make(chan int)

    go get_inv(55, numChannel)
    go get_inv(-134, numChannel)

    a, b := <-numChannel,<-numChannel

    fmt.Println("55 inverted is", a)
    fmt.Println("-134 inverted is", b)

}

func get_inv(i int, c chan int){

    inversedVal := i/-1
    c <- inversedVal

}

By default, a channel will be unbuffered - but you can opt to buffer it by passing in a second parameter that is the size of the buffer. Once the specified size of the buffer fills up, the channel won’t be able to open up any more communications.

Removing the concurrency aspect, if we make a channel with a buffer and try to use the channel with more values than the buffer size permits, we will get a deadlock error.

func main(){
    fmt.Println("Program starting")

    numChannel := make(chan int, 2)

    numChannel <- 10
    numChannel <- 20
    numChannel <- 30

    fmt.Println(<-numChannel)
    fmt.Println(<-numChannel)
    fmt.Println(<-numChannel)
}

Output:

Program starting
fatal error: all goroutines are asleep - deadlock!

When receiving values from a channel, there are two values you can retrieve - the value being send, and a boolean on if the channel is still open. Closing a channel is a way for the sender to flag that no more values will be coming through, so the receiver can act accordingly (useful if using the range function). A channel is closed with the close function, passing in a variable to the channel.

func main(){
    fmt.Println("Program starting")

    numChannel := make(chan int, 10)

    numChannel <- 10
    close(numChannel)


    v,isOpen := <- numChannel
    fmt.Println(v, isOpen)

    v,isOpen = <- numChannel
    fmt.Println(v, isOpen)
}

Output: Program starting 10 true 0 false

or, if a function will return a series of values, you can use the range function which will end as soon as the channel is closed.

func main(){
    fmt.Println("Program starting")

    numChannel := make(chan int, 10)

    numChannel <- 10
    numChannel <- 20
    close(numChannel)


    for num := range numChannel {
        fmt.Println(num)
    }
    fmt.Println("No more reading from channel")


}

When using channels, the select statement can come in handy - which is typically used with a combination of for (before) and case (beneath) which is used to retrieve values from different channels that may have been set up. The select will block until one of the channels received a value. You may opt to have a default block to sleep between scans.

func chanHandler(numbers, quit chan int){
    //var x int
    numbers <- 5
    quit <- 1
}

func main(){
    fmt.Println("Program starting")

    numChannel := make(chan int)
    quitChannel := make(chan int)
    var numReceived int
    go chanHandler(numChannel, quitChannel)

    for {
        select {
        case numReceived = <-numChannel:
            fmt.Println("Received number", numReceived)
        case <-quitChannel:
            fmt.Println("Quit received")
            return
        default:
            //fmt.Println("Nothing received")
            time.Sleep(50 * time.Millisecond)
        }
    }
}

Output Program starting Received number 5 Quit received

Keep in mind, without the default condition, the select will block until a value is received.