Grokking Concurrency in Go
Vote

0% completed

​

Channel Direction in Function Signatures

Three Channel Types

Implicit Conversion from Bidirectional

Direction in Function Signatures

Mistakes the Compiler Catches

Reading Nested Channel Types

Choosing a Direction

Key Takeaways

The function sum below receives three numbers from one channel and is meant to report their total on a second channel. Its author typed the wrong channel name on the last line, so the total goes back into nums instead of into results.

Go
Go

. . . .

The program compiles, and it fails only when it runs. The goroutine waits to send on nums, which nobody reads any more, and main waits to receive on results, which nobody writes. With both goroutines blocked, the runtime stops the program:

fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: main.main() goroutine 7 [chan send]: main.sum(...)

Now change only the first line of sum, so that its parameters say which way each channel is used:

func sum(nums <-chan int, results chan<- int, n int) {

The same mistake no longer compiles, and the compiler reports the exact line:

./main.go:12:2: invalid operation: cannot send to receive-only channel <-chan int nums (variable of type <-chan int)

The two versions differ in one line, yet one fails at run time and the other fails at build time. What follows explains what those arrows in a channel type mean, which conversions Go allows between them, and which mistakes they turn into compile errors.

Three Channel Types

A channel type may state a direction. A channel that can be used both to send and to receive values of type T is written chan T, and it is called bidirectional. Go has two more channel types for the same element type, and each allows only one of the two operations.

A channel value through which the code may only send has the type chan<- T, and it is called a send-only channel. A channel value from which the code may only receive has the type <-chan T, and it is called a receive-only channel. The Go specification calls both of them directional.

The arrow shows which way values move. In chan<- T, the arrow points into the word chan, so values go into the channel. In <-chan T, the arrow comes out of the word chan, so values come out of it. The same arrow appears in the send statement ch <- v and the receive expression <-ch, so the type and the operation read the same way.

TypeSend ch <- vReceive <-chTypical use
chan TYesYesWhere the channel is made
chan<- TYesNoParameter of a function that produces values
<-chan TNoYesParameter of a function that consumes values

Implicit Conversion from Bidirectional

A directional channel is not a separate channel. It is a restricted view of an existing one, and the restriction exists only in the type that the compiler checks. The Go specification says that a channel may be constrained to send only or to receive only by assignment or by explicit conversion.

In practice the assignment happens without any extra code. A value of type chan T can be assigned to a variable of type chan<- T or <-chan T, and it can be passed to a parameter of either type. The program below makes one buffered channel and creates both restricted views of it.

Go
Go

. . . .

Output:

chan string | <-chan string | chan<- string same channel: true true len through each view: 1 1 1 page from https://example.com/a

The three variables have three different types, but they compare as equal to the original, because they hold the same channel. A value sent through out is in the buffer that len reports through every view, and it comes out through in. The direction changes what the compiler lets each piece of code do with the channel. It does not copy the channel or change how it behaves at run time.

The conversion works in one direction only. A bidirectional channel can become a directional one, but a directional channel can never become bidirectional again, not even with an explicit conversion. A send-only value also cannot become receive-only. These are the compiler's messages for each attempt, taken from one build:

cannot use in (variable of type <-chan int) as chan int value in variable declaration cannot convert in (variable of type <-chan int) to type chan int cannot use in (variable of type <-chan int) as chan<- int value in variable declaration

This one-way rule is what makes the restriction reliable. A function that receives a <-chan int has no way to get a version of it that can send.

Direction in Function Signatures

The usual place for a direction is a function parameter. The code that makes a channel keeps the bidirectional value and passes it to each function. Each function declares only the operation it needs, and Go applies the conversion at the call.

Here is the summing program from the start, written with directions and with the mistake fixed. It also adds a producer function, send, which takes a send-only channel.

Go
Go

. . . .

Output:

total: 60

The signature of each function now documents its role. A reader can see from send(out chan<- int, ...) that send only produces values, and from sum(nums <-chan int, results chan<- int, n int) that sum reads one channel and writes the other. Only main holds the bidirectional channels, because only main connects the two functions.

One channel, two restricted views
One channel, two restricted views

Mistakes the Compiler Catches

Once a parameter has a direction, every use of the opposite operation in that function is a compile error. These are the messages Go 1.27 prints for the common cases, copied from a build of one test file:

CodeCompiler message
out <- 1 where out is <-chan intcannot send to receive-only channel
v := <-in where in is chan<- intcannot receive from send-only channel
for v := range out where out is chan<- intcannot range over out ... receive from send-only channel
in == out with opposite directionsmismatched types <-chan int and chan<- int

Each of these mistakes would compile with plain chan int parameters. Some would then deadlock, as the opening program did. Others would do something worse: a consumer that sends a value back into its own input channel might have that value read by another consumer. Nothing would fail, and the program would simply produce a wrong result.

Reading Nested Channel Types

A channel can carry other channels, and then the position of the arrow matters. The specification says that the <- operator belongs to the leftmost chan it can belong to. Parentheses make the other grouping explicit.

var a chan<- chan int // send-only channel that carries chan int values var b chan<- <-chan int // send-only channel that carries <-chan int values var c <-chan <-chan int // receive-only channel that carries <-chan int values var d chan (<-chan int) // bidirectional channel that carries <-chan int values

Types like these appear when one goroutine hands another a channel on which to reply. They are uncommon, and when one appears, a named type or a comment that states the grouping helps the next reader.

Choosing a Direction

The usual rule is that the function that makes a channel keeps the type chan T, and every function that only sends or only receives declares the matching direction. A bidirectional parameter is then a signal to the reader that the function really does both operations, which is rare.

The direction costs nothing at run time. It is a check performed while compiling, and the compiled program uses the same channel either way. The benefit is that a wrong operation becomes a build error, which is reported before the program runs and names the exact line.

Key Takeaways

  • chan<- T allows only sends and <-chan T allows only receives; the arrow shows which way values move.
  • A chan T value converts automatically to either direction when it is assigned or passed as an argument.
  • A directional channel can never be converted back to bidirectional, not even explicitly.
  • Every view refers to the same channel; direction is checked by the compiler and has no run-time cost.
  • Declare the direction on every parameter that only sends or only receives, so that the opposite operation is a compile error.

A channel's direction in a function signature states which operation that function performs on the channel. The compiler checks every use against that statement, so a send in the wrong place is reported when the program is built, not when it deadlocks or produces a wrong result.

Reading Progress

0%


Vote for new content

On This Page

Three Channel Types

Implicit Conversion from Bidirectional

Direction in Function Signatures

Mistakes the Compiler Catches

Reading Nested Channel Types

Choosing a Direction

Key Takeaways