Lindgren Tech

Some ramblings, some stuff, most of it not useful. [+] Menu


[Random thoughts about Go Channels]

Random thoughts about Go Channels

So in my never ending quest for learning the teeny little bits about GoLang since it seems like that is the direction my career is going.

(tl;dr: first it was Java, then it was Rails, now it seems Go is the new "good to learn because every company uses it" language)

So far I've really enjoyed these things about Go:

  1. Being able to attach methods to structs, making it sort of Object Oriented, but not really at the same time. It's a nice blend of procedural and OO.
  2. Functions are first-class citizens, e.g. I can pass functions around as arguments or declare a type as a function signature (see: https://pkg.go.dev/net/http#HandlerFunc).
  3. Channels! (today's topic)

Channels are a pretty highly debated topic in the Go community, the most notable complaints (that I've thought about at least) being the difference between a buffered/unbuffered channel and how that blocks sending/receiving with the channel.

Before going into it I'll explain the difference between the two.

Buffered Channels

A buffered channel is a channel that only has x number of slots, which are set when the channel is declared. e.g: ch := make(chan int, 3) declares a channel with 3 slots in it. It can be looked at almost like a stack, it does not block until the channel is full. One can produce asynchronously until the channel is full.

Here is a code example illustrating this https://play.golang.org/p/vBjzgqhIiaZ I'll put it inline here too:

package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	// Make a channel with 2 slots
	ch := make(chan int, 2)
	go consume(ch)

	for i := 0; i < 5; i++ {
		fmt.Fprintf(os.Stdout, "producing int %d\n", i)
		ch <- i
	}

	// sleeping to let the last 2 get consumed.
	time.Sleep(500 * time.Millisecond)
}

func consume(ch chan int) {
	fmt.Println("sleeping 3 sec...")
	time.Sleep(3 * time.Second)
	for {
		fmt.Fprintf(os.Stdout, "Got %d\n", <-ch)
	}
}
Code illustrating reading/writing to a buffered channel

Basically this program does a few things:

  1. Creates a buffered channel with 2 slots
  2. Starts a consuming goroutine to consume from the channel
  3. Produces 5 messages to the channel, outputting when it is trying to produce
  4. The sub-thread consumes as it can, but the main thread can only produce two ints before hanging on the third
  5. The sub-thread consumes after three seconds and the program runs to completion

Now this seems pretty simple since it is like writing to a stack or queue that has a limit. Things start to get interesting once we move onto...

Unbuffered Channels

An unbuffered channel is much the same as a buffered channel minus the fact that it blocks on consume until the message is received for every message (unless the channel is closed). This can be looked at as a pipe that can just continually be written to or read from, with

Here is a code example again, https://play.golang.org/p/ODGQpNfv2lo, but I will put it inline as well:

package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	ch := make(chan int)
	go consume(ch)

	for i := 1; i <= 5; i++ {
		fmt.Fprintf(os.Stdout, "producing int %d\n", i)
		ch <- i
	}

	// sleeping to let the last 2 get consumed.
	time.Sleep(500 * time.Millisecond)
}

func consume(ch chan int) {
	count := 1
	for ; ; count++ {
		if count%3 == 0 {
			fmt.Println("TOO FAST, taking a break. Sleeping 3 seconds")
			time.Sleep(3 * time.Second)
		}
		fmt.Fprintf(os.Stdout, "Got %d\n", <-ch)
	}
}
Code illustrating reading/writing to an unbuffered channel

This one is a bit harder to understand so it may be better to just run and inspect the output as you read the steps I have below.

  1. Like before, making a channel (unbuffered) and starting a consumer goroutine.
  2. Immediately start producing to the messages, the consumer thread receives 2 of them, then decides to take a break for 3 seconds, which causes the main thread to hang.
  3. Once the sleeping is done the consumer catches up and consumes everything left by the main thread as the loop runs to completion.

Personally, unbuffered channels are much easier to understand IMO. If one goroutine produces a message to a channel, it hangs until a different goroutine picks it up. Easy.

Unbuffered channels are the nicest from the point of view that they can have multiple routines consuming them, so it is easy to scale batch processing etc. Here is a code example where I spin up five workers to consume off of the same channel! https://play.golang.org/p/xeXb_1a7YUn

package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	ch := make(chan int)
	go consumeMultiple(1, ch)
	go consumeMultiple(2, ch)
	go consumeMultiple(3, ch)
	go consumeMultiple(4, ch)
	go consumeMultiple(5, ch)

	for i := 1; i <= 5; i++ {
		fmt.Fprintf(os.Stdout, "producing int %d\n", i)
		ch <- i
	}

	time.Sleep(500 * time.Millisecond)
}

func consumeMultiple(id int, ch chan int) {
	fmt.Fprintf(os.Stdout, "Consumer %d got %d\n", id, <-ch)
}
Consuming from the same channel from different workers

So what was the point of this blog post other than a channel tutorial?

Anyways, thanks for reading my ramblings. I'm over 1k words already so I should probably end this one here!

Written by Jacob Lindgren. Published on .