Channels

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.

Go provides chan keyword to create a channel. A channel can transport data of only one data type. No other data types are allowed to be transported from that channel.

  • You can send and receive values with the channel operator, <-.

  • A channel is a first-class value that can be allocated and passed around like any other.

// The data flows in the direction of the arrow
ch := make(chan int)
ch <- v    // Send v to channel ch.
v := <-ch  // Receive from ch, and
            // assign value to v.

By default, sends and receives block until the other side is ready. This allows goroutines to synchronize without explicit locks or condition variables.

func main() {
    messages := make(chan string)
    go func() { messages <- "ping" }()
    msg := <-messages
    fmt.Println(msg) // ping
}

All the above channel operations are blocking by default. Channel operations are also blocking in nature. When some data is written to the channel, goroutine is blocked until some other goroutine reads it from that channel. At the same time channel operations tell the scheduler to schedule another goroutine, that’s why a program doesn’t block forever on the same goroutine.

Like array, slice and map, each channel type has an element type. A channel can only transfer values of the element type of the channel.Channel types can be bi-directional or single-directional. Assume T is an arbitrary type,

  • chan T denotes a bidirectional channel type. Compilers allow both receiving values from and sending values to bidirectional channels.

  • chan<- T denotes a send-only channel type. Compilers don't allow receiving values from send-only channels.

  • <-chan T denotes a receive-only channel type. Compilers don't allow sending values to receive-only channels.

Values of bidirectional channel type chan T can be implicitly converted to both send-only type chan<- T and receive-only type <-chan T, but not vice versa (even if explicitly).

Along with transferring values (through channels), the ownership of some values may also be transferred between goroutines. When a goroutine sends a value to a channel, we can view the goroutine releases the ownership of some values. When a goroutine receives a value from a channel, we can view the goroutine acquires the ownerships of some values.

Channel operations

There are five channel specified operations. Assume the channel is ch, their syntax and function calls of these operations are listed here.

  1. Close the channel by using the following function call

    close(ch)

    where close is a built-in function. The argument of a close function call must be a channel value, and the channel ch must not be a receive-only channel.

  2. Send a value, v, to the channel by using the following syntax

    ch <- v

    where v must be a value which is assignable to the element type of channel ch, and the channel ch must not be a receive-only channel. Note that here <- is a channel-send operator.

  3. Receive a value from the channel by using the following syntax

    <-ch

    A channel receive operation always returns at least one result, which is a value of the element type of the channel, and the channel ch must not be a send-only channel. Note that here <- is a channel-receive operator. Yes, its representation is the same as a channel-send operator.

    For most scenarios, a channel receive operation is viewed as a single-value expression. However, when a channel operation is used as the only source value expression in an assignment, it can result a second optional untyped boolean value and become a multi-value expression. The untyped boolean value indicates whether or not the first result is sent before the channel is closed. (Below we will learn that we can receive unlimited number of values from a closed channel.)

    Two channel receive operations which are used as source values in assignments:

    v = <-ch
    v, sentBeforeClosed = <-ch
  4. Query the value buffer capacity of the channel by using the following function call

    cap(ch)

    where cap is a built-in function which has ever been introduced in containers in Go. The return result of a cap function call is an int value.

  5. Query the current number of values in the value buffer (or the length) of the channel by using the following function call

    len(ch)

    where len is a built-in function which also has ever been introduced before. The return value of a len function call is an int value. The result length is number of elements which have already been sent successfully to the queried channel but haven't been received (taken out) yet.

Each value written to a channel can only be read once. If multiple goroutines are reading from the same channel, a value written to the channel will only be read by one of them.

All the just introduced channel operations are already synchronized, so no further synchronizations are needed to safely perform these operations.

To make the explanations for channel operations simple and clear, in the remaining of this article, channels will be classified into three categories:

  1. nil channels.

  2. non-nil but closed channels.

  3. not-closed non-nil channels.

The following table simply summarizes the behaviors for all kinds of operations applying on nil, closed and not-closed non-nil channels.

Close

panic

panic

succeed to close (C)

Send Value To

block for ever

panic

block or succeed to send (B)

Receive Value From

block forever

never block (D)

block or succeed to receive (A)

Unbuffered channels

By default channels are unbuffered. Every write to an open, unbuffered channel causes the writing goroutine to pause until another goroutine reads from the same channel. Likewise, a read from an open, unbuffered channel causes the reading goroutine to pause until another goroutine writes to the same channel. This means you cannot write to or read from an unbuffered channel without at least two concurrently running goroutines.

Buffered channels

An unbuffered channel is used to perform synchronous communication between goroutines; a buffered channel is used for perform asynchronous communication. Channels can be buffered. Provide the buffer length as the second argument to make to initialise a buffered channel:

ch := make(chan int, 100)

Sends to a buffered channel block only when the buffer is full. Receives block when the buffer is empty.

func main() {
    messages := make(chan string, 2)

    messages <- "buffered"
    messages <- "channel"

    fmt.Println(<-messages)
    fmt.Println(<-messages)
}

Buffered channels are useful when you know how many goroutines you have launched, want to limit the number of goroutines you will launch, or want to limit the amount of work that is queued up.

Buffered channels work great when you want to either gather data back from a set of goroutines that you have launched or when you want to limit concurrent usage.

Close channels

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

close(ch)

Receivers can test whether a channel has been closed by assigning a second parameter to the receive expression:

v, ok := <-ch

ok is false if there are no more values to receive and the channel is closed.

Note: Only the sender should close a channel, never the receiver. Sending on a closed channel will cause a panic.

Channels aren't like files; you don't usually need to close them. Closing is only necessary when the receiver must be told there are no more values coming, such as to terminate a range loop.

A channel can be closed so that no more data can be sent through it. Receiver goroutine can find out the state of the channel using val, ok := <- channel syntax where ok is true if the channel is open or read operations can be performed and false if the channel is closed and no more read operations can be performed.

Once a channel is closed, any attempts to write to the channel or close the channel again will panic. Interestingly, attempting to read from a closed channel always succeeds. If the channel is buffered and there are values that haven’t been read yet, they will be returned in order. If the channel is unbuffered or the buffered channel has no more values, the zero value for the channel’s type is returned.

for-loop for channels

The loop for i := range c receives values from the channel repeatedly until it is closed.

// We'll iterate over 2 values in the `queue` channel.
queue := make(chan string, 2)
queue <- "one"
queue <- "two"
close(queue)

// This `range` iterates over each element as it's
// received from `queue`. Because we `close`d the
// channel above, the iteration terminates after
// receiving the 2 elements.
for elem := range queue {
    fmt.Println(elem)
}

Using buffered channels and for range, we can read from closed channels. Since for closed channels, data lives in the buffer, we can still extract that data.

Channel types

We can use channels to synchronize execution across goroutines.

import "fmt"
import "time"

func worker(done chan bool) {
    fmt.Print("working...")
    time.Sleep(time.Second)
    fmt.Println("done")
    done <- true
}
func main() {
    done := make(chan bool, 1)
    go worker(done)

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

Restrict channel direction

So far, we have seen channels which can transmit data from both sides or in simple words, channels on which we can do read and write operations. But we can also create channels which are unidirectional in nature. For example, receive-only channels which allow only read operation on them and send-only channels which allow only to write operation on them.

The unidirectional channel is also created using make function but with additional arrow syntax.

roc := make(<-chan int)
soc := make(chan<- int)

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

func ping(pings chan<- string, msg string) {
    pings <- msg
}

func pong(pings <-chan string, pongs chan<- string) {
    msg := <-pings
    pongs <- msg
}
func main() {
    pings := make(chan string, 1)
    pongs := make(chan string, 1)

    ping(pings, "passed message")
    pong(pings, pongs)

    fmt.Println(<-pongs)
}

Channels vs Mutexes

In general, parallel goroutines have to synchronize: for example, when they need to access or mutate a shared resource such as a slice. Synchronization is enforced with mutexes but not with any channel types (not with buffered channels). Hence, in general, synchronization between parallel goroutines should be achieved via mutexes.

Conversely, in general, concurrent goroutines have to coordinate and orchestrate. For example, if G3 needs to aggregate results from both G1 and G2, G1 and G2 need to signal to G3 that a new intermediate result is available. This coordination falls under the scope of communication—therefore, channels.

It’s important to know whether goroutines are parallel or concurrent because, in general, we need mutexes for parallel goroutines and channels for concurrent ones.

Channel axioms

A send to a nil channel blocks forever

var c chan string
c <- "Hello, World!"
// fatal error: all goroutines are asleep - deadlock!

A receive from a nil channel blocks forever

var c chan string
fmt.Println(<-c)
// fatal error: all goroutines are asleep - deadlock!

A send to a closed channel panics

var c = make(chan string, 1)
c <- "Hello, World!"
close(c)
c <- "Hello, Panic!"
// panic: send on closed channel

A receive from a closed channel returns the zero value immediately

var c = make(chan int, 2)
c <- 1
c <- 2
close(c)
for i := 0; i < 3; i++ {
  fmt.Printf("%d ", <-c)
}
// 1 2 0

select statement

select is just like switch without any input argument but it only used for channel operations. The select statement is used to perform an operation on only one channel out of many, conditionally selected by case block.

It is the control structure for concurrency in Go, and it elegantly solves a common problem: if you can perform two concurrent operations, which one do you do first? The select keyword allows a goroutine to read from or write to one of a set of multiple channels. Each case in a select is a read or a write to a channel. If a read or write is possible for a case, it is executed along with the body of the case. Like a switch, each case in a select creates its own block.

Unlike a switch statement, where the first case with a match wins, the select statement selects randomly if multiple options are possible.

The select statement is blocking except when it has a default case (we will see that later). Once, one of the case conditions fulfill, it will unblock. So when a case condition fulfills?

If all case statements (channel operations) are blocking then select statement will wait until one of the case statement (its channel operation) unblocks and that case will be executed. If some or all of the channel operations are non-blocking, then one of the non-blocking cases will be chosen randomly and executed immediately. select на nil канале выбросит ошибку

The select statement lets a goroutine wait on multiple communication operations. A select blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple are ready.

for {
    select {
    case c <- x:
        x, y = y, x+y
    case <-quit:
        fmt.Println("quit")
        return
    }
}

Default case

The default case in a select is run if no other case is ready. Use a default case to try a send or receive without blocking:

select {
case i := <-c:
    // use i
default:
    // receiving from c would block
}

Close channel case

However, you need to properly handle closed channels. If one of the cases in a select is reading a closed channel, it will always be successful, returning the zero value. Every time that case is selected, you need to check to make sure that the value is valid and skip the case. If reads are spaced out, your program is going to waste a lot of time reading junk values.

As we saw earlier, reading from or writing to a nil channel causes your code to hang forever. While that is bad if it is triggered by a bug, you can use a nil channel to disable a case in a select. When you detect that a channel has been closed, set the channel’s variable to nil. The associated case will no longer run, because the read from the nil channel never returns a value:

for {
    select {
        case v, ok:= <- in: 
        if !ok { 
                in = nil // the case will never succeed again!
                continue
            }
            // process the v that was read from in
        case v, ok := <-in2: 
            if !ok {
                in2 = nil // the case will never succeed again!
                continue
            }
            // process the v that was read from in2
        case <-done:
            return
    }
}

Timeout select

Any time you need to limit how long an operation takes in Go, you’ll see a variation on this pattern. We have a select choosing between two cases. The first case takes advantage of the done channel pattern we saw earlier. We use the goroutine closure to assign values to result and err and to close the done channel. If the done channel closes first, the read from done succeeds and the values are returned.

timerDuration := 1 * time.Hour
timer := time.NewTimer(timerDuration)
select { 
for {                                     
    timer.Reset(timerDuration)            
    select {
    case event := <-ch:
        handle(event)
    case <-timer.C:                       
        log.Println("warning: no messages received")
    }
}

Non-Blocking Channel Operations

Basic sends and receives on channels are blocking. However, we can use select with a default clause to implement non-blocking sends, receives, and even non-blocking multi-way selects.

Non-Blocking Send

msg := "hi"
    select {
    case messages <- msg:
        fmt.Println("sent message", msg)
    default:
        fmt.Println("no message sent")
    }

Non-blocking Receive

select {
    case msg := <-messages:
        fmt.Println("received message", msg)
    default:
        fmt.Println("no message received")
    }

Last updated