Behaviors of Channels

By William Kennedy (speaker) on November 6, 2017

I learned over time that it’s best to forget about how channels are structured and focus on how they behave. So now when it comes to channels, I think about one thing: signaling. A channel allows one goroutine to signal another goroutine about a particular event. Signaling is at the core of everything you should be doing with channels. Thinking of channels as a signaling mechanism will allow you to write better code with well defined and more precise behavior.

Note: This post was live-blogged at dotGo 2017. Let us know on Twitter (@srcgraph) if we missed anything. All content is from the talk; any mistakes or misrepresentations are our fault, not the speaker's.

Setup

Writing a service for a TV, but the tv stream fails because you can't write logs anymore. To simulate this we set up a custom io.Writer to simulate problems that can happen called device. So we use the log package to log, but to our custom io.Writer device. When running the code the device fails and the whole app fails because the calls to log.Println are blocking.

Bill then proceded to do live coding for a custom Logger which solves our problem, below is the code he wrote annotated with what he was explaining as he wrote it:

package logger

import (
	"fmt"
	"io"
	"log"
	"sync"
)

type Logger struct {
	ch chan string
	wg sync.WaitGroup
}

// New good practice to create a factory function to create Logger. Important
// to use a buffered channel so we can implement the "Drop Pattern". So we
// have a cap parameter.
func New(w io.Writer, cap int) *Logger {
	l := Logger{
		ch: make(chan string, cap),
	}

	// The goroutine to consume ch
	// Waitgroup is to manage the below goroutine
	l.wg.Add(1)
	go func() {
		// important to note everything we are doing is just the core
		// langauge, not even the stdlib.
		for v := range l.ch {
			fmt.Fprintf(w, v)
		}
		l.wg.Done()
	}()

	return &l
}

func (l *Logger) Stop() {
	// We can just close the channel and wait for the consuming goroutine to
	// be done.
	close(l.ch)
	l.wg.Wait()
}

func (l *Logger) Println(s string) {
	// Non-blocking send. Because we have the default case we will not block
	// if `l.ch` is at capacity, and just drop the log message.
	select {
	case l.ch <- s:
	default:
		fmt.Println("DROP")
	}

	// ^^ Bill loves this piece of code, because how the primitives in the
	// languages are really concise but powerful for this problem.
}

// in main.go
func main() {
	// Before this would fail because something would block on device, when
	// using the stdlib logger
	var d device
	l := log.New(&d, "", 0)

	// After we use our custom logger, and the app doesn't fail when device is
	// backed up.
	var d device
	l := /*logger.*/ New(d, 10)
}

The important part above is the Drop Pattern implemented in Logger.Println, since it will prevent the application being blocked due to device blocking.