Skip to content

Go Channels

Channels are the pipes that connect concurrent goroutines. You can send values into channels from one goroutine and receive those values into another goroutine.

1. Basic Channels

By default, channels are unbuffered, meaning they only accept sends (chan <-) if there is a corresponding receive (<- chan) ready to receive the sent value.

package main

import "fmt"

func main() {
    // 1. Create a new channel with make(chan val-type)
    messages := make(chan string)

    // 2. Send a value into a channel using the <- syntax
    go func() { messages <- "ping" }()

    // 3. Receive the value from the channel
    msg := <-messages
    fmt.Println(msg)
}

2. Channel Synchronization

We can use channels to synchronize execution across goroutines. Here's an example of using a blocking receive to wait for a goroutine to finish.

func worker(done chan bool) {
    fmt.Print("working...")
    time.Sleep(time.Second)
    fmt.Println("done")

    // Send a value to notify that we're done
    done <- true
}

func main() {
    done := make(chan bool, 1)
    go worker(done)

    // Block until we receive a notification from the worker
    <-done
}

3. Channel Directions

When using channels as function parameters, you can specify if a channel is meant to only send or receive values. This increases type safety.

// This function only accepts a channel for sending values
func ping(pings chan<- string, msg string) {
    pings <- msg
}

// This function accepts one channel for receives (pings) 
// and a second for sends (pongs)
func pong(pings <-chan string, pongs chan<- string) {
    msg := <-pings
    pongs <- msg
}

Why use Channels?

  1. Thread Safety: Channels handle the locking logic internally, so you don't have to use Mutexes for simple data passing.
  2. Orchestration: They are great for "handing off" work from one stage of a program to another (Pipelines).
  3. Clean Code: They follow the Go mantra: "Do not communicate by sharing memory; instead, share memory by communicating."