DEV Community

chris
chris

Posted on • Updated on

How to bufferize data in Go channels and act upon it

This small tutorial will look into how to use Go channels as buffers and how to write code that will react on the type of data in that channel.

Use cases:

  • streaming data
  • receive errors

There are probably other use cases, feel free to add yours in the comments! I'd be interested in how you use channels.

What are Go channels?

"Go By Example" is an amazing website for quick and practical Go tutorials and they have a couple on
channels:

You will need this one if you want to work with channels

Go channel are one of the best feature of Go, they allow you to run parallelized processes to your code, as bit like saying "when I run the main code, please run this at the same time". It's like cooking rice and your sauce at the same time. And the bester part is that you can make them communicate easily!

Crossing the Channel

Let's get our first process running! We will create an infinite loop that will print something.

package main

import (
    "fmt"
    "time"
)

func main() {
    for {
        // let's make the loop slower for our Human eyes
        time.Sleep(2 * time.Second)
        fmt.Println(time.Now)
    }
}


Enter fullscreen mode Exit fullscreen mode

Then we need to create a channel that will receive that data. For that we will use the go call that will launch a function in parallel.


func firstChannel() {
          for {
              // let's make the loop slower for our Human eyes
              time.Sleep(2 * time.Second)
              fmt.Println(time.Now)
          }

}

func main() {
          go firstChannel()
}
Enter fullscreen mode Exit fullscreen mode

At that point there isn't much you will be able to see, the code will look the same for you but the firstChannel() function is going to run in parallel of main().

Let's make a channel and send some data to main so it can react.

package main

import (
    "fmt"
    "log"
    "time"
)

var (
    // Here we declare a global channel so we can share it with different functions.
    OutputChannel chan time.Time
)

// This function is a small Go trick to launch some functions before the execution of main(), here we use it to initialize our OutputChannel.
func init() {
    OutputChannel = make(chan time.Time)
}

func firstChannel() {
    for {
        time.Sleep(2 * time.Second)
        data, err := time.Now()
        if err != nil {
            log.Fatal(err)
        }
        OutputChannel <- data
    }
}

func main() {
    go firstChannel()

    // Infinite loop that will listen if there is data in our channel and will
    // print something if there is data received.
    for {
        select {
        case data <- OutputChannel:
            fmt.Println("We received:", data)
        default:
            continue
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

And voila! We are sending data from a channel to another and printing the result.

Ok, this is fun, would it be possible to act on the data itself based on specific parameters? Of course! It is what the select and switch operators are for.

Let's modify a bit our code, we want a code that will count to 100 and return "Even" if the number is even and "Odd" if the number is odd.

package main

import (
    "fmt"
    "time"
)

var (
    OutputChannel chan int
)

func init() {
    OutputChannel = make(chan int)
}

func firstChannel() {
    for i := 0; i <= 100; i++ {
        time.Sleep(1 * time.Second)
        OutputChannel <- i
    }
}

func main() {
    go firstChannel()

    for {
        // Receiving data from our channel.
        data := <-OutputChannel

        // Using the modulo as condition.
        switch data % 2 {
        case 0:
            fmt.Println("This is an even number:", data)
        default:
            fmt.Println("This is an odd number", data)
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

What about over-engineering our code and send our odd number to see if there are prime but this in another function?

package main

import (
    "fmt"
    "math/big"
)

var (
    OutputChannel chan int
)

func init() {
    OutputChannel = make(chan int)
}

func firstChannel() {
    // We removed the sleep part to have the code run faster.
    for i := 0; i <= 100; i++ {
        OutputChannel <- i
    }
}

func isPrime(number int) bool {
    // As we are lazy, we are just going to use the built-in function for finding the prime.
    if big.NewInt(int64(number)).ProbablyPrime(0) {
        fmt.Println("Wow, we found a prime number! Here it is:", number)
        return true
    }

    return false
}

func main() {
    go firstChannel()

    for {
        // receiving data from our channel
        data := <-OutputChannel

        // using the modulo as condition
        switch data % 2 {
        case 0:
            fmt.Println("This is an even number:", data)
        default:
            go isPrime(data)
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

So if you are running this code and your didn't really wait till 100 and closed it with CTRL-C, you probably realized two things.

  • 1) The code works and isPrime returns the number after the count of the next number, especially when the count of the number start to be large. This is the behaviour we wanted, an unsynced action for something. You could use that in case you want to use your channels for managing errors and keep your main process running.
Wow, we found a prime number! Here it is: 83
This is an even number: 84
This is an even number: 86
This is an even number: 88
This is an even number: 90
Wow, we found a prime number! Here it is: 89  <= Ha ha!
This is an even number: 92
This is an even number: 94
This is an even number: 96
This is an even number: 98
This is an even number: 100
Wow, we found a prime number! Here it is: 97  <= Ha ha (again)!
Enter fullscreen mode Exit fullscreen mode
  • 2) Our code breaks at the end because we didn't close the channel properly, so it would be necessary to send our program we are done counting.

We are already using default on switch and select to have non blocking channels, it means that they are going to keep listening even if no data is received. Otherwise your channel will block and not as intended here.

Usually, if you want to close Go channels you have few options, you can send a close(channelX)to your channelX or you can create a Go channel getting a Boolean and using that as a plug you'd pull (see the example here.

As we are having fun with channels, let's use the channel option. We are going to create a killswitch channel that will receive a message when the counter reaches 100. Remember that Go is falsy, it means that a default Boolean is false, you need to explicitly set it to true. Some languages, such as Ruby, have the opposite philosophy.

So here we have our final code:

package main

import (
    "fmt"
    "math/big"
)

var (
    OutputChannel chan int
    KillSwitch    chan bool
)

func init() {
    OutputChannel = make(chan int)
    KillSwitch = make(chan bool)
}

func firstChannel() {
    for i := 0; i <= 100; i++ {
        OutputChannel <- i
                // When i reaches 100, we change the switch to true.
        if i == 100 {
            KillSwitch <- true
        }
    }
    // We are adding a return at the end of the first channel that will be triggered
    // when the function as counted to 100.
    return
}

func isPrime(number int) bool {
    // As we are lazy, we are just going to use the built-in function for
    // finding the prime.
    if big.NewInt(int64(number)).ProbablyPrime(0) {
        fmt.Println("Wow, we found a prime number! Here it is:", number)
        return true
    }

    return false
}

func main() {
    go firstChannel()

    for {
        select {
        case data := <-OutputChannel:
            // using the modulo as condition
            switch data % 2 {
            case 0:
                fmt.Println("This is an even number:", data)
            case 1:
                go isPrime(data)
            default:
                continue
            }
        case <-KillSwitch:
            fmt.Println("Done!")
            return
        default:
            continue
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Some closing thoughts

https://unsplash.com/photos/9AxFJaNySB8

Thank you for reading so far in this long blog post! This example is quite simple but I wanted to demonstrate the power of channels and how you can built resilient parallelized application with Go. It is one of the main feature of this language, why not using it extensively. Of course, I have still a lot to learn and would be really interested in your thoughts on this matter.

I focused here on channels and not so much on streaming but you can only how we can work on a specific stream of data (here our counting function) and build business logic on it.

Thanks for reading, I hope you enjoyed it!

Top comments (0)