Free — no signup required

Concurrency: Channels

2 min read

The Pipes Between Goroutines

Goroutines running in parallel need a way to send results back to the caller or communicate with each other. The naive approach — having all goroutines write to a shared variable — requires locks and is a common source of race conditions and bugs.

Go's philosophy is: "Don't communicate by sharing memory; share memory by communicating."

Channels are typed, thread-safe pipes. One goroutine sends a value into a channel; another goroutine receives it. The act of sending and receiving also synchronizes the goroutines — a sender blocks until a receiver is ready, and vice versa (for unbuffered channels).

package main

import "fmt"

func checkServer(hostname string, results chan<- string) {
    // chan<- means this function can only SEND to the channel
    // Simulate a health check result
    results <- hostname + ": healthy"
}

func main() {
    servers := []string{"web01", "web02", "web03"}

    // Create an unbuffered channel for strings
    results := make(chan string)

    // Start one goroutine per server
    for _, server := range servers {
        go checkServer(server, results)
    }

    // Collect exactly one result per server
    for i := 0; i < len(servers); i++ {
        result := <-results // Block until a result arrives
        fmt.Println(result)
    }
}

The chan<- string type annotation on the function parameter is a directional channel — it can only be used to send, not receive. This is a compile-time safety guarantee that prevents accidental misuse.

Buffered Channels

An unbuffered channel requires a receiver to be ready before the sender can proceed. A buffered channel has a queue — the sender can deposit up to N values without blocking, even if no receiver is ready yet.

// Buffer holds up to 10 results without blocking senders
results := make(chan string, 10)

Buffered channels are useful when you know the maximum number of results upfront and want to decouple producers from consumers slightly. However, if the buffer fills up, the sender blocks again — so buffered channels are not a substitute for proper flow control.

The fan-out / fan-in pattern

The combination of goroutines + channels enables a powerful pattern: fan-out (distribute work across many goroutines) and fan-in (collect results back into a single channel). This is the backbone of most concurrent Ops tools — parallel SSH runners, multi-region health checkers, concurrent API pollers.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free