Lesson 6 / 25

Directional Channel Types in APIs

Let the compiler enforce who sends and who receives.

chan<- and <-chan

A channel type can be restricted: chan<- T is send-only and <-chan T is receive-only. A bidirectional chan T converts implicitly to either, but not back. Use them in function signatures to document and enforce roles: a producer takes chan<- T or returns <-chan T, a consumer takes <-chan T. The compiler then rejects a consumer that tries to send or to close (closing a receive-only channel is a compile error), which encodes the "only the sender closes" rule in the type system. A common idiom is a generator that creates the channel, starts a goroutine to fill and close it, and returns it as <-chan T.

A generator returning a receive-only channel

The function owns the channel; callers can only receive.

package main

import "fmt"

// count owns out: it creates it, sends on it and closes it.
func count(n int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for i := range n { // Go 1.22+: range over an int
			out <- i
		}
	}()
	return out
}

func printAll(in <-chan int) {
	for v := range in {
		fmt.Println(v)
	}
	// in <- 1   // compile error: send to receive-only channel
	// close(in) // compile error: cannot close receive-only channel
}

func main() {
	printAll(count(3))
}

Return <-chan, accept the narrowest type

Exported functions should rarely accept or return a plain chan T. Narrow types make ownership obvious in code review and prevent accidental double closes.

Quick check: What happens if code calls close on a value of type <-chan int?

  • It is silently ignored
  • It panics at run time
  • It closes the channel normally
  • The program fails to compile
Answer

The program fails to compile — Closing a receive-only channel is rejected by the compiler.