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)
}
}
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
| Type | Syntax | Operations | Common Use |
|---|
| Bidirectional | chan T | Send & receive | Internal use |
| Send-only | chan<- T | Send only | Function parameters |
| Receive-only | <-chan T | Receive only | Return 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
- Use directional channels in function signatures - Prevents misuse
- Return receive-only channels from constructors - Encapsulation
- Accept send-only channels for reporting - Clear responsibilities
- Keep bidirectional channels internal - Use within functions/goroutines
- 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) { ... }