Golang 的 Channel
简介
Golang 中的 channel 是一种非常重要的同步和通信机制,
它允许 goroutines 之间安全地传递数据。
Channel操作速查:
| 通道类型 \ 操作 | 读 | 写 | 关闭 |
|---|---|---|---|
| nil chan | 永久阻塞 | 永久阻塞 | panic |
| 已关 chan | 可读 | panic | panic |
| 活跃 chan | 可读或阻塞 | 可写或阻塞 | 可关 |
注意: 从空通道中仅写入或仅读取数据会永远阻塞。
- channel 写入结束后,要关闭channel,否则空Channel读取数据会永远阻塞。
死锁(Deadlock)是并发编程中常见的一个问题,
它指的是多个进程或线程在执行过程中
因互相等待对方持有的资源而无法继续执行的状态。
浅尝
创建 Channel
ch := make(chan int) // 创建一个无缓冲的整数通道
chBuf := make(chan int, 10) // 创建一个缓冲大小为 10 的整数通道
发送和接收
ch <- 42 // 发送 42 到通道 ch
val := <-ch // 从通道 ch 接收值
val, ok := <-ch // 读取通道 ch,同时返回值和 ok
关闭 Channel
一旦通道不再需要接收新的值,可以关闭它:
close(ch) // 关闭通道 ch
关闭后的通道不能再发送值,但是仍然可以从关闭的通道接收值,直到所有的值都被接收完毕。
判断 Channel 是否已关闭
val, ok := <-ch
if !ok {
fmt.Println("通道已关闭")
}
如果通道已关闭且没有更多的值可以接收,<-ch 会返回零值,同时 ok 为 false。
分类
无缓通道
没有缓冲区的 chan ,在写入后会阻塞,直到有接收。
下面的例子中,仅写入,没有读,所以会永远阻塞,
不会执行到 fmt.Println("写入之后")
无缓:写一次,读一次,只读不写、只写不读都会阻塞
// 无缓,仅写入,协程中阻塞在写入行,进行不到下一行
func TestChanNoBuf(t *testing.T) {
ch := make(chan int)
go func() {
fmt.Println("写入之前")
ch <- 1
fmt.Println("写入之后")
}()
time.Sleep(time.Second)
}
有缓通道
缓冲 channel 有一个固定的缓冲区,即使没有接收者的情况下存储一定数量的消息。
在缓冲区未满时,发送操作不会阻塞;
在缓冲区未空时,接收操作也不会阻塞。
chBuf := make(chan int, 2)
chBuf <- 1
chBuf <- 2 // 此时通道已满,再发送会阻塞
尝试
// 有缓
func TestChanBuf(t *testing.T) {
ch := make(chan int, 5)
go func() {
for i := 0; i < 100; i++ {
// time.Sleep(time.Second) // 造成慢写,此时快读,使读时阻塞
fmt.Println("写入前", i)
ch <- i
fmt.Println("写入后", i)
fmt.Println()
}
close(ch)
}()
for {
fmt.Println("读前")
// time.Sleep(time.Second * 2) // 造成慢读,此时写快,使写时阻塞
d, ok := <-ch
fmt.Println("读出", d, ok)
if !ok {
break
}
}
}
有缓:chan 可以被写入数量为缓冲区数量,
写满继续写,会阻塞,等待读。--写快读慢。
读空继续读,会阻塞,等待写。--写慢读快。
缓冲区有空未才可继续写。
其实就是 chan 的特性,读写都会阻塞,等待写读。
单向
仅读通道
定义一个仅读类型 接受者 Receiver
// 定义
type Receiver <-chan int
// 使用
receiver := Receiver(ch)
如何快速记住 <-chan int 呢?
chan 在 <- 右边 就是读.
下面 向 ch 中写入10个数字,使用仅读的 receiver 读取
func TestChanReader(t *testing.T) {
ch := make(chan int, 2)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()
receiver := Receiver(ch)
// receiver <- 1 // receiver 为仅读,不可写入,会有错误提示
for {
d, ok := <-receiver
fmt.Println(d)
if !ok {
break
}
}
}
receiver 因为初始化时使用了 仅接收模式的 Receiver 作为类型,所以不能写
-
提问:为什么
close了ch,receiver也被关闭了呢?因为 chan 是引用类型,使用 ch 初始化
receiver,
receiver和ch指向同一个地址,所以关闭ch,receiver也会关闭。
仅写通道
定义一个仅写类型 发送者 Sender
// 定义
type Sender chan<- int
// 使用
sender := Sender(ch)
快速记住 chan<- int
chan 在 <- 左边 就是写.
下面使用 Sender 类型 发送数据
func TestChanSender(t *testing.T) {
ch := make(chan int, 2)
sender := Sender(ch)
go func() {
for i := 0; i < 10; i++ {
sender <- i
}
close(sender)
}()
// <-sender // 此时 sender 为仅写,不能读,会有错误提示
for {
d, ok := <-ch
fmt.Println(d, ok)
if !ok {
break
}
}
}
sender 因为初始化时使用了 仅接收模式的 Sender 作为类型,所以不能写
- 提问:为什么 close 了 sender,ch 也被关闭了呢?
同上。
因为 chan 是引用类型,使用ch初始化sender,
sender和ch指向同一个地址,所以关闭sender,ch也会关闭。
而sender 因为初始化时使用了 仅发送模式的Sender作为类型,所以不能读
单向合璧
结合发送者 Sender 和 Receiver 类型:
func TestChanSenderReceiver(t *testing.T) {
ch := make(chan int, 1)
receiver := Receiver(ch)
sender := Sender(ch)
// 写入
go func() {
for i := 0; i < 10; i++ {
sender <- i
}
close(sender)
}()
// 读
for {
d, ok := <-receiver
if !ok {
break
}
fmt.Println("读出", d)
}
}
双向
最普通的就是双向,比如
ch := make(chan int)
出错情况
nil 通道
对 nil 通道进行读写操作会导致当前 goroutine 永久阻塞,
最终可能触发死锁错误(如果所有 goroutine 都被阻塞)。
func TestChanNilRead(t *testing.T) {
var ch chan int // ch 是 nil 通道
defer close(ch) // panic: close of nil channel
// 读
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Println(r) // 此 协程中未发生panic,读时仅阻塞
}
}()
fmt.Println("等待接收")
fmt.Println(<-ch) // 读不出来,因为写入时阻塞
}()
// 写入
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Println(r) // 此 协程中未发生panic,写入时仅阻塞
}
}()
fmt.Println("等待写入")
ch <- 1 // 尝试向 nil 通道发送数据
fmt.Println("已经写入") // 这行不会执行
}()
// 若在主协程中阻塞,将发生死锁
// 主协程等待
time.Sleep(1 * time.Second)
fmt.Println("主协程结束")
}
| 操作 | 读 | 写 | 关 |
|---|---|---|---|
| 空通道 | 永久阻塞 | 永久阻塞 | panic |
已关通道
// 关闭的 chan ,只能读,不能写入、关闭
func TestChanClosed(t *testing.T) {
ch := make(chan int, 1)
close(ch)
// close(ch) // 重复关闭: panic: close of closed channel
// ch <- 1 // 写: panic: send on closed channel
d, ok := <-ch // 读 没问题
fmt.Println(d, ok) // 读: 零值0 false
}
| 操作 | 读 | 写 | 关 |
|---|---|---|---|
| 已关 chan | 可读 | panic | panic |
活跃通道
// 活跃的 chan 读写关没问题
func TestChanActive(t *testing.T) {
ch := make(chan int, 1)
ch <- 1 // 写
d, ok := <-ch // 读
fmt.Println(d, ok) // 1 true
close(ch) // 关闭
}
| 操作 | 读 | 写 | 关 |
|---|---|---|---|
| 活跃通道 | 可读或阻塞 | 可写或阻塞 | 可关 |
Select
select 可以同时等待多个通道操作,包括发送和接收。
func TestChanSelect(t *testing.T) {
// 初始化 chan 并写入
ch := make(chan int, 10)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
fmt.Println("已经关闭")
}()
// 计时器
timer := time.NewTimer(time.Second * 10)
for {
time.Sleep(time.Millisecond * 500)
select {
case d, ok := <-ch: // 读
fmt.Println(d, ok)
if !ok {
fmt.Println("chan 已关闭")
return
}
case ch <- 123: // 写入 , 需确保 chan 不会 close
fmt.Println("写入成功")
case <-timer.C: // 超时
fmt.Println("超时")
// close(ch)
default: // 无case触发
fmt.Println("无操作")
return
}
}
}
select 的特性
-
随机性:
当多个 case 可以执行时,select 会随机选择其中一个执行。
select 会公平地选择可执行的 case,而不是总是选择第一个可执行的 case。
这种随机选择机制有助于避免死锁和其他并发问题。 -
阻塞性:
如果所有 case 都不可执行,则 select 会阻塞,直到有一个 case 可以执行。
比如select {},会永远阻塞。 -
default:
如果提供了 default 分支,则在所有 case 都不可执行时执行 default 分支。
Range 迭代 Channel
可以使用 for range 循环迭代通道中的所有值,直到通道关闭:
func TestChanRange(t *testing.T) {
ch := make(chan int, 1)
go func() {
for i := 0; i < 3; i++ {
ch <- i
fmt.Println("写入", i)
}
close(ch) // 写入完成后 close 是个好习惯
}()
// range
for d := range ch {
fmt.Println("读出", d)
}
fmt.Println("range 结束") // 可以执行到此行
}
- range 时也是读,所以当 chan 为空时会阻塞,直到有数据可读或通道关闭
所以如果 chan 停止写入且未关闭,range 读完后会阻塞,并最终导致 死锁 - chan 关闭,range 读完会退出循环。
- chan 写入完成后 close 是个好习惯。
Channel 与并发
Fan-In:多个 goroutine 向同一个通道发送数据。
Fan-Out:一个 goroutine 向多个通道发送数据。
Worker Pool:使用通道来管理一组工作 goroutine。
Deadlock
当 goroutine 尝试向一个没有接收者的通道发送数据,或者尝试从一个没有发送者的通道接收数据时,会发生死锁。使用缓冲通道或确保有对应的接收者/发送者可以避免死锁。
总结
Channel 是 Golang 的并发编程中的核心组件之一,它提供了 goroutines 之间安全的数据交换机制。
通过合理使用通道,可以有效地实现并发控制和数据同步。理解和熟练掌握通道的使用对于编写高效、可靠的 Go 程序至关重要。


