Channel Direction

Writing a parameter as chan<- or <-chan states whether a function may send or receive, and the compiler enforces it.

Syntaxfunc worker(in <-chan Job, out chan<- Result)

A channel type can be narrowed to one direction. chan<- int can only be sent on. <-chan int can only be received from. Plain chan int does both.

Read the arrow as the direction a value moves relative to the word chan. In chan<- int the value goes into the channel, so it is send-only. In <-chan int the value comes out, so it is receive-only.

Where you write it

Almost always on a function's parameters and its return types. make always produces a two-way channel, and you narrow it at the boundary. A function taking chan<- int is documenting that it produces values. One taking <-chan int is documenting that it consumes them. The direction becomes part of the signature, so a reader knows which way the data flows without reading the body.

The narrowing happens automatically on assignment and on a function call. The long form is an explicit conversion, (chan<- int)(ch), and it means exactly the same thing. You will rarely need to write it.

What it buys you, and what trips people up

Mistakes become compile errors rather than runtime panics. Receiving from a send-only channel gives invalid operation: cannot receive from send-only channel. Closing a receive-only one gives cannot close receive-only channel. Closing a send-only channel is allowed, because closing is a sender's job.

The narrowing is one-way. There is no conversion from chan<- int back to chan int, so a function handed a direction cannot widen it again.

Example

Example · go
package main

import "fmt"

// out is send-only. Writing <-out inside this function fails to compile.
func produce(out chan<- int) {
	for i := 1; i <= 3; i++ {
		out <- i * i
	}
}

// in is receive-only. This function cannot send on it or close it.
func sum(in <-chan int, count int) int {
	total := 0
	for i := 0; i < count; i++ {
		total += <-in
	}
	return total
}

func main() {
	ch := make(chan int) // make always gives a two-way channel

	// Passing ch converts it to send-only for produce and receive-only
	// for sum. The conversion is automatic and costs nothing.
	go produce(ch)

	fmt.Println("total:", sum(ch, 3))
}

// Output:
// total: 14

When to use it

  • Return <-chan Event from a constructor so callers cannot close the stream.
  • Write a worker signature that states it reads jobs and writes results.
  • Stop a consumer from accidentally sending on the channel it is meant to drain.
  • Make the data flow through a pipeline readable from the function signatures alone.

More examples

A function that hands back a receive-only channel

The most common place direction appears: a return type. Inside <code>counter</code> the channel is two-way, because the function needs to send on it and close it. The value handed back is <code>&lt;-chan int</code>, so a caller can only read. That one character stops a whole class of bug, where a caller closes a channel the producer is still sending on. Any function that starts a goroutine and streams its output should return <code>&lt;-chan T</code> rather than <code>chan T</code>.

Example · go
package main

import "fmt"

// counter starts a goroutine and hands back a receive-only channel. The
// caller can read the numbers and nothing else: it cannot send on this
// channel, and it cannot close it by mistake.
func counter(n int) <-chan int {
	out := make(chan int) // two-way in here, so this function can send
	go func() {
		for i := 1; i <= n; i++ {
			out <- i
		}
		close(out) // the function that sends is the one that closes
	}()
	return out // converted to <-chan int on the way out
}

func main() {
	total := 0
	for v := range counter(4) {
		total += v
	}
	fmt.Println("total:", total)
}

// Output:
// total: 10

Exactly which operations the compiler refuses

A compiling program with the rejected lines kept as comments, alongside the error text the compiler actually prints. The detail worth remembering is in <code>produce</code>: <code>close</code> on a send-only channel is fine. Direction does not restrict closing to two-way channels, it restricts it to senders, which is the same rule stated in the type system. Keep this example to hand when an error message about a send-only or receive-only channel appears and the fix is not obvious.

Example · go
package main

import "fmt"

func produce(out chan<- int) {
	out <- 42  // allowed: out is send-only
	close(out) // also allowed: closing is a sender's job

	// This line does not compile:
	//   v := <-out
	//   invalid operation: cannot receive from send-only channel out
}

func consume(in <-chan int) int {
	// Neither of these compiles:
	//   in <- 1
	//   invalid operation: cannot send to receive-only channel in
	//   close(in)
	//   invalid operation: cannot close receive-only channel in
	return <-in
}

func main() {
	ch := make(chan int, 1)
	produce(ch)
	fmt.Println("read:", consume(ch))
}

// Output:
// read: 42

Narrowing is one-way, and how the arrow binds

Three things in one short program. The implicit narrowing on assignment and the explicit conversion are the shorthand and longhand of the same operation, so you can read either. The narrowing cannot be undone, which is what makes it a real guarantee rather than a hint. And the arrow binds to the leftmost <code>chan</code>, so <code>chan&lt;- chan int</code> already means <code>chan&lt;- (chan int)</code> and the parentheses change nothing. That last rule only matters when a channel carries channels, which happens in request-reply code where each request includes the channel its answer should come back on.

Example · go
package main

import "fmt"

func main() {
	both := make(chan int, 1)

	// A two-way channel becomes either direction on assignment...
	var send chan<- int = both
	var recv <-chan int = both

	// ...and the explicit conversion says exactly the same thing.
	send = (chan<- int)(both)
	recv = (<-chan int)(both)

	send <- 9
	fmt.Println("received:", <-recv)

	// There is no way back. This does not compile:
	//   var back chan int = send
	//   cannot use send (variable of type chan<- int) as chan int value

	// The arrow attaches to the leftmost chan, so these two types are
	// identical: a send-only channel whose values are two-way int channels.
	var a chan<- chan int
	var b chan<- (chan int)
	a, b = b, a
	fmt.Println("both nil:", a == nil, b == nil)
}

// Output:
// received: 9
// both nil: true true

Discussion

  • Be the first to comment on this lesson.