Skip to content
AgileLogics
7 min read

Channels

Prerequisites: Goroutines

A channel is a typed pipe that goroutines use to send values to each other. Sending and receiving also synchronise the goroutines involved — the Go proverb is "don't communicate by sharing memory; share memory by communicating."

  • Unbuffered (make(chan int)): a send blocks until another goroutine receives, and vice versa.
  • Buffered (make(chan int, 3)): sends only block when the buffer is full; receives only block when it is empty.
  • close(ch) signals that no more values will be sent. Receivers can still drain buffered values; after that they get the zero value and ok == false. for v := range ch stops automatically once the channel is closed and empty.
  • Directional types document and enforce intent: chan<- int can only send, <-chan int can only receive.

Syntax

ch := make(chan string)    // unbuffered
buf := make(chan int, 2)   // buffered, capacity 2

ch <- "hi"                 // send
msg := <-ch                // receive
v, ok := <-ch              // ok is false once ch is closed and drained
close(ch)

func producer(out chan<- int) {} // send-only parameter
func consumer(in <-chan int) {}  // receive-only parameter

Example

package main

import "fmt"

func produce(n int, out chan<- int) {
    for i := 1; i <= n; i++ {
        out <- i
    }
    close(out) // the sender closes, never the receiver
}

func square(in <-chan int, out chan<- int) {
    for v := range in {
        out <- v * v
    }
    close(out)
}

func main() {
    // Unbuffered: main blocks on the receive until the goroutine sends.
    done := make(chan string)
    go func() {
        done <- "worker finished"
    }()
    fmt.Println(<-done)

    // Buffered: two sends succeed without a waiting receiver.
    buf := make(chan int, 2)
    buf <- 10
    buf <- 20
    fmt.Println(len(buf), cap(buf))
    fmt.Println(<-buf, <-buf)

    // A pipeline: produce -> square -> main.
    nums := make(chan int)
    squares := make(chan int)
    go produce(4, nums)
    go square(nums, squares)
    for s := range squares {
        fmt.Println(s)
    }

    // nums is closed and empty now.
    v, ok := <-nums
    fmt.Println(v, ok)
}

Expected output

worker finished
2 2
10 20
1
4
9
16
0 false

The pipeline output is in order because each stage is a single goroutine and channels preserve the order of values sent on them.

Common mistakes

  • Sending on an unbuffered channel when nothing will ever receive. If every goroutine is blocked, the program crashes with fatal error: all goroutines are asleep - deadlock!.
  • Forgetting to close a channel that a receiver ranges over — the range waits forever.
  • Closing a channel from the receiving side, closing it twice, or sending on a closed channel. The last two panic. Only the sender should close, and only when receivers need to know that no more values are coming.
  • Using a buffered channel to "fix" a deadlock without understanding it. A buffer changes when blocking happens, not whether your design is correct.

Practice

Write lengths(words []string) <-chan int that starts a goroutine sending the length of each word and then closes the channel. In main, range over the result for []string{"go", "is", "fun"} and print the total.

Show solution
package main

import "fmt"

func lengths(words []string) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, w := range words {
            out <- len(w)
        }
    }()
    return out
}

func main() {
    total := 0
    for n := range lengths([]string{"go", "is", "fun"}) {
        total += n
    }
    fmt.Println(total) // 7
}