Skip to main content
When using channels as function parameters, you can specify if a channel is meant to only send or only receive values. This specificity increases the type-safety of your program and makes intent clearer.

Channel Direction Syntax

package main

import "fmt"

// Send-only channel (can only send)
func ping(pings chan<- string, msg string) {
	pings <- msg
}

// Receive-only input, send-only output
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)
}

Direction Types

Bidirectional (default)

ch := make(chan string)
// Can both send and receive
ch <- "send"
msg := <-ch

Send-Only

func sender(ch chan<- string) {
	ch <- "message"  // OK
	// msg := <-ch    // Compile error!
}
The arrow points toward the chan keyword for send-only channels: chan<- Type

Receive-Only

func receiver(ch <-chan string) {
	msg := <-ch      // OK
	// ch <- "msg"   // Compile error!
}
The arrow points away from the chan keyword for receive-only channels: <-chan Type

Type Conversion Rules

Go automatically converts bidirectional channels to directional channels:
ch := make(chan int)  // Bidirectional

sendOnly(ch)     // Automatically converts to chan<- int
receiveOnly(ch)  // Automatically converts to <-chan int

func sendOnly(ch chan<- int) { ch <- 42 }
func receiveOnly(ch <-chan int) { <-ch }
You CANNOT convert a directional channel back to bidirectional or change its direction.
func broken(ch <-chan int) {
	var bi chan int = ch  // Compile error!
}

Why Use Channel Directions?

1. Type Safety

Prevent accidental misuse:
func producer(out chan<- int) {
	// out <- 42      // OK
	// x := <-out     // Compile error - prevents bugs
}

2. Intent Documentation

Make code self-documenting:
// Clear: this function only reads from 'input'
func process(input <-chan Task, output chan<- Result) {
	// ...
}

3. Compiler Enforcement

Catch errors at compile time:
func worker(jobs <-chan int, results chan<- int) {
	for j := range jobs {
		results <- j * 2  // OK
	}
	// jobs <- 1  // Compile error - caught before runtime
}

Practical Example: Pipeline Pattern

func generate(nums ...int) <-chan int {
	out := make(chan int)
	go func() {
		for _, n := range nums {
			out <- n
		}
		close(out)
	}()
	return out  // Converts to receive-only
}

func square(in <-chan int) <-chan int {
	out := make(chan int)
	go func() {
		for n := range in {
			out <- n * n
		}
		close(out)
	}()
	return out
}

func main() {
	// Set up pipeline
	ch1 := generate(2, 3, 4)
	ch2 := square(ch1)
	
	// Consume output
	for num := range ch2 {
		fmt.Println(num)  // 4, 9, 16
	}
}
Returning a receive-only channel from a function prevents callers from sending on it, making the channel’s purpose clear.

Common Patterns

Producer Returns Receive-Only

func producer() <-chan int {
	ch := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			ch <- i
		}
		close(ch)
	}()
	return ch  // External code can only receive
}

Consumer Accepts Send-Only for Results

func consumer(results chan<- string) {
	for i := 0; i < 5; i++ {
		results <- fmt.Sprintf("result %d", i)
	}
}

Processor: Receive Input, Send Output

func processor(in <-chan int, out chan<- int) {
	for val := range in {
		out <- val * 2
	}
}

Direction with Close

Only send-only channels can be closed. You cannot close a receive-only channel.
func sender(ch chan<- int) {
	ch <- 42
	close(ch)  // OK
}

func receiver(ch <-chan int) {
	<-ch
	// close(ch)  // Compile error!
}
This makes sense: only the sender should close a channel.

Complete Example: Worker Pool

type Job struct {
	ID int
}

type Result struct {
	Job Job
	Value int
}

func worker(id int, jobs <-chan Job, results chan<- Result) {
	for job := range jobs {
		fmt.Printf("Worker %d processing job %d\n", id, job.ID)
		results <- Result{Job: job, Value: job.ID * 2}
	}
}

func main() {
	jobs := make(chan Job, 100)
	results := make(chan Result, 100)
	
	// Start workers
	for w := 1; w <= 3; w++ {
		go worker(w, jobs, results)  // Auto-converted to directional
	}
	
	// Send jobs
	for j := 1; j <= 5; j++ {
		jobs <- Job{ID: j}
	}
	close(jobs)
	
	// Collect results
	for r := 1; r <= 5; r++ {
		result := <-results
		fmt.Printf("Result: %v\n", result)
	}
}

Syntax Summary

TypeSyntaxOperationsCommon Use
Bidirectionalchan TSend & receiveInternal use
Send-onlychan<- TSend onlyFunction parameters
Receive-only<-chan TReceive onlyReturn values

Memory: Arrow Direction

Send-only: Arrow goes INTO the channel chan<- T Receive-only: Arrow comes OUT OF the channel <-chan T

Best Practices

  1. Use directional channels in function signatures - Prevents misuse
  2. Return receive-only channels from constructors - Encapsulation
  3. Accept send-only channels for reporting - Clear responsibilities
  4. Keep bidirectional channels internal - Use within functions/goroutines
  5. Close send-only channels when done - Signal completion

Design Pattern: Encapsulation

type EventStream struct {
	ch chan Event
}

// Public method returns receive-only channel
func (e *EventStream) Subscribe() <-chan Event {
	return e.ch
}

// Private method uses bidirectional channel
func (e *EventStream) publish(event Event) {
	e.ch <- event
}
This prevents external code from sending events, maintaining encapsulation.

Common Mistakes

Trying to Reverse Direction

// BAD: Cannot convert back
func bad(in <-chan int) {
	var biChan chan int = in  // Compile error!
}

Closing Receive-Only Channel

// BAD: Cannot close receive-only
func bad(ch <-chan int) {
	close(ch)  // Compile error!
}

Not Using Directions When You Should

// Less clear
func worker(jobs chan int, results chan int) { ... }

// Better - shows intent
func worker(jobs <-chan int, results chan<- int) { ... }

Build docs developers (and LLMs) love