Skip to content
Theory · Goroutines and channels · lesson 4 of 13

Channels

A goroutine can compute a value, but the go statement discards the function's result. How does a cook hand a finished dish to a waiter? A kitchen has a serving hatch for that: the cook puts a plate on one side, and the waiter takes it from the other.

A value from a goroutine

In Go, that hatch is a channel. One goroutine sends values into it, and another receives them:

GO
package main

import "fmt"

func main() {
    ch := make(chan int)
    go func() {
        ch <- 6 * 7
    }()
    fmt.Println(<-ch) // 42
}

Let's walk through it line by line:

  1. ch := make(chan int) opens a hatch for int values.
  2. go func() { … }() starts the cook goroutine: it computes 6 * 7 and sends 42 into the channel.
  3. fmt.Println(<-ch)—main, playing the waiter, waits at the hatch, takes 42 and prints it.

The channel type

A channel is created with make: make(chan int) gives a channel of int values, and its type is chan int. The channel type sets the type of its values: sending a string into a chan int does not compile.

A channel is passed to a function as an ordinary parameter: func send(ch chan int). The copy of a channel refers to the same channel, as a copy of a map refers to the same table.

Send and receive

Two operations work with a channel. The arrow shows where the value goes.

Send

The statement ch <- v sends the value v into the channel. The arrow points toward the channel: the value goes into it.

Receive

The expression <-ch receives a value from the channel. The arrow comes out of the channel: the value comes out. The received value can be stored in a variable: v := <-ch.

An unbuffered channel

A channel created with make(chan int) is called unbuffered: it holds no values. Such a hatch has no shelf: the cook holds the plate until the waiter takes it. The two operations wait for each other:

  • a send waits until another goroutine starts a receive;
  • a receive waits for a send.

That is why main in the first example needs no WaitGroup: <-ch waits until the goroutine sends 42.

Press the button and watch the cook wait at the hatch until the waiter takes the soup.

LiveUnbuffered channelbuffer: 0 · senders waiting: 1

The sender is blocked: an unbuffered channel stores nothing. The cook waits at the hatch until a receiver takes the soup.

The hatch is the channel: things pass through it, nothing is stored

Values from one sender arrive in the order it sent them:

GO
ch := make(chan string)
go func() {
    ch <- "one"
    ch <- "two"
}()
fmt.Println(<-ch, <-ch) // one two

Step by step:

  1. The first receive takes "one".
  2. Until main has taken the first plate, the cook cannot serve the second.
  3. The second receive takes "two", and the program prints one two.

This is where the hatch picture stops working: in a kitchen, the cook and the waiter are always different people. In Go, one and the same goroutine may send and receive a value. But with an unbuffered channel it would then wait for itself.

Deadlock

A goroutine waiting on a send or a receive waits until another goroutine performs the matching operation. If every goroutine of the program is waiting, none of them can move. This is a deadlock. The Go runtime notices it and stops the program with an error:

TEXT
fatal error: all goroutines are asleep - deadlock!

The compiler does not find this error: the build succeeds, and the program stops when it runs.

A nil channel

Interviewers like to ask what happens with a nil channel. The answer has three facts:

  • a channel declared with var ch chan int is nil: the hatch has not been opened yet;
  • a send into a nil channel and a receive from it wait forever;
  • if nobody else is working, the program stops with the same error.

That is why a channel is created with make.

Common mistake

A function that sends a value into a channel was called without go:

GO
func send(ch chan int) {
    ch <- 42
}

func main() {
    ch := make(chan int)
    send(ch) // ✗
    fmt.Println(<-ch)
}

Here is what happens:

  1. send runs inside main itself and waits for a receiver on the line ch <- 42.
  2. The cook stands at the hatch with a plate, and the waiter is the cook.
  3. The receive on the next line never starts, so the program stops with fatal error: all goroutines are asleep - deadlock!.

The fix is go send(ch): then the send waits in a separate goroutine, and main reaches the receive.

Key points

  • A channel passes values between goroutines: ch <- v sends, <-ch receives.
  • An unbuffered channel holds no values: a send waits for a receive, and a receive waits for a send.
  • When every goroutine is waiting, that is a deadlock, and the program stops at run time.
  • A send into a nil channel and a receive from it wait forever; create channels with make.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way