Channel Channel Channel

Posted by seavolvox on Wed, 14 Aug 2019 11:22:50 +0200

In the last tutorial, we discussed how to use goroutine to achieve concurrency in Go. In this tutorial, we will discuss channels and how goroutine uses channels to communicate.

What is a channel?

Channels can be considered as channels for goroutine communication. Similar to the way water flows from one end of a pipe to the other, data can be sent from one end and received through channels at the other end.

Declaration channel

Each channel has a type associated with it. This type is the data type that allows channel transmission. It is not allowed to use this channel to transmit other types.

chan T is a channel of type T.

The zero value of the channel is nil. The nil channel is useless, so make must be used to define the channel, similar to map and slice.

<!-- more -->

Let's write some code that declares channels.

func main() {  
    var a chan int
    if a == nil {
        fmt.Println("channel a is nil, going to define it")
        a = make(chan int)
        fmt.Printf("Type of a is %T", a)
    }
}

var a chan int declares a channel, which is nil. The statement in the if condition defines the channel, where the value of the channel is the zero value of int (0).

As usual, abbreviated declarations are also an effective and concise way to define channels.

a := make(chan int)  

Send and receive from channel

The syntax for sending and receiving data from channels is given below.

data := <- a // read from channel a  
a <- data // write to channel a  

The direction of the arrow relative to the channel specifies whether to send or receive data.

In the first line, the arrow is pointed out from a, so we read the value from channel A and store it in variable data.

In the second line, the arrow points to a, so we write to channel a.

By default, sending and receiving are blocked

By default, transmission and reception of channels are blocked. What does that mean? When data is sent to the channel, the sending statement is blocked until other goroutines read from the channel. Similarly, when data is read from a channel, the read is blocked until some goroutines write the data to the channel.

This property of channels helps goroutine s communicate effectively without using explicit locks or conditional variables that are common in other programming languages.

Channel Sample Program

In the previous tutorial about goroutine, we used the Sheep method to let the main program sleep to wait for go hello() to complete. Now let's rewrite the program with channels.

func hello(done chan bool) {
    fmt.Println("hello world goroutine")
    done <- true
}

func main() {
    done := make(chan bool)
    go hello(done)
    <-done
    fmt.Println("main function")
}

In the above program, we created a Boolean channel done and passed it as a parameter to go hello(done). The main program then receives data from the <-done channel, which is blocked, meaning that the code will not move to the next line until goroutine writes the data to the done channel. This prevents the main program goroutine from exiting.

Code <-done receives data from the completed goroutine, but does not assign it to any variable for use or storage. This is perfectly legitimate.

Deadlock deadlock

An important factor to consider when using channels is deadlock. If goroutine is sending data on the channel, it is expected that some other goroutines should receive data. If this does not happen, the program will run in confusion.

func main() {  
    ch := make(chan int)
    ch <- 5
}

In the above program, ch creates a channel, and we send it 5 to the channel ch < - 5. In this program, no other goroutine receives ch data from the channel. Therefore, the following errors will occur when the program runs:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:  
main.main()  
    /tmp/sandbox249677995/main.go:6 +0x80

One-way channel

All communications we have discussed so far are two-way channels, on which data can be sent and received. You can also create a one-way channel, that is, a channel that only sends or receives data.

func sendData(sendch chan<- int) {  
    sendch <- 10
}

func main() {  
    sendch := make(chan<- int)
    go sendData(sendch)
    fmt.Println(<-sendch)
}

In the above program, the first line of the main program creates a send-only channel. Chan<-int indicates that the arrow points to only the sending channel. It is not allowed for us to attempt to receive data only on the sending channel. When the program runs, the compiler will report an error.

invalid operation: <-sendch (receive from send-only type chan<- int)

Because we define channels that can only be sent, we cannot receive data. Bidirectional channels can be converted to send-only or receive-only channels.

The correct code is as follows:

func sendData(sendch chan<- int) {  
    sendch <- 10
}

func main() {  
    chnl := make(chan int)
    go sendData(chnl)
    fmt.Println(<-chnl)
}

Close channel and for range channel

The sender can close the channel to notify the receiver that data is no longer sent on that channel.

The receiver can use additional variables to check whether the channel has been closed when receiving data from the channel.

v, ok := <-ch

If ok is true, the value is successfully received from the channel's send operation. If ok is false, it means that we are reading from the closed channel, and the value read from the closed channel is the zero of the channel type.

package main

import (  
    "fmt"
)

func producer(chnl chan int) {  
    for i := 0; i < 10; i++ {
        chnl <- i
    }
    close(chnl)
}
func main() {  
    ch := make(chan int)
    go producer(ch)
    for {
        v, ok := <-ch
        if ok == false {
            break
        }
        fmt.Println("Received ", v, ok)
    }
}

The for range grammar can also be used to receive values from the channel until it closes.

Let's rewrite the above program using for range.

package main

import (  
    "fmt"
)

func producer(chnl chan int) {  
    for i := 0; i < 10; i++ {
        chnl <- i
    }
    close(chnl)
}
func main() {  
    ch := make(chan int)
    go producer(ch)
    for v := range ch {
        fmt.Println("Received ",v)
    }
}

Rewrite the example of calculating numbers in the previous tutorial:

package main

import (  
    "fmt"
)

func digits(number int, dchnl chan int) {  
    for number != 0 {
        digit := number % 10
        dchnl <- digit
        number /= 10
    }
    close(dchnl)
}
func calcSquares(number int, squareop chan int) {  
    sum := 0
    dch := make(chan int)
    go digits(number, dch)
    for digit := range dch {
        sum += digit * digit
    }
    squareop <- sum
}

func calcCubes(number int, cubeop chan int) {  
    sum := 0
    dch := make(chan int)
    go digits(number, dch)
    for digit := range dch {
        sum += digit * digit * digit
    }
    cubeop <- sum
}

func main() {  
    number := 589
    sqrch := make(chan int)
    cubech := make(chan int)
    go calcSquares(number, sqrch)
    go calcCubes(number, cubech)
    squares, cubes := <-sqrch, <-cubech
    fmt.Println("Final output", squares+cubes)
}

Topics: Go Programming