marvin
By Marvin Desmond
Posted 30th March 2023

So what are GoRoutines and Channels?

A goroutine is a lightweight thread managed by the Go runtime.

Channels is how you communicate between routines.

This brings us to another concept, concurrency and parallelism.

Concurrency is the task of running and managing the multiple computations at the same time. While parallelism is the task of running multiple computations simultaneously. Concurrency is about multiple tasks which start, run, and complete in overlapping time periods, in no specific order. Parallelism is about multiple tasks or subtasks of the same task that literally run at the same time on a hardware with multiple computing resources like multi-core processor.

So what are some benefits:
Faster processing ~ The benefit is getting tasks done faster. Imagine that you are searching a computer for files, or processing data, if it’s possible to work on these workloads in parallel, you end up getting the response back faster.
Responsive apps ~ Another benefit is getting more responsive apps. If you have an app with a UI, imagine it would be great if you can perform some background work without interrupting the responsiveness of the UI.

Finally ~ GoRoutines run on concurrency. Remember that structuring your program to run concurrently can enable parallelism. This gives your program the potential to run more quickly. But parallelism is not the goal of concurrency. You can certainly have concurrency without parallelism.

Now let's implement a basic program implementing a GoRoutine. This is done by adding go to just before a Go function when calling it.
package main

import "fmt"

func printValue(prefix string) {
    for i := 0; i < 10; i++ {
        fmt.Printf("%s : %d\n", prefix, i)
    }
}

func main() {
    go printValue("GoRoutine")
    printValue("Normal")
    fmt.Println("Hello World")
}
            [Golang]$ go run goroutines.go
Normal : 0
Normal : 1
Normal : 2
Normal : 3
Normal : 4
Normal : 5
Normal : 6
Normal : 7
Normal : 8
Normal : 9
Hello World
[Golang]$
          
Wait? What! But the GoRoutine (go printValue) has not printed its value. Why is that? Well, interesting questions have interesting answers. And I have an answer for just that.

In reality, even the main Go function is a goroutine, and so when the printValue goroutine starts to executes, it starts later and on its own thread, independent of the main program. So when the main program ends, it does not check whether there are other goroutines running, since they're independent. To allow the goroutine to run, a crude way is to add a time delay on the main function.
package main

import (
    "fmt"
    "time"
)

func printValue(prefix string) {
    for i := 0; i < 10; i++ {
        fmt.Printf("%s : %d\n", prefix, i)
    }
}

func main() {
    go printValue("GoRoutine")
    printValue("Normal")
    fmt.Println("Hello World")
    time.Sleep(2 * time.Second)
}
            [Golang]$ go run goroutines.go
Normal : 0
Normal : 1
Normal : 2
Normal : 3
Normal : 4
Normal : 5
Normal : 6
Normal : 7
Normal : 8
Normal : 9
Hello World
GoRoutine : 0
GoRoutine : 1
GoRoutine : 2
GoRoutine : 3
GoRoutine : 4
GoRoutine : 5
GoRoutine : 6
GoRoutine : 7
GoRoutine : 8
GoRoutine : 9
[Golang]$
          
A better way is to use the data type WaitGroup from the package sync to ensure that the goroutine is done before ending the main program goroutine so to say.

First thing is to initialize a WaitGroup. A WaitGroup is just a counter to track goroutines. You add 1 to the WaitGroup if you have a goroutine to wait for. When the goroutine finishes, you decrement the counter.

Let's therefore call the printValue and the WaitGroup decrementor in an anonymous function that is also goroutine in itself, replacing the printValue goroutine with a normal function. Now you call wg.Wait at the end of the main function to block until the WaitGroup counter is zero.
package main

import (
    "fmt"
    "sync"
)

func printValue(prefix string) {
    for i := 0; i < 10; i++ {
        fmt.Printf("%s : %d\n", prefix, i)
    }
}

func main() {
    var wg sync.WaitGroup

    wg.Add(1)
    go func(){
        printValue("GoRoutine")	
        wg.Done()		
    }()
    printValue("Normal")
    fmt.Println("Hello World")

    wg.Wait()
}
            [Golang]$ go run goroutines.go
Normal : 0
Normal : 1
Normal : 2
Normal : 3
Normal : 4
Normal : 5
Normal : 6
Normal : 7
GoRoutine : 0
GoRoutine : 1
GoRoutine : 2
GoRoutine : 3
GoRoutine : 4
Normal : 8
Normal : 9
Hello World
GoRoutine : 5
GoRoutine : 6
GoRoutine : 7
GoRoutine : 8
GoRoutine : 9
[Golang]$
          
Goroutines run in the same address space, so access to shared memory must be synchronized. The sync package provides useful primitives, although you won't need them much in Go as there are other primitives. (See the next slide.)
Channels
From where we've reached, it's been really simple, but not massively useful without a way for the GoRoutines to communicate. This can be achieved with channels.

Channels are the pipes that connect concurrent goroutines. You can send values into channels from one goroutine and receive those values into another goroutine using the channel operator, <-. So, instead of now printing our values in the goroutine directly to the terminal, the results should be communicated back to the main goroutine. We want to bind them using a channel.
But let's start simple, goroutine to goroutine via a channel.
package main

import "fmt"

func main () {
    message := make(chan string)
    go func() { message <- "ping" }()
    msg := <- message
    fmt.Println(msg)
}
Create a new channel with make(chan val-type). Channels are typed by the values they convey.
messages := make(chan string)
Send a value into a channel using the channel <- syntax. Here we send "ping" to the messages channel we made above, from a new goroutine.
go func() { messages <- "ping" }()
The <-channel syntax receives a value from the channel. Here we’ll receive the "ping" message we sent above and print it out.
msg := <-messages
fmt.Println(msg)
When we run the program the "ping" message is successfully passed from one goroutine to another via our channel.

By default sends and receives block until both the sender and receiver are ready. This property allowed us to wait at the end of our program for the "ping" message without having to use any other synchronization.
            [Golang]$ go run channels.go
ping
[Golang]$
          
So onto our previous program. Implementing it using (unbuffered) channel, the resulting codebase becomes:
package main

import "fmt"

func goRoutinePrint(prefix string, c chan string) {
    for i := 0; i < 10; i++ {
        c <- fmt.Sprintf("%s : %d\n", prefix, i)
    }
    close(c)
    // only the sender should close a channel, not the receiver
}

func normalPrint(prefix string) {                          
    for i := 0; i < 10; i++ {                                                
            fmt.Printf("%s : %d\n", prefix, i)                             
    }                                                                        
}

func main() {
    diff_c := make(chan string)
    go goRoutinePrint("GoRoutine", diff_c)
    normalPrint("Normal")
    for msg := range diff_c {
    fmt.Print(msg)		
    }	
    fmt.Println("Hello World")
}
Points to note:
~ As the channel receiver receives data from the other goroutine, a for loop is used to access them. This for loop ends when the sender closes the channel.
~ fmt.Sprintf is used to format string data and dynamic data and bind its output to the channel
            [Golang]$ go run goroutines.go       
Normal : 0
Normal : 1
Normal : 2
Normal : 3
Normal : 4
Normal : 5
Normal : 6
Normal : 7
Normal : 8
Normal : 9
GoRoutine : 0
GoRoutine : 1
GoRoutine : 2
GoRoutine : 3
GoRoutine : 4
GoRoutine : 5
GoRoutine : 6
GoRoutine : 7
GoRoutine : 8
GoRoutine : 9
Hello World
[Golang]$
          
Buffered channels
In the previous codebase, the concept of channels was unbuffered. So what are buffered channels?

To understand buffered channels, let's get to know unbuffered channels. By default channels are unbuffered, meaning that they will only accept sends (chan <-) if there is a corresponding receive (<- chan) ready to receive the sent value. Buffered channels accept a limited number of values without a corresponding receiver for those values.
So why is this? When a channel sends and receives on a single goroutine (for instance, the main goroutine), since it runs on a single thread, the send will block and the goroutine will not proceed since it's waiting for the receiver which will never be met because the goroutine cannot continue. This results in goroutine deadlock. This is where buffered channels prove helpful.
package main 

import "fmt"

func main () {
    c := make(chan string, 2)
    c <- "Hello"
    c <- "World"
    /* if you try to send/receive a third value in the channel
    it will be unbuffered and result in goroutine deadlock
    So you have to increase the limit of the buffered channel
    to account for that.
    Go captures this issue at runtime, not compile time */

    // EITHER
    msg := <-c
    fmt.Println(msg)
    msg = <-c
    fmt.Println(msg)
    /* OR 
    fmt.Println(<-c)
    fmt.Println(<-c)
    */ 
}
Select statement
Suppose there are two GoRoutines that send data on channels, by default unbuffered, at different time intervals. And within the main goroutine, the data is to be received from both the channels and the data printed. The channel overall speeds will depend on the slower channel, as it will block the main goroutine everytime till data is received. A select statement allows us to receive from whichever channel is ready.
package main

import (
    "fmt"
    "time"
)

func main() {
    c1 := make(chan string)
    c2 := make(chan string)
    go func() {
        for {
            c1 <- "Every 500ms"
            time.Sleep(500 * time.Millisecond)
        }
    }()

    go func() {
        for {
            c2 <- "Every two seconds"
            time.Sleep(2 * time.Second)
        }
    }()

    for {
        select {
        case msg1 := <-c1:
            fmt.Println(msg1)
        case msg2 := <-c2:
            fmt.Println(msg2)
        }
    }
}
            [Golang]$ go run channels.go 
Every two seconds
Every 500ms
Every 500ms
Every 500ms
Every 500ms
Every two seconds
Every 500ms
Every 500ms
Every 500ms
Every 500ms
Every two seconds
Every 500ms
Every 500ms
Every 500ms
Every 500ms
Every two seconds
Every 500ms
Every 500ms
Every 500ms
Every 500ms
Every two seconds
Every 500ms
Every 500ms
Every 500ms
^Csignal: interrupt
[Golang]$
          
Without select statement, this would be the codebase:
package main

import (
    "fmt"
    "time"
)

func main() {
    c1 := make(chan string)
    c2 := make(chan string)
    go func() {
        for {
            c1 <- "Every 500ms"
            time.Sleep(500 * time.Millisecond)
        }
    }()

    go func() {
        for {
            c2 <- "Every two seconds"
            time.Sleep(2 * time.Second)
        }
    }()

    for {
    fmt.Println(<-c1)
    fmt.Println(<-c2)
    }
}
And this would be the output, resulting in a blocking operation:
            [Golang]$ go run channels.go
Every 500ms
Every two seconds
Every 500ms
Every two seconds
Every 500ms
Every two seconds
Every 500ms
^Csignal: interrupt
[Golang]$
          
I guess that's it for now!