Channel Directions
Restrict channels to send-only or receive-only in function signatures to document and enforce data flow.
When passing a channel to a function, you can constrain its direction: chan<- T is send-only, <-chan T is receive-only. The compiler enforces the constraint - misuse is a compile error, not a runtime panic.
Without direction constraints, any function could accidentally receive from a channel meant only for sending, or send to one meant only for receiving. Here, ping is a pure sender and pong is a relay - it receives from one channel and forwards to another. Restricting each function to only the direction it needs makes the mistake a compile error instead of a silent bug.
package main
import "fmt"
func ping(pings chan<- string, msg string) {
pings <- msg
}
func pong(pings <-chan string, pongs chan<- string) {
msg := <-pings
pongs <- msg
}
func main() {
pings := make(chan string, 1)
pongs := make(chan string, 1)
ping(pings, "passed message")
pong(pings, pongs)
fmt.Println(<-pongs) // "passed message"
}A bidirectional chan T is assignable to chan<- T or <-chan T without a cast - the compiler inserts the narrowing automatically:
ch := make(chan int)
var send chan<- int = ch // valid: narrowing to send-only
var recv <-chan int = ch // valid: narrowing to receive-only
// send = recv // compile error: <-chan int is not assignable to chan<- intA function can also return a directional channel. The return type <-chan int tells callers they may only receive - they cannot accidentally send to or close the internal channel. The body below is a mock of a real producer pattern (streaming events, reading from a database); the important part is the return type signature, not the loop internals.
func generate(done <-chan struct{}) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for i := 0; ; i++ {
select {
case out <- i: // in production: emit a db row, event, generated ID, etc.
case <-done: // caller signals stop - goroutine exits and closes out
return
}
}
}()
return out // returns immediately; goroutine keeps running in background
}
func main() {
done := make(chan struct{})
nums := generate(done)
fmt.Println(<-nums) // 0
fmt.Println(<-nums) // 1
fmt.Println(<-nums) // 2
close(done) // built-in: unblocks any goroutine waiting on <-done, signaling it to stop
}In production
Directional channel types are documentation enforced by the compiler. A function that accepts chan<- T cannot accidentally receive from it, and a function accepting <-chan T cannot close it (only the sender should close). Use them in public APIs to make data flow explicit and prevent misuse by callers. The discipline also helps readers understand a function's role at a glance: a <-chan parameter means "this is a consumer", a chan<- parameter means "this is a producer".
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free