mdawar.dev

A blog about programming, Web development, Open Source, Linux and DevOps.

Go - Channels

Channels provide a mechanism for goroutines to communicate by sending and receiving values of a specified element type

Declaration

A channel is declared with the keyword chan followed by an element type.

go
// The zero value of an uninitialized channel is nil.
// A nil channel is never ready for communication.
var ch1 chan int      // nil
var ch2 chan bool     // nil
var ch3 chan struct{} // nil

The optional <- operator specifies the channel direction (send or receive).

go
// A channel may be constrained only to send or only to receive.
chan T   // Can be used to send and receive (Bidirectional channel)
chan<- T // Can only be used to send (Directional channel)
<-chan T // Can only be used to receive (Directional channel)

Initialization

Channels must be initialized before they can be used.

The built-in function make is be used to initialize a channel of a specific element type.

go
// Initialize a channel of element type `int`.
// A channel is a reference to the data structure created by `make`.
ch := make(chan int)

Buffered & Unbuffered Channels

The function make accepts an optional second argument to specify the capacity of the channel.

The capacity (number of elements) sets the size of the buffer in the channel.

go
// A channel created without specifying the capacity is an unbuffered channel.
ch1 := make(chan int)     // Unbuffered channel
ch2 := make(chan int, 0)  // Unbuffered channel
ch3 := make(chan int, 10) // Buffered channel with capacity 10

// Buffered channel information.
len(ch3) // Number of values currently in the buffer
cap(ch3) // 10 (The maximum buffer size)

// The length and capacity of an unbuffered channel is always 0.
len(ch1) // 0
cap(ch1) // 0

Sending & Receiving

The channel operator <- is used to send and receive values with channels.

Channels act as FIFO (first-in-first-out) queues.

go
// The data flows in the direction of the arrow <- (channel operator).
ch <- v   // Send v to channel ch
v := <-ch // Receive from channel ch and assign the value to v
<-ch      // Receive from channel ch and discard the result

Unbuffered Channels

Sends and receives block until the other side is ready when using an unbuffered channel.

go
ch := make(chan int)

go func() {
  ch <- 1 // Send operation, block until the other side is ready to receive
}()

<-ch // Receive operation, block until the other side is ready to send

Buffered Channels

Sends to a buffered channel block only when the buffer if full.

Receives from a buffered channel block only when the buffer is empty.

go
ch := make(chan int, 10) // Buffer size 10

ch <- v // Send operation, block only when the buffer is full
<-ch    // Receive operation, block only when the channel is empty

Closing a Channel

A sender can close a channel to indicate that no more values will be sent.

Closing is necessary only when the receiver must be told that there are no values coming.

go
ch := make(chan int)

// Only the sender should close the channel and never the receiver.
close(ch)

// Sending on a closed channel will cause a panic.
ch <- 1 // panic: send on closed channel

Receivers can test whether a channel has been closed using the second parameter of the receive expression.

go
ch := make(chan int)

close(ch)

// Check if the channel is closed using the second boolean value.
// Receive operations on a closed channel yield the zero value of the element type.
v, ok := <-ch // 0, false

if !ok {
  // Channel is closed, no more values to receive
}

Iterating Over Channel Values

A for range loop receives values from a channel repeatedly until it’s closed.

go
// Iterate over values received from a channel until it's closed.
// Closing the channel by the sender is necessary to terminate the loop.
for v := range ch {
  // ...
}

Empty a Channel

go
// Receive values and discard them until the channel is closed.
for range ch {}

Select Statement

The select statement is used to handle multiple channel operations, it allows waiting on multiple channels simultaneously.

go
// Select blocks until one of its cases can run.
// If multiple cases are ready a random one is chosen.
select {
case ch1 <- x:
  fmt.Println("Sent x on channel ch1")
case <-ch2:
  fmt.Println("Received from channel ch2")
default:
  // The default case is run if no other case is ready.
  // It can be used to send or receive without blocking.
}

Wait for Goroutine

A channel can be used to wait for a goroutine by sending a value from the goroutine and waiting for it on the main goroutine.

go
package main

import (
  "fmt"
  "time"
)

func main() {
  done := make(chan bool)

  go func() {
    fmt.Println("Working...")
    time.Sleep(2 * time.Second) // Simulate work
    fmt.Println("Work done")

    // Send a signal that the goroutine has finished working.
    done <- true
  }()

  // Block until a value is received from the goroutine.
  <-done
}

If a channel is only used for a signal, it can be closed instead of sending a value:

go
package main

import (
  "fmt"
  "time"
)

func main() {
  // The empty struct has size zero.
  // It's useful in this case as we're not interested in the value.
  done := make(chan struct{})

  go func() {
    fmt.Println("Working...")
    time.Sleep(2 * time.Second) // Simulate work
    fmt.Println("Work done")

    // Close the channel to signal that the goroutine has finished working.
    close(done)
  }()

  // Block until the channel is closed.
  <-done
}