Go语言Channel通道
- 前言:
- 一、Channel通道初识
-
- [1. Channel是什么?](#1. Channel是什么?)
- [2. Channel的定义与创建](#2. Channel的定义与创建)
-
- [2.1 定义Channel](#2.1 定义Channel)
- [2.2 创建Channel](#2.2 创建Channel)
- [2.3 最简单的Channel使用示例](#2.3 最简单的Channel使用示例)
- 二、Channel的核心操作
-
- [1. 发送数据](#1. 发送数据)
- [2. 接收数据](#2. 接收数据)
-
- [2.1 基础接收写法](#2.1 基础接收写法)
- [2.2 带状态判断的接收写法](#2.2 带状态判断的接收写法)
- [3. 关闭通道](#3. 关闭通道)
-
- [3.1 关闭通道示例](#3.1 关闭通道示例)
- [3.2 for-range简化读取](#3.2 for-range简化读取)
- 三、无缓冲Channel与有缓冲Channel的区别
-
- [1. 核心特性对比](#1. 核心特性对比)
- [2. 阻塞规则](#2. 阻塞规则)
-
- [2.1 无缓冲Channel](#2.1 无缓冲Channel)
- [2.2 有缓冲Channel](#2.2 有缓冲Channel)
- [3. 示例对比](#3. 示例对比)
-
- [3.1 无缓冲Channel示例](#3.1 无缓冲Channel示例)
- [3.2 有缓冲Channel示例](#3.2 有缓冲Channel示例)
- 四、Channel的死锁问题
-
- [1. 死锁的定义](#1. 死锁的定义)
- [2. 常见的死锁场景](#2. 常见的死锁场景)
-
- [2.1 单goroutine操作无缓冲Channel](#2.1 单goroutine操作无缓冲Channel)
- [2.2 Channel只发不收/只收不发](#2.2 Channel只发不收/只收不发)
- [2.3 缓冲区满且无接收方](#2.3 缓冲区满且无接收方)
- [3. 避免死锁的核心原则](#3. 避免死锁的核心原则)
- 五、定向Channel
-
- [1. 定向Channel的定义](#1. 定向Channel的定义)
- [2. 定向Channel的使用示例](#2. 定向Channel的使用示例)
- [3. 定向Channel的使用场景](#3. 定向Channel的使用场景)
- 六、select多路复用
-
- [1. select的基本语法](#1. select的基本语法)
- [2. select的核心规则](#2. select的核心规则)
- [3. select的使用示例](#3. select的使用示例)
-
- [3.1 监听多个Channel](#3.1 监听多个Channel)
- [3.2 超时控制](#3.2 超时控制)
- 七、Channel的应用场景
-
- [1. goroutine间通信](#1. goroutine间通信)
- [2. 生产者-消费者模式](#2. 生产者-消费者模式)
- [3. 工作池模式](#3. 工作池模式)
- [4. 超时控制](#4. 超时控制)
- 八、Timer定时器与Channel
-
- [1. Timer定时器](#1. Timer定时器)
- [2. Ticker打点器](#2. Ticker打点器)
- [3. time.After](#3. time.After)
- 总结
前言:
上期内容为大家带来了Go语言goroutine协程的知识点学习,这期内容我将为大家带来Go语言中核心的并发通信机制------Channel通道的学习,这部分内容是实现goroutine之间安全通信的关键,也是Go语言"不要通过共享内存来通信,而要通过通信来共享内存"设计理念的核心体现。
一、Channel通道初识
1. Channel是什么?
Channel(通道)是Go语言中各个并发执行体(goroutine)之间的通信机制,我们可以把它理解成goroutine之间通信的"管道",就像生活中水管传递水一样,Channel可以在goroutine之间传递数据。
Channel是类型相关的,也就是说一个Channel只能传递指定类型的数据,比如传递int类型的Channel就不能传递string类型的数据。并且Channel的核心作用是在多个goroutine之间传递数据和同步执行,解决多goroutine并发访问数据的安全问题。
2. Channel的定义与创建
2.1 定义Channel
在Go语言中,定义Channel的语法格式如下:
go
var 通道名 chan 数据类型
比如定义一个传递int类型数据的Channel:
go
var ch chan int
此时定义的ch是一个Channel类型的变量,默认值为nil,还不能直接使用,需要通过make函数初始化。
2.2 创建Channel
创建Channel必须使用make函数,语法格式:
go
通道名 = make(chan 数据类型, [缓冲区大小])
根据是否设置缓冲区,Channel分为两种:
- 无缓冲Channel:不指定缓冲区大小,缓冲区容量为0
go
// 无缓冲Channel
var ch1 chan int
ch1 = make(chan int)
// 简化写法
ch1 := make(chan int)
- 有缓冲Channel:指定缓冲区大小,缓冲区容量为指定的数值
go
// 有缓冲Channel,容量为10
var ch2 chan int
ch2 = make(chan int, 10)
// 简化写法
ch2 := make(chan int, 10)
2.3 最简单的Channel使用示例
我们先通过一个简单示例感受Channel的基本使用,实现子goroutine向主goroutine传递消息:
go
package main
import (
"fmt"
"time"
)
func main() {
// 定义并初始化一个bool类型的无缓冲Channel
var ch chan bool
ch = make(chan bool)
// 启动一个子goroutine
go func() {
for i := 0; i < 10; i++ {
fmt.Println("goroutine-", i)
}
// 模拟耗时操作
time.Sleep(time.Second * 3)
// 向Channel中发送数据,告知主goroutine子goroutine执行完成
ch <- true
}()
// 从Channel中接收数据,主goroutine会阻塞等待,直到有数据传入
data := <-ch
fmt.Println("ch data:", data)
}

从结果可以看到,主goroutine会一直等待子goroutine执行完成并向Channel发送数据后,才会继续执行。
二、Channel的核心操作
Channel有三个核心操作:发送数据、接收数据、关闭通道,下面我们分别讲解。
1. 发送数据
向Channel发送数据的语法格式:
go
通道名 <- 数据
其中<-是Channel的发送运算符,数据会被发送到Channel中。
go
// 向ch1发送int类型数据42
ch1 := make(chan int)
ch1 <- 42
注意:发送数据的类型必须和Channel定义的类型一致,否则会编译报错。
2. 接收数据
从Channel接收数据有两种常见写法:
2.1 基础接收写法
go
变量 := <-通道名
<-是Channel的接收运算符,会从Channel中取出数据并赋值给变量。
go
ch1 := make(chan int)
// 先启动goroutine发送数据,避免死锁
go func() {
ch1 <- 42
}()
// 接收数据
value := <-ch1
fmt.Println("接收的数据:", value) // 输出:接收的数据:42

2.2 带状态判断的接收写法
go
变量, ok := <-通道名
这种写法可以判断Channel是否已经关闭且没有数据:
- ok为true:表示成功接收到数据
- ok为false:表示Channel已经关闭且缓冲区中没有数据
go
ch1 := make(chan int)
go func() {
ch1 <- 42
close(ch1) // 关闭通道
}()
// 第一次接收
value1, ok1 := <-ch1
fmt.Println("value1:", value1, "ok1:", ok1)
// 第二次接收(通道已关闭且无数据)
value2, ok2 := <-ch1
fmt.Println("value2:", value2, "ok2:", ok2)

3. 关闭通道
关闭Channel使用close函数,语法格式:
go
close(通道名)
关闭通道后有以下特性:
- 不能再向关闭的Channel发送数据,否则会panic
- 可以继续从关闭的Channel接收数据,直到缓冲区数据全部取完
- 从已关闭且无数据的Channel接收数据,会得到该类型的零值,且ok为false
3.1 关闭通道示例
go
package main
import (
"fmt"
"time"
)
func test7(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i // 向通道发送数据
}
close(ch) // 发送完数据后关闭通道
}
func main() {
ch1 := make(chan int)
go test7(ch1) // 启动goroutine发送数据
// 循环读取通道数据
for {
time.Sleep(time.Second)
data, ok := <-ch1
if !ok {
fmt.Println("读取完毕", ok)
break
}
fmt.Println("ch1 data:", data)
}
}

3.2 for-range简化读取
Go语言提供了for-range语法,可以简化Channel的读取操作,它会自动判断Channel是否关闭,当Channel关闭且数据读取完毕后,循环会自动退出:
go
package main
import (
"fmt"
"time"
)
func test7(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}
func main() {
ch1 := make(chan int)
go test7(ch1)
// for-range读取通道数据
for data := range ch1 {
time.Sleep(time.Second)
fmt.Println("ch1 data:",data)
}
fmt.Println("读取完毕")
}

运行结果和上面的示例一致,代码更加简洁。
三、无缓冲Channel与有缓冲Channel的区别
这是Channel最核心的知识点之一,我们从特性、阻塞规则、使用场景三个维度详细讲解。
1. 核心特性对比
| 特性 | 无缓冲Channel | 有缓冲Channel |
|---|---|---|
| 缓冲区容量 | 0 | 指定数值(大于0) |
| 通信模型 | 同步通信 | 异步通信 |
| 数据存储 | 无存储,直接传递 | 先存储在缓冲区,按需读取 |
2. 阻塞规则
2.1 无缓冲Channel
无缓冲Channel也叫同步Channel,发送和接收操作必须同时准备好:
- 发送数据时:会阻塞当前goroutine,直到有其他goroutine从该Channel接收数据
- 接收数据时:会阻塞当前goroutine,直到有其他goroutine向该Channel发送数据
简单理解:无缓冲Channel就像两个人面对面传球,传球的人(发送)必须等接球的人(接收)准备好,否则传球的人只能一直举着球等待。
2.2 有缓冲Channel
有缓冲Channel也叫异步Channel,阻塞规则和缓冲区状态相关:
- 发送数据时:只有当缓冲区满了,发送操作才会阻塞;缓冲区未满时,发送操作不会阻塞
- 接收数据时:只有当缓冲区空了,接收操作才会阻塞;缓冲区有数据时,接收操作不会阻塞
简单理解:有缓冲Channel就像快递柜,快递员(发送方)把包裹放进柜子(缓冲区),不用等收件人(接收方)当场取;只有柜子满了,快递员才需要等待;收件人随时可以取,只有柜子空了才需要等待。
3. 示例对比
3.1 无缓冲Channel示例
go
package main
import "fmt"
func main() {
ch := make(chan int) // 无缓冲Channel
// 启动goroutine接收数据
go func() {
fmt.Println("接收方准备接收")
value := <-ch
fmt.Println("接收方收到数据:", value)
}()
fmt.Println("发送方准备发送")
ch <- 100 // 无缓冲,会阻塞直到接收方准备好
fmt.Println("发送方发送完成")
}

3.2 有缓冲Channel示例
go
package main
import "fmt"
func main() {
ch := make(chan int, 3) // 有缓冲Channel,容量3
fmt.Println("初始状态:cap=", cap(ch), "len=", len(ch)) // cap=3 len=0
ch <- 1
fmt.Println("发送1后:cap=", cap(ch), "len=", len(ch)) // cap=3 len=1
ch <- 2
ch <- 3
fmt.Println("发送3后:cap=", cap(ch), "len=", len(ch)) // cap=3 len=3
// 缓冲区已满,再发送会阻塞
// ch <- 4 // 取消注释会导致死锁
// 接收数据
value := <-ch
fmt.Println("接收数据:", value) // 接收数据:1
fmt.Println("接收后:cap=", cap(ch), "len=", len(ch)) // cap=3 len=2
}

四、Channel的死锁问题
死锁是使用Channel时最容易踩的坑,Go程序运行时会直接panic,提示fatal error: all goroutines are asleep - deadlock!。
1. 死锁的定义
当所有goroutine都处于阻塞状态,没有任何一个goroutine可以继续执行,程序就会发生死锁。
2. 常见的死锁场景
2.1 单goroutine操作无缓冲Channel
go
package main
func main() {
ch := make(chan int)
ch <- 10 // 死锁!只有main一个goroutine,发送后阻塞,无接收方
}
2.2 Channel只发不收/只收不发
go
package main
func main() {
ch := make(chan int)
go func() {
ch <- 10 // 子goroutine发送数据后阻塞,无接收方
}()
// main goroutine直接退出,子goroutine永久阻塞,导致死锁
}
2.3 缓冲区满且无接收方
go
package main
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
ch <- 3 // 缓冲区满,无接收方,死锁
}
3. 避免死锁的核心原则
- 无缓冲Channel必须保证发送和接收操作在不同goroutine中
- 有缓冲Channel要控制发送数据量,避免缓冲区满且无接收方
- 发送方负责关闭Channel,接收方通过for-range或ok判断处理关闭状态
- 复杂场景可以配合select+超时机制避免永久阻塞
五、定向Channel
默认的Channel是双向的,既可以发送数据也可以接收数据。Go语言还支持定向Channel,即只能发送或只能接收的Channel,主要用于函数参数约束,避免Channel滥用。
1. 定向Channel的定义
- 只写Channel:
chan<- 数据类型,只能发送数据,不能接收数据 - 只读Channel:
<-chan 数据类型,只能接收数据,不能发送数据
2. 定向Channel的使用示例
go
package main
import (
"fmt"
"time"
)
// 只写Channel作为函数参数,限制函数只能发送数据
func writeOnly(ch chan<- int) {
ch <- 100 // 合法:发送数据
// value := <-ch // 非法:不能从只写Channel接收数据,编译报错
}
// 只读Channel作为函数参数,限制函数只能接收数据
func readOnly(ch <-chan int) int {
data := <-ch // 合法:接收数据
fmt.Println("只读函数接收数据:", data)
// ch <- 200 // 非法:不能向只读Channel发送数据,编译报错
return data
}
func main() {
ch1 := make(chan int) // 双向Channel
go writeOnly(ch1) // 双向Channel自动转换为只写Channel
go readOnly(ch1) // 双向Channel自动转换为只读Channel
time.Sleep(time.Second * 3)
}

3. 定向Channel的使用场景
- 函数参数约束:明确函数对Channel的操作权限,避免误操作
- 团队协作:规范Channel的使用方式,提高代码可读性和可维护性
- 大型项目:减少因Channel滥用导致的bug
六、select多路复用
select是Go语言中专门用于处理Channel操作的关键字,可以同时监听多个Channel的发送/接收操作,实现多路复用。
1. select的基本语法
go
select {
case 通道操作1:
// 操作1就绪时执行的逻辑
case 通道操作2:
// 操作2就绪时执行的逻辑
default:
// 所有通道操作都未就绪时执行的逻辑
}
2. select的核心规则
- 每个case必须是Channel的发送或接收操作
- 多个case同时就绪时,随机选择一个执行(不是按顺序)
- 没有case就绪且有default时,执行default逻辑,不阻塞
- 没有case就绪且无default时,阻塞直到某个case就绪
3. select的使用示例
3.1 监听多个Channel
go
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
time.Sleep(time.Second * 2)
ch1 <- 100
}()
go func() {
time.Sleep(time.Second * 1)
ch2 <- 200
}()
// 监听两个Channel
select {
case num1 := <-ch1:
fmt.Println("从ch1接收:", num1)
case num2 := <-ch2:
fmt.Println("从ch2接收:", num2)
// default:
// fmt.Println("default")
}
}
3.2 超时控制
结合time.After实现Channel操作的超时控制,避免永久阻塞:
go
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
// 子goroutine 3秒后发送数据
go func() {
time.Sleep(time.Second * 3)
ch <- 100
}()
// 只等待1秒,超时则执行default
select {
case value := <-ch:
fmt.Println("成功接收:", value)
case <-time.After(time.Second * 1):
fmt.Println("接收超时!")
}
}
七、Channel的应用场景
Channel是Go语言并发编程的核心,常见的应用场景有:
1. goroutine间通信
最基础的场景,实现goroutine之间的数据传递和同步,比如我们前面的示例。
2. 生产者-消费者模式
这是最经典的并发模式,生产者goroutine生成数据并发送到Channel,消费者goroutine从Channel读取数据并处理。
go
// 编程实例:电商订单处理 (生产者---消费者模型)
type Order struct {
ID int
UserID string
amount float64
status string
createdAt time.Time
}
// 产生订单 --- 生产者
func orderProduct(orderChan chan Order, number int) {
defer close(orderChan)
for i := 0; i < number; i++ {
order := Order{
ID: i,
UserID: fmt.Sprintf("user_%d", rand.Intn(100)),
amount: rand.ExpFloat64() * 1000,
status: "pending",
createdAt: time.Now(),
}
orderChan <- order
fmt.Printf("生成订单:ID=%d,用户ID=%s,金额=%.2f\n", order.ID, order.UserID, order.amount)
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
}
}
// 处理订单 --- 消费者
// <-chan 只接收类型为Order的channel
// chan<- 只发送类型为Order的channel
func orderProcessor(orderChan <-chan Order, resultChan chan<- Order) {
defer close(resultChan)
for order := range orderChan {
fmt.Printf("处理订单:ID=%d,用户ID=%s,金额=%.2f\n", order.ID, order.UserID, order.amount)
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
order.status = "completed"
resultChan <- order
}
}
// 收集处理结果 --- 消费者
func orderResultCollector(resultChan <-chan Order, done chan<- struct{}) {
for order := range resultChan {
fmt.Printf("订单处理完成:ID=%d,用户ID=%s,金额=%.2f,状态=%s\n", order.ID, order.UserID, order.amount, order.status)
}
done <- struct{}{}
}
func main() {
rand.Seed(time.Now().UnixNano())
// 创建管道
orderChan := make(chan Order, 10)
resultChan := make(chan Order, 10)
done := make(chan struct{})
// 启动订单生成器
go orderProduct(orderChan, 20)
// 启动多个订单处理器(工人)
for i := 1; i <= 3; i++ {
go orderProcessor(orderChan, resultChan)
}
// 启动结果收集器
go orderResultCollector(resultChan, done)
// 等待所有处理完成
<-done
}
3. 工作池模式
通过Channel控制goroutine的数量,实现任务的批量处理,避免创建过多goroutine导致资源耗尽。
go
package main
import (
"fmt"
"time"
)
// 工作goroutine
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("Worker %d 处理任务 %d\n", id, j)
time.Sleep(time.Second)
results <- j * 2
}
close(results)
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// 启动4个worker
for w := 0; w <= 3; w++ {
go worker(w, jobs, results)
}
// 发送100个任务
for j := 0; j < 100; j++ {
jobs <- j
}
close(jobs)
// 收集结果
for value := range results {
fmt.Println("result:", value)
}
}
4. 超时控制
结合select和time.After实现操作的超时控制,比如网络请求、数据库查询等,模板示例:
go
package main
import (
"fmt"
"time"
)
var done = make(chan struct{})
func event() {
fmt.Println("event执行开始")
time.Sleep(4 * time.Second) //4秒
fmt.Println("event执行结束")
close(done)
}
func main() {
go event()
select {
case <-done:
fmt.Println("协程执行完毕")
case <-time.After(3 * time.Second):
fmt.Println("超时")
return
}
}

八、Timer定时器与Channel
Go语言的time包中的定时器(Timer)和打点器(Ticker)都是基于Channel实现的,是Channel的重要应用场景。
1. Timer定时器
Timer表示一个单次的定时事件,时间到了会向Channel发送当前时间。
go
package main
import (
"fmt"
"time"
)
func main() {
// 创建定时器,3秒后触发
timer := time.NewTimer(time.Second * 3)
fmt.Println("当前时间:", time.Now())
// 等待定时器触发
t := <-timer.C
fmt.Println("定时器触发:", t)
// 提前停止定时器(注释上面的<-timer.C才能测试)
// timer.Stop()
// fmt.Println("定时器已停止")
}

2. Ticker打点器
Ticker表示一个重复的定时事件,每隔指定时间就会向Channel发送当前时间。
go
package main
import (
"fmt"
"time"
)
func main() {
// 创建打点器,每隔500毫秒触发一次
ticker := time.NewTicker(500 * time.Millisecond)
done := make(chan bool)
go func() {
for {
select {
case <-done:
return
case t := <-ticker.C:
fmt.Println("定时触发 at", t.Format("15:04:05"))
}
}
}()
// 运行2秒后停止
time.Sleep(2 * time.Second)
ticker.Stop()
done <- true
fmt.Println("Ticker停止")
}

3. time.After
time.After是Timer的简化版,返回一个Channel,指定时间后会向该Channel发送当前时间。
go
package main
import (
"fmt"
"time"
)
func main() {
// 3秒后向Channel发送时间
afterChan := time.After(time.Second * 3)
fmt.Println("等待3秒...")
t := <-afterChan
fmt.Println("3秒到了:", t)
// 延迟执行函数
time.AfterFunc(time.Second*2, func() {
fmt.Println("2秒后执行的函数")
})
time.Sleep(time.Second * 3)
}

总结
Channel作为Go语言并发编程的核心组件,完全贯彻了"通过通信来共享内存"的设计思想,让goroutine之间的数据传递和同步变得更加简洁安全。
好了,感谢大家的支持,这期的Channel内容就先到这里了,如果有讲解不到位的地方,欢迎大家在评论区指正!