Skip to content

Go Channels — Unbuffered Buffered Range and Directional Channels Explained

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Go Channels. We cover key concepts, practical examples, and best practices to help you master this topic.

Go channels provide typed communication between goroutines with unbuffered channels for synchronization, buffered channels for async handoff, range for channel iteration, and directional channel types for API safety.

What You'll Learn

  • Creating and using channels
  • Unbuffered vs buffered channels
  • Channel direction
  • Range and close patterns

Why It Matters

Channels are Go's primary concurrency primitive. Docker uses channels for stream handling. Kubernetes uses channels for watch APIs and event processing. DodaZIP uses channels for progress reporting.

Real-World Use

HTTP server request pipelines, worker pools, event processing systems, data streaming, pipeline architectures — all use channels for Goroutine communication.

flowchart LR
    A["Channels"] --> B["Unbuffered"]
    B --> C["Buffered"]
    C --> D["Directional"]
    D --> E["Range/Close"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Basic Channel Operations

func main() {
    ch := make(chan int)
    go func() { ch <- 42 }()
    val := <-ch
    fmt.Println(val)
}

Unbuffered Channels

func main() {
    ch := make(chan string)
    go func() {
        fmt.Println("Goroutine: sending...")
        ch <- "hello"
        fmt.Println("Goroutine: sent!")
    }()
    time.Sleep(100 * time.Millisecond)
    fmt.Println("Main: receiving...")
    msg := <-ch
    fmt.Println("Main: received", msg)
}
// Goroutine: sending...
// Main: receiving...
// Main: received hello
// Goroutine: sent!

Buffered Channels

func main() {
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3
    fmt.Println(<-ch)
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

Range over Channel

func generate(count int) <-chan int {
    ch := make(chan int)
    go func() {
        for i := 1; i <= count; i++ {
            ch <- i
        }
        close(ch)
    }()
    return ch
}

func main() {
    for num := range generate(5) {
        fmt.Println("Received:", num)
    }
}

Directional Channels

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

func consumer(ch <-chan int) {
    for val := range ch {
        fmt.Println("Consumed:", val)
    }
}

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

Select Statement

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "from ch1"
    }()
    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "from ch2"
    }()

    select {
    case msg := <-ch1:
        fmt.Println(msg)
    case msg := <-ch2:
        fmt.Println(msg)
    case <-time.After(50 * time.Millisecond):
        fmt.Println("timeout")
    }
}

Fan-Out Pattern

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

func main() {
    const numJobs = 10
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    for r := 1; r <= numJobs; r++ {
        <-results
    }
}

Fan-In Pattern

func producer(name string, count int) <-chan string {
    ch := make(chan string)
    go func() {
        for i := 0; i < count; i++ {
            ch <- fmt.Sprintf("%s: %d", name, i)
            time.Sleep(100 * time.Millisecond)
        }
        close(ch)
    }()
    return ch
}

func fanIn(chs ...<-chan string) <-chan string {
    out := make(chan string)
    var wg sync.WaitGroup
    for _, ch := range chs {
        wg.Add(1)
        go func(c <-chan string) {
            defer wg.Done()
            for msg := range c {
                out <- msg
            }
        }(ch)
    }
    go func() { wg.Wait(); close(out) }()
    return out
}

func main() {
    combined := fanIn(producer("A", 3), producer("B", 3))
    for msg := range combined {
        fmt.Println(msg)
    }
}

Pipeline Pattern

func nums(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums { out <- n }
        close(out)
    }()
    return out
}

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() {
    for r := range square(square(nums(1, 2, 3))) {
        fmt.Println(r)
    }
}

Common Mistakes

1. Send on Unbuffered Without Receiver

ch := make(chan int)
ch <- 42  // Deadlock! No receiver

2. Closing Channel Twice

ch := make(chan int)
close(ch)
close(ch)  // Panic

3. Send on Closed Channel

ch := make(chan int)
close(ch)
ch <- 42  // Panic

4. Not Closing Channel

ch := make(chan int)
go func() { ch <- 1 /* missing close */ }()
for v := range ch { }  // Never exits

5. Reading from nil Channel

var ch chan int  // nil
ch <- 42  // Blocks forever
<-ch      // Blocks forever

Practice Questions

1. What's the difference between buffered and unbuffered channels? Unbuffered synchronize — send blocks until receive. Buffered allow sending up to capacity without blocking.

2. When does range over a channel exit? When the channel is closed with close(ch). Only the sender should close.

3. What is a directional channel? A channel restricted to send-only (chan<-) or receive-only (<-chan). Compile-time safety.

4. What does select do? Choose which of multiple channel operations proceeds. Like switch for channels.

Challenge: Build a concurrent pipeline that reads integers, doubles them in parallel workers, and collects results.

Solution
func stage(in <-chan int, fn func(int) int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- fn(n)
        }
        close(out)
    }()
    return out
}

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

    doubled := stage(stage(input, func(n int) int { return n * 2 }),
        func(n int) int { return n * 2 })

    for r := range doubled {
        fmt.Println(r)
    }
}

FAQ

{{< faq question="Should I use channels or mutexes?" >}} Channels communicate data between goroutines. Mutexes protect shared state. "Share memory by communicating, don't communicate by sharing memory." Prefer channels for data flow, mutexes for state protection. {{< /faq >}}

{{< faq question="Can I close a channel more than once?" >}} No. Closing a closed channel panics. Use sync.Once if you must close from multiple sources. {{< /faq >}}

{{< faq question="What is the zero value of a channel?" >}} nil. Sending to or receiving from a nil channel blocks forever. Used intentionally in select statements to disable cases. {{< /faq >}}

{{< faq question="How do I check if a channel is closed?" >}} Use the two-value receive: val, ok := <-ch. ok is false if the channel is closed. Or use range, which exits on close. {{< /faq >}}

{{< faq question="What buffer size should I use?" >}} Unbuffered (0) for synchronization. Small buffer (1-100) for handoff. Large buffers can mask design issues. Profile to find the right size. {{< /faq >}}

Try It Yourself

package main

import "fmt"

func main() {
    ch := make(chan int, 5)
    go func() {
        for i := 0; i < 5; i++ {
            ch <- i
        }
        close(ch)
    }()

    for v := range ch {
        fmt.Printf("Received: %d\n", v)
    }

    val, ok := <-ch
    fmt.Printf("Closed: val=%d, ok=%v\n", val, ok)
}

Expected output:

Received: 0
Received: 1
Received: 2
Received: 3
Received: 4
Closed: val=0, ok=false

What's Next

Now that you understand channels, learn about the select statement for multiplexing channel operations.

Topic Description Link
Go Select Multiplexing, timeouts, default {{< ref "19-select" >}}
Go WaitGroups Synchronization patterns {{< ref "20-waitgroups" >}}
Rust Channels Compare Rust's mpsc channels Rust

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse Go