DEV Community

Quame Jnr
Quame Jnr

Posted on

Channels in Golang

#go

“Channels are a typed conduit through which you can send and receive values with the channel operator, <-“. This is the definition of channels in the Golang documentation but what exactly does that mean?

First, you can look at channels as some form of portal between two worlds, where you throw something through it from world A and it appears in world B.

Let’s take an example;

Imagine, we’re in a multiverse. We’re in WorldA and trying to send message to our other self in WorldB that our nemesis is coming.

We first create a channel using the make function. This will take chan as an argument to specify that it is a channel and the type will be the type of data we want to pass around. So we’ll create a channel that takes a string, “Our nemesis is coming” and pass the data through the portal. To pass a data through a portal, you use the expression: <-. The expression is like an arrow and the arrow direction is the direction of the data flow.

package main

import "fmt"

func main(){
    // Create our channel which we are naming portal
    portal := make(chan string)

    // This is the message we're sending to our other self in WorldB
    msg := "Our nemesis is coming" 

    // Message goes through portal
    portal <- msg

    // Message is received by our other self in World B
    worldBSelf := <- portal

    // We're going to print our message to see if everything is working well.
    fmt.Println(worldBSelf)
}
Enter fullscreen mode Exit fullscreen mode

Now when we run our code, we realise we have an error like the one below.

Output

❯ go run main.go
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
           Go/channels/main.go:9 +0x48
exit status 2
Enter fullscreen mode Exit fullscreen mode

So what is wrong? As I said earlier, think of a channel as a portal, it is just a conduit, just a passage, so if data is going in, then it’s coming out at the same time. This means our WorldBSelf needs to also be at the portal at the exact moment we’re sending the data through the portal so as to receive the data, otherwise we can’t send the data. Thus, both sending and receiving of data has to happen concurrently otherwise it fails and when we talk about concurrency in Golang, then we’re talking about goroutines. Let’s make some changes to our code.

package main

import "fmt"

func main(){
    // Create our channel which we are naming portal
    portal := make(chan string)

    // This is the data we're sending to our other self in WorldB
    msg := "Our nemesis is coming" 

    go func() {
    // Data goes through portal
        portal <- msg
    }()

    // Data is received by our other self in World B
    worldBSelf := <- portal

    // We're going to print our data to see if everything is working well.
    fmt.Println(worldBSelf)
}
Enter fullscreen mode Exit fullscreen mode

Output

Our nemesis is coming
Enter fullscreen mode Exit fullscreen mode

What we’ve done now is to turn the sending of our data into a goroutine so it runs concurrently with the receiving of our data. Now when we run our data, everything should be ok because our WorldBSelf is also present at the portal at the exact time we’re sending the data so he can receive it.

Channels are a good way to get data from goroutines as a goroutine is another thread that is running concurrently with the main thread. You can get data from the goroutine to your main thread through channels.

Let’s go through a practical example. Let’s say you’re trying to find the sum of elements in an array.

You can divide the array into two or three, spin up goroutines to find the sum of the individual sub arrays then get the result from each goroutine and add them together to get the total sum of the array.

package main 

import "fmt"

// calculate the sum of elements in an array
func calculateSum(numbers []int, channel chan int) {
    sum := 0
    for _, v := range numbers {
        sum += v
    }
    channel <- sum
}

func main(){
    // Create channel
    result:= make(chan int)

    array := []int{1,2,3,4,5,6,7,8,9,10}

    // Divide arrays into sub arrays
    firstSubArray, secondSubArray := array[:len(array)/2], array[len(array)/2:]

    // start goroutines
    go calculateSum(firstSubArray, result)
    go calculateSum(secondSubArray, result)

    // get result from goroutines
    x := <- result
    y := <- result

    // calculate total sum of the results from individual subarrays
    totalSum := x + y 
    fmt.Printf("x: %d, y: %d, total sum: %d\n", x, y, totalSum)
}

Enter fullscreen mode Exit fullscreen mode

Output

X: 40, y: 15, total sum: 55
Enter fullscreen mode Exit fullscreen mode

We divided our array into two sub arrays, we then put our calculateSum function into two goroutines, passing our sub arrays and channel as arguments. The result of those calculations are then passed through our channel to be received in the main thread and the total sum calculated.

Buffered Channels

Remember when I said we can’t send data through channels if we’re not sending and receiving the data concurrently? Technically, we can, with buffered channels. Buffered channel is just a channel that can temporarily store data. Look at it this way, since you and your WorldBSelf can’t always be at the same place at the same time to transfer data, you found a more asynchronous way to do that. You found out there is a locker that is also a portal to WorldB, which means whatever you put in that locker, your WorldBSelf can access that artefact when they are ready.

We can go to our very first code and add a buffer size to our channels to create this lockerPortal.

package main

import "fmt"

func main(){
    // Create our channel which we are naming portal
    lockerPortal := make(chan string, 1) // Add buffer size to channel

    // This is the message we're sending to our other self in WorldB
    msg := "Our nemesis is coming" 

    // Message goes through portal
    lockerPortal <- msg

    // Message is received by our other self in World B
    worldBSelf := <- lockerPortal

    // We're going to print our message to see if everything is working well.
    fmt.Println(worldBSelf)
}
Enter fullscreen mode Exit fullscreen mode

With these changes, our code should run ok. Note that the buffered size provided denotes the quantity of data the channel can take. A buffer size of 1 means only one data can be passed through the channel at a time.

We can try this by adding another message to the lockerPortal before reading our first data.

package main

import "fmt"

func main(){
    // Create our channel which we are naming portal
    lockerPortal := make(chan string, 1) // Add buffer size to channel

    // This is the message we're sending to our other self in WorldB
    msg1 := "Our nemesis is coming" // renamed to msg1
    msg2 := "He is already in your World" // new code

    // Message goes through portal
    lockerPortal <- msg1 //renamed to msg1
    lockerPortal <- msg2 // new code

    // Message is received by our other self in World B
    worldBSelf := <- lockerPortal
    worldBSelf := <- lockerPortal

    // We're going to print our message to see if everything is working well.
    fmt.Println(worldBSelf)
}
Enter fullscreen mode Exit fullscreen mode

When we run this we should have a deadlock error and the error is on the line we tried to add msg2 because we had exceeded our buffer size. If we rearrange the lines and put msg2 in our lockerPortal after receiving msg1, then it should work fine.

package main

import "fmt"

func main(){
    // Create our channel which we are naming portal
    lockerPortal := make(chan string, 1)

    // This is the message we're sending to our other self in WorldB
    msg1 := "Our nemesis is coming"
    msg2 := "He is already in your World"

    // Send msg1 and receive it
    lockerPortal <- msg1 
    worldBSelf := <- lockerPortal
    fmt.Println(worldBSelf)

    // Send msg2 and receive it
    lockerPortal <- msg2 
    worldBSelf := <- lockerPortal
    fmt.Println(worldBSelf)
}
Enter fullscreen mode Exit fullscreen mode

Output

Our nemesis is coming
He is already in your World
Enter fullscreen mode Exit fullscreen mode

There are more concepts in Golang channels like closing channels and using select statements but I hope this was a good introduction to channels.

Top comments (4)

Collapse
 
brockcaldwell profile image
Brock Caldwell

What would be a real-world example use case for this?

Collapse
 
quame_jnr1 profile image
Quame Jnr

It will be a great use case for any asynchronous task you would want to do as channels will be a great way to pass data from those Goroutines. Let's say you're trying to make a request to an API for some data but the API is paginated. You can create requests to all the individual pages and put them in goroutines to make multiple requests concurrently to all the individual pages to get your data but when you do that then you're not expecting a return at the moment so you can't get return values from go routines. Thus, you can create a channel and pass the data through it so you can aggregate your data. You're basically saying, go on and do your own thing and when you done just pass the data through this channel so I can use it.

Collapse
 
mikey247 profile image
Michael Umeokoli

Great read man.

Collapse
 
quame_jnr1 profile image
Quame Jnr

Thank you