Go Channels

A channel is a pipe that connects goroutines. One goroutine sends a value into the channel; another receives it. Channels provide safe communication between concurrent goroutines without shared memory conflicts.

Go's philosophy: Do not communicate by sharing memory; share memory by communicating.

Creating and Using a Channel

package main

import "fmt"

func main() {
    ch := make(chan int) // create a channel that carries int values

    go func() {
        ch <- 42 // send 42 into the channel
    }()

    value := <-ch // receive from the channel
    fmt.Println(value) // 42
}

Channel Operations Diagram

Goroutine A                    Goroutine B
    │                               │
    │     ch := make(chan int)      │
    │                               │
    ├─── ch <- 42 ─────────────────►│
    │     (send)                    │  value := <-ch
    │                               │  (receive)
    │                               │
    │                               ▼
                               value = 42

Channel Direction

SyntaxMeaning
ch <- valSend val into channel ch
val := <-chReceive from channel ch, store in val
<-chReceive and discard the value

Unbuffered vs Buffered Channels

Unbuffered Channel (default)

Sender blocks until receiver is ready. Receiver blocks until sender sends. Both goroutines must be ready at the same time.

ch := make(chan string) // unbuffered

Buffered Channel

A buffered channel holds a specified number of values without blocking. The sender only blocks when the buffer is full. The receiver only blocks when the buffer is empty.

package main

import "fmt"

func main() {
    ch := make(chan string, 3) // buffer holds 3 strings

    ch <- "first"
    ch <- "second"
    ch <- "third"
    // no goroutine needed — buffer absorbs the sends

    fmt.Println(<-ch) // first
    fmt.Println(<-ch) // second
    fmt.Println(<-ch) // third
}

Passing Channels to Functions

Channels can be passed to functions with directional types to enforce send-only or receive-only access.

package main

import "fmt"

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

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

func main() {
    ch := make(chan int)
    go producer(ch)
    consumer(ch)
}

Output:

Received: 1
Received: 2
Received: 3
Received: 4
Received: 5

Closing a Channel and range

A sender closes a channel with close(ch) to signal no more values will be sent. A receiver can range over a channel — the loop automatically ends when the channel is closed.

close(ch)         // sender closes the channel

for val := range ch { // receiver loops until channel is closed
    fmt.Println(val)
}

Buffered vs Unbuffered Comparison

FeatureUnbufferedBuffered
Created withmake(chan T)make(chan T, n)
Blocks senderUntil receiver readyOnly when buffer is full
Blocks receiverUntil sender sendsOnly when buffer is empty
SynchronizationStrict — both sides syncLooser — decoupled

Key Points

  • Channels connect goroutines for safe communication
  • Use <- to send into and receive from a channel
  • Unbuffered channels synchronize both sides; buffered channels decouple them
  • Only the sender should close a channel, never the receiver
  • Use range to receive all values until a channel is closed
  • Directional channel types (chan<-, <-chan) enforce usage at compile time

Leave a Comment