banner
露娜SAMA

露娜SAMA的小站

大闲人,超级二次元,喜欢音游、二游、美少女游戏,Ciallo~(∠・ω<)⌒★

Golang Concurrency Programming - Channel

channel#

Simply executing functions concurrently is meaningless. Functions need to exchange data to reflect the significance of concurrent execution.

Although shared memory can be used for data exchange, it is prone to race conditions in different goroutines. To ensure the correctness of data exchange, mutexes must be used to protect the memory, which inevitably leads to performance issues.

The concurrency model of Go is CSP (Communicating Sequential Processes), which achieves communication through communication rather than sharing memory.

If goroutines are the execution bodies of concurrent Go programs, channels are the connections between them. A channel is a communication mechanism that allows one goroutine to send specific values to another goroutine.

In Go, a channel is a special type. A channel acts like a conveyor belt or queue, always following the first-in-first-out principle, ensuring the order of data transmission and reception. Each channel is a conduit of a specific type, meaning that when creating a channel, you need to specify the element type.

channel type#

A channel is a type, a reference type. The format for declaring a channel type is as follows:

var variable chan elementType

Here are a few examples:

var ch1 chan int // Declare a channel that transmits integers
var ch2 chan bool  // Declare a channel that transmits booleans
var ch3 chan []int // Declare a channel that transmits slices of integers

Creating a channel#

A channel is a reference type, and the zero value of a channel type is nil.

var ch chan int
fmt.Println(ch) // nil

Declared channels need to be initialized with the make function before use.

The format for creating a channel is as follows:

make(chan elementType, [bufferSize])

The buffer size of a channel is optional.

ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

Channel operations#

Channels have three operations: send, receive, and close.

Both sending and receiving use the <- symbol.

Now let's define a channel with the following statement:

ch := make(chan int)

Sending#

Send a value into the channel.

ch <- 10

Receiving#

Receive a value from a channel.

x := <- ch // Receive value from ch and assign it to variable x
<-ch // Receive value from ch, ignoring the result

Closing#

We close the channel by calling the built-in close function.

close(ch)

It is important to note that a channel should only be closed when notifying the receiving goroutine that all data has been sent. Channels can be garbage collected, which is different from closing files; closing a file after finishing operations is mandatory, but closing a channel is not.

A closed channel has the following characteristics:

Sending a value to a closed channel will cause a panic.
Receiving from a closed channel will continue to get values until the channel is empty.
Receiving from a closed channel that has no values will yield the zero value of the corresponding type.
Closing an already closed channel will cause a panic.

Unbuffered channels#

image

Unbuffered channels are also known as blocking channels. Let's look at the following code:

func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Println("Send successful")
}

The above code can compile, but it will produce the following error during execution:

fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:

main.main()
     .../src/github.com/pprof/studygo/day06/channel02/main.go:8 +0x54

Why does the deadlock error occur?

Because we created an unbuffered channel with ch := make(chan int), an unbuffered channel can only send values when there is someone to receive them. It's like living in a community without a delivery locker or collection point; the courier must hand the item directly to you. In simple terms, an unbuffered channel must have a receiver to send.

The code above will block at ch <- 10, causing a deadlock. How can we solve this problem?

One way is to enable a goroutine to receive the value, for example:

func recv(c chan int) {
    ret := <-c
    fmt.Println("Receive successful", ret)
}

func main() {
    ch := make(chan int)
    go recv(ch) // Enable goroutine to receive value from the channel
    ch <- 10
    fmt.Println("Send successful")
}

Buffered channels#

Another way to solve the above problem is to use buffered channels.

image

We can specify the capacity of the channel when initializing it with the make function, for example:

func main() {
    ch := make(chan int, 1) // Create a buffered channel with a capacity of 1
    ch <- 10
    fmt.Println("Send successful")
}

As long as the channel's capacity is greater than zero, it is a buffered channel. The channel's capacity indicates the number of elements it can hold. It's like your community's delivery locker has a limited number of slots; when full, it cannot accept more, blocking until someone retrieves an item.

close()#

You can close a channel using the built-in close() function (if your channel is not storing or retrieving values, remember to close it).

package main

import "fmt"

func main() {
    c := make(chan int)
    go func() {
        for i := 1; i < 5; i++ {
            c <- 1
        }
        close(c)
    }()
    for {
        if data, ok := <-c; ok {
            fmt.Println(data)
        } else {
            break
        }
    }
    fmt.Println("main ends")
}

How to elegantly loop through values from a channel#

When sending a finite amount of data through a channel, we can close the channel using the close function to inform the receiving goroutine to stop waiting. When the channel is closed, sending values to that channel will cause a panic, and receiving values from that channel will always yield the zero value of the type. So how can we determine if a channel has been closed?

Let's look at the following example:

// channel exercise
func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    // Start a goroutine to send numbers 0 to 100 into ch1
    go func() {
        for i := 0; i < 100; i++ {
            ch1 <- i
        }
        close(ch1)
    }()
    // Start a goroutine to receive values from ch1 and send their squares to ch2
    go func() {
        for {
            i, ok := <-ch1 // ok=false when the channel is closed
            if !ok {
                break
            }
            ch2 <- i * i
        }
        close(ch2)
    }()
    // In the main goroutine, receive values from ch2 and print them
    for i := range ch2 { // The for range loop will exit when the channel is closed
        fmt.Println(i)
    }
}

From the above example, we see two ways to check if a channel is closed while receiving values; we typically use the for range method.

One-way channels#

Sometimes we pass channels as parameters between multiple task functions, and often we restrict the channel to only send or only receive within different task functions.

Go provides one-way channels to handle this situation. For example, we can modify the above example as follows:

func counter(out chan<- int) {
    for i := 0; i < 100; i++ {
        out <- i
    }
    close(out)
}

func squarer(out chan<- int, in <-chan int) {
    for i := range in {
        out <- i * i
    }
    close(out)
}
func printer(in <-chan int) {
    for i := range in {
        fmt.Println(i)
    }
}

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go counter(ch1)
    go squarer(ch2, ch1)
    printer(ch2)
}

In this case,

chan <- int is a send-only channel, which can send but not receive;
<-chan int is a receive-only channel, which can receive but not send.

It is possible to convert a bidirectional channel to a one-way channel in function parameters and any assignment operations, but the reverse is not allowed.

Common exceptions#

image

Note: Closing an already closed channel will also cause a panic.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.