Skip to main content

Channels — Communication Between Goroutines

One of Go's famous design philosophy quotes:

"Do not communicate by sharing memory; instead, share memory by communicating."

Channels are the core tool that embodies this philosophy. They are typed pipes that safely pass values between goroutines.

Channel Basics — Creation and Usage

package main

import "fmt"

func main() {
// Create channel — make(chan type)
ch := make(chan int)

// Send value from goroutine
go func() {
ch <- 42 // Send value to channel
}()

// Receive value in main
value := <-ch // Receive value from channel
fmt.Println("Received:", value) // 42
}

Channel operations:

  • Send: ch <- value — blocks until receiver is ready
  • Receive: value := <-ch — blocks until sender is ready

Unbuffered vs Buffered Channels

Unbuffered Channel

make(chan T) — send and receive must happen simultaneously. This is a ** rendezvous**mechanism.

package main

import (
"fmt"
"time"
)

func main() {
ch := make(chan string) // Unbuffered channel

go func() {
time.Sleep(1 * time.Second)
fmt.Println("Before sending")
ch <- "Hello" // Blocks until receiver is ready
fmt.Println("After sending")
}()

fmt.Println("Waiting for receive...")
msg := <-ch // Blocks until sender is ready
fmt.Println("Received:", msg)
}
// Output:
// Waiting for receive...
// Before sending
// After sending
// Received: Hello

Buffered Channel

make(chan T, capacity) — can send without blocking until the buffer is full.

package main

import "fmt"

func main() {
ch := make(chan int, 3) // Buffer size 3

// Send without blocking while buffer has space
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 // Buffer is full, would block!

fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 3

// Channel length and capacity
ch2 := make(chan int, 5)
ch2 <- 10
ch2 <- 20
fmt.Printf("Length: %d, Capacity: %d\n", len(ch2), cap(ch2)) // Length: 2, Capacity: 5
}
TypeUnbufferedBuffered
SynchronizationSimultaneous send/receiveAsynchronous (within buffer)
Blocks whenNo counterpart readyBuffer full / empty
Use caseSynchronization signalBuffering speed differences

Directional Channels

Restricting channel direction in function parameters prevents mistakes.

package main

import "fmt"

// chan<- int : send-only channel
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}

// <-chan int : receive-only channel
func consumer(ch <-chan int) {
for v := range ch {
fmt.Println("Received:", v)
}
}

func main() {
ch := make(chan int, 5) // Bidirectional channel
go producer(ch) // Auto-converts to send-only
consumer(ch) // Auto-converts to receive-only
}

Channel direction types:

  • chan T — bidirectional (read and write)
  • chan<- T — send-only (write only)
  • <-chan T — receive-only (read only)

Closing Channels

close(ch) closes a channel so no more values can be sent. Receivers can detect when a channel is closed.

package main

import "fmt"

func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch) // Close the channel

// Method 1: Check channel state with ok pattern
for {
value, ok := <-ch
if !ok {
fmt.Println("Channel is closed")
break
}
fmt.Println("Value:", value)
}

// Method 2: range over channel (auto-exits when closed)
ch2 := make(chan string, 3)
ch2 <- "a"
ch2 <- "b"
ch2 <- "c"
close(ch2)

for s := range ch2 {
fmt.Println(s)
}
}

Channel closing rules:

  • Sending to a closed channel → panic
  • Receiving from a closed channel → zero value and false
  • Closing an already closed channel → panic
  • The sender should close the channel(receiver closing risks panicking the sender)

range-over-channel

Using range on a channel automatically receives until the channel is closed.

package main

import (
"fmt"
"sync"
)

func generateNumbers(start, end int) <-chan int {
ch := make(chan int)
go func() {
defer close(ch) // Close channel when function exits
for i := start; i <= end; i++ {
ch <- i
}
}()
return ch
}

func main() {
// Pipeline pattern
numbers := generateNumbers(1, 10)

var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for n := range numbers { // Receives until channel closes
fmt.Printf("%d ", n)
}
fmt.Println()
}()
wg.Wait()
}

Pipeline Pattern

Connect channels to build a data processing pipeline.

package main

import "fmt"

// Stage 1: Generate numbers
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}

// Stage 2: Square numbers
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}

// Stage 3: Filter even numbers
func filterEven(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
if n%2 == 0 {
out <- n
}
}
}()
return out
}

func main() {
// Pipeline: generate → square → filterEven
nums := generate(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
squares := square(nums)
evens := filterEven(squares)

for v := range evens {
fmt.Printf("%d ", v) // 4 16 36 64 100
}
fmt.Println()
}

Fan-out, Fan-in Pattern

Distribute work to multiple goroutines (fan-out) and collect results (fan-in).

package main

import (
"fmt"
"sync"
)

// Fan-out: distribute one channel to multiple workers
func fanOut(input <-chan int, workers int) []<-chan int {
outputs := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
out := make(chan int)
outputs[i] = out
go func(out chan<- int) {
defer close(out)
for n := range input {
out <- n * n
}
}(out)
}
return outputs
}

// Fan-in: merge multiple channels into one
func fanIn(inputs ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)

output := func(c <-chan int) {
defer wg.Done()
for n := range c {
merged <- n
}
}

wg.Add(len(inputs))
for _, c := range inputs {
go output(c)
}

go func() {
wg.Wait()
close(merged)
}()

return merged
}

func main() {
// Create input channel
input := make(chan int, 10)
for i := 1; i <= 10; i++ {
input <- i
}
close(input)

// Fan-out: distribute to 3 workers
workers := fanOut(input, 3)

// Fan-in: merge results
results := fanIn(workers...)

sum := 0
for r := range results {
sum += r
}
fmt.Println("Sum:", sum) // 385 (1²+2²+...+10²)
}

Key Takeaways

  • Unbuffered channel: Synchronous communication, send and receive happen simultaneously
  • Buffered channel: Asynchronous communication, no blocking within buffer size
  • Directional channels: Use chan<-, <-chan in function parameters to clarify intent
  • Closing channels: Sender closes; detect with range or ok pattern
  • Pipeline: Chain channels to compose data flows
  • Fan-out/Fan-in: Parallel processing and result collection pattern