Home Creating pipelines using channels in Go
Post
Cancel

Creating pipelines using channels in Go

A Simple Pipeline

We start out with a book. We receive our books from a channel of type chan *Book.

1
2
3
4
5
6
7
8
type Book struct {
	Author  string
	Content string
}

func (b *Book) Print() string {
	return b.Content
}

There are two functions that are implemented against the book. One is printing the book and the second is saving the book into some sort of a data store. We will simplify that by just println. Note that save is our final step at which our program terminates if there are no more books (i.e. channel is closed)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func print(input chan *Book) chan *Book {
	output := make(chan *Book)
	go func() {
		defer close(output)
		for printable := range input {
			println("Printing:", printable.Print())
			output <- printable
		}
	}()
	return output
}

func saveBook(input chan *Book) {
	for book := range input {
		println("Saving book for author:", book.Author)
	}
}

We will simulate the input channel as

1
2
3
4
5
6
7
8
books := make(chan *Book)

go func() {
	defer close(books)
	for i := 0; i <= 1; i++ {
		books <- &Book{fmt.Sprintf("Author-%d", i), fmt.Sprintf("Content-%d", i)}
	}
}()

Our implementation is straight forward, and actually very elegant.

1
saveBook(print(books))

Yes, that’s it. Every step will do its thing then hand the book to the next step.

Working example in Go Playground

Printing Books and Magazines

Our printing house is having a new business opportunity. We are printing a Magazine

1
2
3
4
5
6
7
8
9
type Magazine struct {
	Author  string
	Content string
	Issue int
}

func (m *Magazine) Print() string {
	return m.Content
}

Magazine has its own specific saveMagazine function

1
2
3
4
5
func saveMagazine(magazines chan *Magazine) {
	for magazine := range magazines {
		println("Saving magazine for issue:", magazine.Issue)
	}
}

Here comes the tricky part. Whether it is a magazine or a book, we are using the same print mechanism. But our print(input chan *Book) chan *Book is useful only for books. We can copy and paste all our printing business, but that doesn’t sound like the best idea.

print doesn’t care if it is a magazine or a book, as long as it has a Print function. This brings to mind the keyword interface. Let’s create a Printable interface and make print depend on that instead.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Printable interface {
	Print() string
}

func print(input chan Printable) chan Printable {
	output := make(chan Printable)
	go func() {
		defer close(output)

		for printable := range input {
			println("Printing:", printable.Print())
			output <- printable
		}
	}()
	return output

}

This looks better. A print function only cares about printing. It doesn’t care about authors or issues! But Go will not get away so easy with it. Go wouldn’t let us cast channels. var books chan Printable = make(chan *Book) is illegal. There is no casting to make this happen and we will need to write our own conversion for any client that wants to use this function.

Let’s focus on books and magazine will be the same. We will create two conversion functions back and forth between Printable and Book channels

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func bookChanToPrintableChan(books chan *Book) (chan Printable) {
	printables := make(chan Printable)
	go func() {
		defer close(printables)
		for printable := range books {
			printables <- printable
		}
	}()
	return printables
}

func printableChanToBookChan(printables chan Printable) (chan *Book) {
	books := make(chan *Book)
	go func() {
		defer close(books)
		for printable := range printables {
			books <- printable.(*Book)
		}
	}()
	return books
}

our pipeline will look something like this:

1
saveBook(printableChanToBookChan(print(bookChanToPrintableChan(books))))

Working example in Go Playground

Thinking Outside The Box

We might not be able to cast from a channel of one type to a channel of another. But we can cast channels to a compatible type. Let’s see what we can do with that.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type Books chan *Book

func (b Books) Printables() chan Printable{
	printables := make(chan Printable)
	go func() {
		defer close(printables)
		for printable := range b {
			printables <- printable
		}
	}()
	return printables
}

type Printables chan Printable

func (p Printables) Books() chan *Book {
	books := make(chan *Book)
	go func() {
		defer close(books)
		for printable := range p {
			books <- printable.(*Book)
		}
	}()
	return books
}

We created two compatible types Books and Printables. Each of them has a method that converts them into the other type. If we had a magazine Printable would have had another method Magazines. Now we can cast chan *Book to Books. Our code can be rewritten using casting.

1
saveBook(Printables(print(Books(books).Printables())).Books())

Working example in Go Playground

There is still final compromise with declaring new types and using casting. That is using the new declared type directly. The downside with that, is we bring back a bit of coupling. On the channel type declaration, we are letting the Printables channel know about Books channel, which shouldn’t be its concern. However, I just brought that in to make all options available.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Books chan *Book

func (b Books) Printables() Printables {
	printables := make(Printables)
	go func() {
		defer close(printables)
		for printable := range b {
			printables <- printable
		}
	}()
	return printables
}

type Printables chan Printable

func (p Printables) Books() Books {
	books := make(Books)
	go func() {
		defer close(books)
		for printable := range p {
			books <- printable.(*Book)
		}
	}()
	return books
}

Now, save book will look something like this:

1
saveBook(print(books.Printables()).Books())

Working example in Go Playground

These are the three methods side by side. I think with better naming the first example looks more understandable since it highlights the left to right relationship. The last example is not too bad, except for the fact that we still have coupling.

1
2
3
saveBook(printableChanToBookChan(print(bookChanToPrintableChan(books))))
saveBook(Printables(print(Books(books).Printables())).Books())
saveBook(print(books.Printables()).Books())

The choice of which approach to take, will mostly depend on how many of these conversions one might have?

This post is licensed under CC BY 4.0 by the author.
Contents

Working effectively with legacy code

Hello Haskell, language introduction and cheat sheet

Comments powered by Disqus.