Go 语言中的 channel 是基于 CSP(Communicating Sequential Processes)思想实现的通信机制。其核心理念是:
-
通过通信共享内存,而不是通过共享内存通信
也就是说,应当让不同 goroutine 之间通过传递数据来共享状态,避免直接共享内存而产生竞态问题。
-
FIFO 队列
Channel 本身就是一个队列,按照先进先出的原则传递数据。
-
阻塞与死锁
- 无缓冲通道:发送操作和接收操作必须同时发生,否则将阻塞,容易发生死锁。
- 有缓冲通道:有固定的缓冲区,当缓冲区满时发送操作阻塞;当缓冲区空时接收操作阻塞。
-
数据类型限制
每个 channel 只能存放同一种数据类型,且 channel 是引用类型,需要通过
make
进行初始化。 -
同步通信与协程控制
Channel 不仅用于数据传递,还可以用来实现不同 goroutine 之间的同步控制,比如等待子协程执行完毕再退出主协程。
-
多路复用
使用
select
语句可以同时监听多个 channel 的操作,实现非阻塞通信和超时控制。
1. 无缓冲通道的基本使用
知识点说明:
- 无缓冲通道没有存储空间,发送和接收必须成对出现。当一方操作时,另一方必须就绪,否则就会阻塞。
- 该示例展示了如何在一个 goroutine 中发送数据,通过主 goroutine 接收数据。
代码示例:
go
// 文件名: 03channel_unbuffered.go
package main
import (
"fmt"
"time"
)
// 发送数据到无缓冲通道
func senddata(ch chan int) {
ch <- 1
fmt.Println("往通道里存放数据 1")
ch <- 2
fmt.Println("往通道里存放数据 2")
}
func main() {
// 创建无缓冲通道(容量为0)
ch1 := make(chan int)
// 启动一个 goroutine 发送数据到通道
go senddata(ch1)
// 主 goroutine 休眠一段时间以等待子协程发送数据
time.Sleep(3 * time.Second)
// 按照先进先出的顺序接收数据
data := <-ch1
fmt.Println("拿到通道数据:", data)
data2 := <-ch1
fmt.Println("拿到通道数据:", data2)
// 若再接收数据,由于通道中没有数据,将会阻塞
// data3 := <-ch1
// fmt.Println("第三次拿到通道数据:", data3)
}
2. 利用无缓冲通道实现协程同步
知识点说明:
- 协程同步:在 Go 中,主协程退出会导致整个程序结束。如果想等待子协程执行完毕,可以利用无缓冲通道作为信号量。
- 使用空结构体(
struct{}
)作为信号传递,因为它不占用内存空间。
代码示例:
go
// 文件名: 03channel_sync.go
package main
import (
"fmt"
"time"
)
// 定义空结构体,作为信号传递
type none struct{}
// 发送信号到无缓冲通道,模拟子协程任务完成
func senddata(ch chan none) {
fmt.Println("this is senddata")
time.Sleep(2 * time.Second)
ch <- none{} // 发送一个空结构体信号
}
func main() {
fmt.Println("start....")
// 创建无缓冲通道(类型为 none)
ch1 := make(chan none)
// 启动子协程执行任务
go senddata(ch1)
// 主协程等待接收到信号后再继续
<-ch1
fmt.Println("end....")
}
3. 通道的关闭与数据消费
知识点说明:
- 通道关闭 :发送方完成数据发送后,通过
close(ch)
关闭通道,可以防止消费者无限等待数据。 - 数据消费 :接收方可以通过三种方式判断通道是否关闭:
- 判断读取到的值是否为数据类型的零值;
- 使用
value, ok := <-ch
判断; - 使用
range
自动遍历,直到通道关闭。
代码示例:
go
// 文件名: 03channel_close.go
package main
import (
"fmt"
)
// 发送数据并关闭通道
func sendata(ch chan string) {
for i := 0; i < 3; i++ {
ch <- fmt.Sprintf("发送数据%d", i)
}
// 打印提示并关闭通道,确保接收方不会因无数据而死锁
defer fmt.Println("发送数据完毕")
defer close(ch)
}
func main() {
ch1 := make(chan string)
go sendata(ch1)
// 方式1:读取数据后判断零值(适用于数据类型零值不会作为有效数据的情况)
/*
for {
data := <-ch1
if data == "" { // 字符串的零值为 ""
break
}
fmt.Println("从通道中获取到的数据:", data)
}
*/
// 方式2:使用 value, ok 判断通道是否关闭
/*
for {
data, ok := <-ch1
if !ok {
break
}
fmt.Println("2从通道中获取到的数据:", data)
}
*/
// 方式3:使用 range 遍历通道(推荐方式)
for value := range ch1 {
fmt.Println("3从通道中获取到的数据:", value)
}
}
4. 有缓冲通道的使用
知识点说明:
- 有缓冲通道:在创建通道时指定缓冲区大小,允许发送者先存储一定数量的数据而不必等待接收者。
- 当缓冲区满时,发送操作将阻塞;当缓冲区空时,接收操作将阻塞。
- 适用于生产者和消费者速度不一致的场景。
代码示例:
go
// 文件名: 03channel_buffered.go
package main
import (
"fmt"
"time"
)
// 向有缓冲通道发送数据
func senddata(ch chan string) {
for i := 0; i <= 10; i++ {
ch <- fmt.Sprintf("放入数据%d", i)
fmt.Printf("往通道放入数据%d\n", i)
}
// 发送完成后关闭通道
defer close(ch)
}
func main() {
// 创建一个带缓冲的通道,缓冲区大小为 6
ch1 := make(chan string, 6)
go senddata(ch1)
// 等待一定时间,保证数据发送到缓冲区
time.Sleep(2 * time.Second)
// 使用 range 循环读取数据,直到通道关闭
for v := range ch1 {
fmt.Println("读取通道数据:", v)
}
}
5. 使用 select 监听多个通道
知识点说明:
- select 语句:允许同时监听多个通道操作,当其中一个通道操作准备就绪时执行对应的 case;若多个同时就绪,则随机选择。
- 超时控制 :结合
time.After
实现超时等待,避免长时间阻塞。 - default 分支:用于非阻塞检测,如果没有通道操作准备好,则执行 default 分支。
代码示例:
go
// 文件名: 03channel_select.go
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
ch3 := make(chan int)
// 分别启动三个 goroutine,在不同时间发送数据到各自的通道
go func() {
time.Sleep(10 * time.Second)
ch1 <- 1
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- 2
}()
go func() {
time.Sleep(3 * time.Second)
ch3 <- 3
}()
// 循环监听 3 次 select 中的不同通道或超时/default 分支
for i := 0; i < 3; i++ {
select {
case msg := <-ch1:
fmt.Println("接收到数据 from ch1:", msg)
case msg := <-ch2:
fmt.Println("接收到数据 from ch2:", msg)
case msg := <-ch3:
fmt.Println("接收到数据 from ch3:", msg)
// 超时控制
case <-time.After(1 * time.Second):
fmt.Println("设置超时时间!timeout")
// 非阻塞操作
default:
fmt.Println("没有消息准备好")
}
}
}
总结
本次整理中,我们按照以下知识点对 channel 的使用进行了归类整理:
- 无缓冲通道的基本使用:展示了发送和接收操作必须同步进行的特性。
- 利用无缓冲通道实现协程同步:通过空结构体信号,让主协程等待子协程执行完毕。
- 通道的关闭与数据消费:演示了如何关闭通道以及用 range 自动遍历数据,避免因死锁而阻塞。
- 有缓冲通道的使用:展示了带缓冲区通道在生产者与消费者场景中的使用,以及缓冲满、缓冲空时的阻塞机制。
- select 监听多个通道:通过 select 实现多路复用、超时控制以及非阻塞检测。