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#
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.
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#
Note: Closing an already closed channel will also cause a panic.