Go Channels

Channel Basics

Unbuffered channels — synchronous handoff
ch := make(chan string)

go func() {
    ch <- "hello"  // blocks until receiver is ready
}()

msg := <-ch  // blocks until sender sends
fmt.Println(msg)

An unbuffered channel blocks the sender until a receiver is ready, and vice versa. Both goroutines meet at the send/receive.

Buffered channels — async with capacity
ch := make(chan int, 3)  // buffer holds 3 values
ch <- 1  // does not block
ch <- 2  // does not block
ch <- 3  // does not block
// ch <- 4  // would block — buffer full
fmt.Println(<-ch)  // 1 (FIFO)

Buffered channels decouple sender and receiver up to the buffer capacity.

Directional Channels

Restricting channel direction in function signatures
func produce(ch chan<- int) {  // send-only
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}

func consume(ch <-chan int) {  // receive-only
    for v := range ch {
        fmt.Println(v)
    }
}

Directional types are a compile-time constraint. A bidirectional chan int converts automatically.

Select

select multiplexes across channels
select {
case msg := <-ch1:
    fmt.Println("from ch1:", msg)
case msg := <-ch2:
    fmt.Println("from ch2:", msg)
case <-time.After(1 * time.Second):
    fmt.Println("timeout")
}

select blocks until one case is ready. If multiple are ready, one is chosen at random. time.After provides a built-in timeout.

Non-blocking with default
select {
case msg := <-ch:
    fmt.Println("received:", msg)
default:
    fmt.Println("no message available")
}

Closing Channels

close signals no more values
ch := make(chan int)

go func() {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}()

// range exits when channel is closed
for v := range ch {
    fmt.Println(v)
}

// Manual close check
v, ok := <-ch
if !ok {
    fmt.Println("channel closed")
}

Only the sender should close a channel. Sending on a closed channel panics. range over a channel is the idiomatic consumption pattern.

Patterns

Fan-in — merge multiple channels
func fanIn(channels ...<-chan string) <-chan string {
    var wg sync.WaitGroup
    merged := make(chan string)

    for _, ch := range channels {
        wg.Add(1)
        go func(c <-chan string) {
            defer wg.Done()
            for v := range c {
                merged <- v
            }
        }(ch)
    }

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

    return merged
}
Done channel for cancellation
func doWork(done <-chan struct{}) {
    for {
        select {
        case <-done:
            return
        default:
            // work
        }
    }
}

done := make(chan struct{})
go doWork(done)
close(done)  // broadcasts to all receivers

chan struct{} carries no data — it is a pure signal. Prefer context.Context in production code.