什么是Channel
Channel可以看作是一个管道,多个goroutine可以通过它发送和接收数据。Channel的设计初衷是为了让goroutine之间进行安全、高效的通信。
Channel的创建
可以使用make函数来创建一个channel。Channel类型需要指定其传输的数据类型,例如:
ch := make(chan int) // 创建一个传输int类型数据的channel
Channel的基本操作
发送
使用 <- 操作符将数据发送到channel。
ch <- 10 // 将10发送到channel ch
接收
使用 <- 操作符从channel接收数据。
value := <-ch // 从channel ch接收数据并赋值给变量value
关闭
使用 close 函数关闭channel,一个被关闭的channel不能再发送数据,但仍可以接收未被读取的数据。
close(ch)
Channel的方向
Channel可以被指定为只发送或只接收。通过限定channel的方向,可以提高代码的可读性和安全性。
func sendOnly(ch chan<- int) {
ch <- 10 // 只能发送数据
}
func receiveOnly(ch <-chan int) {
value := <-ch // 只能接收数据
}
Buffered Channel(带缓冲的Channel)
Channel可以是带缓冲的,这意味着可以在没有接收者读取的情况下,先行存储一部分数据。make函数创建channel时,可以指定缓冲容量。
ch := make(chan int, 2) // 创建一个容量为2的带缓冲channel
ch <- 1
ch <- 2 // 可以存入两个元素而不会阻塞
Select 语句
select 语句可以让goroutine等待多个channel操作,哪个channel先准备好就执行哪个操作。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "two"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("Received", msg1)
case msg2 := <-ch2:
fmt.Println("Received", msg2)
}
}
}
使用Channel的注意事项
- 避免死锁:如果channel的接收和发送操作不能配对,程序会陷入死锁。
- 避免channel泄漏:打开channel后需要必要的时候关闭,以防资源泄漏。
- 数据传递类型一致:确保channel传递的数据类型一致,否则会引发编译错误。
channel底层原理
在Go语言的runtime实现中,channel是通过
hchan结构体表示的.
环形队列(Circular Queue)
对于带缓冲的channel,hchan中包含一个环形队列用于存储暂时未被接收的数据。
buf:这个指针指向实际存储数据的数组。dataqsiz:环形队列的容量。sendx:发送数据的索引。recvx:接收数据的索引。
通过这种设计,数据可以高效地在队列中循环使用,无需频繁地移动内存。
创建channel时的策略
- 如果是无缓冲channel:
直接为hchan结构体分配内存并返回指针。 - 如果是有缓冲channel,但元素不包含指针类型:
一次性为hchan结构体和底层环数组分配连续内存并返回指针(需要连续内存空间)。 - 如果是有缓冲channel,且元素包含指针类型:
则分别分配hchan结构体内存和底层环数组的内存并返回指针(可以利用内存碎片)。
向channel中发送数据的流程
主要分为两大块:边界检查和数据发送。
数据发送流程:
-
如果channel的读等待队列中存在接收者goroutine,则为同步发送:
- 无缓冲channel:不用经过channel,直接将数据发送给第一个等待接收的goroutine,并将其唤醒等待调度。
- 有缓冲channel,但元素个数为0:不用经过channel(假装经过channel)直接将数据发送给第一个等待接收的goroutine,并将其唤醒等待调度。
-
如果channel的读等待队列中不存在接收者goroutine:
- 如果底层环数组未满,则把发送者携带的数据从队尾写入,此为异步发送。
- 如果底层环数组已满或者是无缓冲channel,则将当前goroutine加入写等待队列,并将其挂起,等待被唤醒,此为阻塞发送。