Skip to content

Commit 6372901

Browse files
authored
Merge pull request #337 from flowchartsman/master
advent 2019 - Directional Channels in Go
2 parents 2c4721d + 64a495c commit 6372901

File tree

1 file changed

+196
-0
lines changed

1 file changed

+196
-0
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
+++
2+
title= "Directional Channels in Go"
3+
date = "2019-12-03T00:00:00+00:00"
4+
series = ["Advent 2019"]
5+
author = ["Andy Walker"]
6+
linktitle = "flag"
7+
+++
8+
9+
Go's [channels](https://tour.golang.org/concurrency/2) provide a primitive for typed, synchronous message passing. Combined with [goroutines](https://tour.golang.org/concurrency/1), they form the backbone of Go's [CSP](https://en.wikipedia.org/wiki/Communicating_sequential_processes)-inspired concurrency model. They're simple and expressive, but they can be difficult to use properly, especially if you need to control who can read from them or write to them.
10+
11+
## The Problem With Bidirectional Channels
12+
13+
Channels are normally declared with the `chan` keyword, followed by the **ElementType**, which represents the type of values passed on that channel. Together, these form the composite type for any value, which you can inspect with `%T`.
14+
15+
```go
16+
var stringChan chan string
17+
fmt.Printf("%T\n", stringChan) // "chan string"
18+
```
19+
[playground](https://play.golang.org/p/F58BWz2HJEZ)
20+
21+
This is the declaration format most people first encounter when working with channels. But any channel created in this way will be bidirectional as the default behavior. This means that anyone who has access to a channel value can read from it *and* write to it. This can cause problems in a concurrent environment, and many a Go programmer has torn hair from their heads trying to debug a `panic: send on a closed channel`.
22+
23+
The common wisdom is that only the sender should close a channel, and this makes sense. Only the sender can know when there's no more data to send, and it's the receiver's responsibility to watch for the close, or ideally, to simply `range` over the channel, exiting the loop naturally when it's done. If this order is upset, it's generally a sign something very wrong is going on, hence the panic. But if anyone can perform any action on a channel, including calling `close()`, how can you reel this in?
24+
25+
## Directional Channels
26+
27+
If you look at the [language spec](https://golang.org/ref/spec#Channel_types) for channels, it turns out that channel direction can actually be _constrained_!
28+
29+
> The optional <- operator specifies the channel *direction*, *send* or *receive*.
30+
31+
This means channels can actually be declared in one of three ways, depending on whether we want them to be bidirectional, receive-only or send-only:
32+
33+
```go
34+
var bidirectionalChan chan string // can read from, write to and close()
35+
var receiveOnlyChan <-chan string // can read from, but cannot write to or close()
36+
var sendOnlyChan chan<- string // cannot read from, but can write to and close()
37+
```
38+
39+
A good way to remember how this works is that, in declarations, the arrow indicates how the channel is allowd to be used:
40+
```
41+
<-chan // data only comes out
42+
chan<- // data only goes in
43+
```
44+
45+
At first glance, this might seem pretty useless --how useful is a new channel if it can't work in both directions?-- but there's another important line in the spec in the very same paragraph:
46+
47+
> A channel may be constrained only to send or only to receive by **assignment** or **explicit conversion**.
48+
49+
This means channels can start out bidirectional, but magically _become_ directional simply by assigning a regular channel to a variable of a constrained type. This is very useful for creating receive-only channels that no one can close but you.
50+
51+
## Receive-only Channels
52+
53+
```go
54+
var biDirectional chan string
55+
var readOnly <-chan string
56+
57+
biDirectional = make(chan string)
58+
59+
takesReadonly(biDirectional)
60+
readOnly = biDirectional
61+
```
62+
63+
`readOnly` now shares the same underlying channel, as `biDirectional`, but it cannot be written to *or* closed. This can also be done on the way into our out of a function, simply by specifying a direction in the argument or return type:
64+
65+
```go
66+
func takesReadonly(c <-chan string){
67+
// c is now receive-only inside the function and anywhere else it might go from here
68+
}
69+
70+
func returnsReadOnly() <-chan string{
71+
c := make(chan string)
72+
go func(){
73+
// some concurrent work with c
74+
}()
75+
return c
76+
}
77+
readOnly := returnsReadOnly()
78+
79+
```
80+
81+
This is a pretty nifty trick, and works a bit differently to conversions in the rest of the language, but, most crucially, the change in direction is reflected in the *type*, which means these restrictions can be enforced at *compile time*.
82+
83+
```go
84+
go func() {
85+
biDirectional <- "hello" // no problem
86+
close(biDirectional) // totally fine
87+
}()
88+
go func() {
89+
readOnly <- "hello" //"invalid operation ... (send to receive-only type <-chan string)"
90+
close(readOnly) //"invalid operation: ... (cannot close receive-only channel)"
91+
}()
92+
93+
fmt.Printf("%T\n", readOnly) // "<-chan string" (different type)
94+
fmt.Println(<-readOnly) // "hello" (same underlying channel!)
95+
```
96+
[playground](https://play.golang.org/p/y1xe8R9wQHK)
97+
98+
This is useful not only to control who can write to or close your channel, but also in terms of descriptiveness and Intentionality. One of the nice things about strongly-typed languages like Go is that they can be tremendously descriptive just through their API. Take the following function as an example:
99+
100+
```go
101+
func SliceIterChan(s []int) <-chan int {}
102+
```
103+
104+
Even without the documentation or implementation, this code unambiguously states that it returns a channel that the consumer is supposed to read from, either forever, or until it's closed (which documentation can help clarify). This lends itself very well to a **for-range** over the provided channel.
105+
106+
```go
107+
for i := range SliceIterChan(someSlice) {
108+
fmt.Printf("got %d from channel\n", i)
109+
}
110+
fmt.Println("channel closed!")
111+
```
112+
113+
Diving into the implementation, the function creates a bidirectional channel for its own use, and then all it needs to do to ensure that it has full control over writing to and closing the channel is to return it, whereupon it will be converted into a read-only channel automatically.
114+
115+
```go
116+
// SliceIterChan returns each element of a slice on a channel for concurrent
117+
// consumption, closing the channel on completion
118+
func SliceIterChan(s []int) <-chan int {
119+
outChan := make(chan int)
120+
go func() {
121+
for i := range s {
122+
outChan <- s[i]
123+
}
124+
close(outChan)
125+
}()
126+
return outChan
127+
}
128+
```
129+
[playground](https://play.golang.org/p/nGMksaNgxAg)
130+
131+
This is a very powerful technique for asserting control over a channel at an API boundary, and one that comes with no cost or need for explicit conversion, beyond simply specifying the channel direction in a declaration. This is so useful, you should use probably use it wherever you return a channel for reading from, unless there's a very good reason not to.
132+
133+
This is a similar approach to what the standard library does with tickers and timers in the `time` package:
134+
135+
```
136+
type Ticker struct {
137+
C <-chan Time // The channel on which the ticks are delivered.
138+
// Has unexported fields.
139+
}
140+
A Ticker holds a channel that delivers `ticks' of a clock at intervals.
141+
142+
func After(d Duration) <-chan Time
143+
After waits for the duration to elapse and then sends the current time on
144+
the returned channel. It is equivalent to NewTimer(d).C. The underlying
145+
Timer is not recovered by the garbage collector until the timer fires. If
146+
efficiency is a concern, use NewTimer instead and call Timer.Stop if the
147+
timer is no longer needed.
148+
```
149+
150+
Unlike the example above, neither timers nor tickers are ever closed to prevent erroneous firings, so dedicated `Stop()` methods are provided on both of these types, along with instructions on how to handle this situation correctly. This is another best practice around receive-only channels, and you should work to ensure that you provide similar mechanisms and instructions if there's any chance the consumer might want to stop reading from your channel early. Check out [Principles of designing Go APIs with channels](https://inconshreveable.com/07-08-2014/principles-of-designing-go-apis-with-channels/) by Alan Shreve for more on this topic.
151+
152+
## Send-only Channels
153+
154+
You can also declare channels as send-only, but these are of more limited use, at least to an API. While they can provide useful assertions internally that a channel is never read from, and you should do this when you can, receiving them with an API is kind of backwards, and you are generally better off using a bidirectional channel internally, and moderating channel writes with a function or method.
155+
156+
Send-only channels make only one appearance in the API of the standard library: in `os/signal`.
157+
158+
```
159+
func Notify(c chan<- os.Signal, sig ...os.Signal)
160+
Notify causes package signal to relay incoming signals to c. If no signals
161+
are provided, all incoming signals will be relayed to c. Otherwise, just the
162+
provided signals will.
163+
164+
Package signal will not block sending to c: the caller must ensure that c
165+
has sufficient buffer space to keep up with the expected signal rate. For a
166+
channel used for notification of just one signal value, a buffer of size 1
167+
is sufficient.
168+
169+
It is allowed to call Notify multiple times with the same channel: each call
170+
expands the set of signals sent to that channel. The only way to remove
171+
signals from the set is to call Stop.
172+
173+
It is allowed to call Notify multiple times with different channels and the
174+
same signals: each channel receives copies of incoming signals
175+
independently.
176+
177+
func Stop(c chan<- os.Signal)
178+
Stop causes package signal to stop relaying incoming signals to c. It undoes
179+
the effect of all prior calls to Notify using c. When Stop returns, it is
180+
guaranteed that c will receive no more signals.
181+
```
182+
183+
Here, the user is expected to pre-allocate an `os.Signal` channel for receiving incoming signals from the OS. The API asserts that the channel will only ever be written to, and informs the user that they need to create a buffered channel of whatever size they deem necessary to avoid blocking. It might seem necessary to take a send-only channel to allow the user to set their own channel depth, but the signature could just as easily have been something like:
184+
185+
```
186+
func Notify(depth uint, sig ...os.Signal) <-chan os.Signal
187+
```
188+
189+
Which returns a receive-only channel, similarly to how package `time` operates. The only difference is by taking a channel as an argument, package `os/signal` can keep track of the user's notify channels, allowing for the multiple calls it mentions to expand the set of signals the channel will receive, or calling `Stop()` to cease them. This is not possible without taking a channel as an argument, so in this case, a send-only channel is the way to go.
190+
191+
## Conclusion
192+
193+
Hopefully you have a better understanding of channel directions and how they might be used, and what they might express. Thanks for following along, Advent readers!
194+
195+
## About the Author
196+
Andy Walker (@flowchartsman) is a Go GDE and co-organizer of [Baltimore Go](https://www.meetup.com/BaltimoreGolang/). He is a programmer in security research for a major cybersecurity company, and enjoys hardware, 3D printing, and talking way too much about philosophy. He can be reached at andy-at-[andy.dev](https://andy.dev).

0 commit comments

Comments
 (0)