vault backup: 2026-06-07 12:14:39
This commit is contained in:
+191
-236
@@ -1,287 +1,242 @@
|
||||
---
|
||||
tags:
|
||||
- Go
|
||||
- golang
|
||||
- go进阶语法
|
||||
- Channel
|
||||
tags: [go, golang, Channel, 并发, CSP]
|
||||
create time: 2026-06-07 14:40
|
||||
---
|
||||
|
||||
# Channel
|
||||
我们知道可以通过`go`关键字来开启一个`goroutine`,我们的样例代码逻辑很简单,都是在各个`goroutine`各自处理自己的逻辑,但有时候我们需要不同的`goroutine`之间能够通信,这里就要用到`channel`。
|
||||
|
||||
## channel是什么
|
||||
官方定义:
|
||||
Channels are a typed conduit through which you can send and receive values with the channel operator
|
||||
Channel是一个可以收发数据的管道
|
||||
## 概述
|
||||
|
||||
Channel 是 Go 中 goroutine 之间通信的核心机制。Go 倡导"以通信共享内存,而非共享内存来通信",channel 正是这一哲学(CSP 模型)的具象化体现。本文涵盖 channel 的创建、操作、单向/双向类型、缓冲语义以及实战技巧。
|
||||
|
||||
## 正文
|
||||
|
||||
### 什么是 Channel?
|
||||
|
||||
> [!question] 💭 思考
|
||||
> 如果多个 goroutine 需要交换数据,除了用锁保护共享变量,还有没有更安全的方式?
|
||||
|
||||
官方定义:Channel 是一个类型化的管道,通过它你可以发送和接收值。
|
||||
|
||||
## channel初始化
|
||||
channel的声明方式如下:
|
||||
```go
|
||||
var channel_name chan channel_type
|
||||
var channel_name [size]chan channel_type // 声明一个channel,其容量大小为size
|
||||
```
|
||||
声明之后的管道,并没有进行初始化为其分配空间,其值是`nil`,我们要使用还要配合`make`函数来对其初始化,之后才可以在程序中使用该管道。
|
||||
```go
|
||||
channel_name := make(chan channel_type)
|
||||
channel_name := make(chan channel_type, size)
|
||||
```
|
||||
或者我们可以直接一步完成声明和初始化,如下:
|
||||
```go
|
||||
channel_name := make(chan channel_type)
|
||||
channel_name := make(chan channel_type, size) //创建带有缓存的管道,size为缓存大小
|
||||
ch := make(chan int) // 创建一个传递 int 的 channel
|
||||
ch <- 42 // 发送:向 ch 写入 42
|
||||
v := <-ch // 接收:从 ch 读取值到 v
|
||||
```
|
||||
|
||||
## channel操作
|
||||
channel的操作主要有以下几种
|
||||
> [!info] ℹ️ 核心理念
|
||||
> **"Don't communicate by sharing memory; share memory by communicating."** — Rob Pike
|
||||
> 与其用锁保护共享数据,不如让数据在 goroutine 之间流动——拥有数据的 goroutine 就是唯一能修改它的那个。
|
||||
|
||||
### 创建与初始化
|
||||
|
||||
```go
|
||||
ch := make(chan int) // 创建一个管道ch
|
||||
ch <- v // 向管道ch中发送数据v.
|
||||
v := <-ch // 从管道中读取数据存储到变量v
|
||||
close(ch) // 关闭管道ch
|
||||
// 无缓冲 channel(同步模式)
|
||||
ch := make(chan int)
|
||||
|
||||
// 有缓冲 channel(异步模式,容量为 3)
|
||||
ch := make(chan int, 3)
|
||||
```
|
||||
在这里需要注意`close(ch)`这个操作,管道用完了,需要对其进行关闭,避免程序一直在等待以及资源的浪费。但是关闭的管道,仍然可以从中接收数据,只是接收到的的数据永远是零值。
|
||||
看下面例子:
|
||||
|
||||
> [!warning] ⚠️ 常见错误
|
||||
> - **未初始化的 channel 是 `nil`**,对 nil channel 发送/接收会永久阻塞
|
||||
> - **重复关闭 channel 会 panic**
|
||||
> - **向已关闭的 channel 发送会 panic**
|
||||
|
||||
### 基本操作
|
||||
|
||||
| 操作 | 语法 | 说明 |
|
||||
|------|------|------|
|
||||
| 发送 | `ch <- value` | 向 channel 写入,可能阻塞 |
|
||||
| 接收 | `<-ch` | 从 channel 读取,可能阻塞 |
|
||||
| 带值接收 | `v := <-ch` | 读取值并赋值 |
|
||||
| 关闭 | `close(ch)` | 标记 channel 不再发送数据 |
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
ch := make(chan int, 5)
|
||||
ch <- 1
|
||||
close(ch)
|
||||
go func() {
|
||||
for i := 0; i < 5; i++ {
|
||||
v := <-ch
|
||||
fmt.Printf("v=%d\n", v)
|
||||
}
|
||||
}()
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
v=1
|
||||
v=0
|
||||
v=0
|
||||
v=0
|
||||
v=0
|
||||
```
|
||||
创建一个缓存为`5`的`int`类型的管道,向管道里写入一个`1`之后,将管道关闭,然后开启一个`gortoutine`从管道读取数据,读取`5`次,可以看到即便管道关闭之后,他仍然可以读取数据,在读完数据之后,将一直读取零值。
|
||||
但是,上述读取方式还有一个问题?比如我们创建一个`int`类型的`channel`,我们需要往里面写入零值,用另一个`goroutine`读取,此时我们就无法区两种常用的读取方式
|
||||
|
||||
### 判定读取
|
||||
还是以上面的例子来看,稍作修改
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ch := make(chan int, 5)
|
||||
ch <- 1
|
||||
close(ch)
|
||||
ch := make(chan int)
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 5; i++ {
|
||||
v, ok := <-ch // 判断句式读取
|
||||
if ok {
|
||||
fmt.Printf("v=%d\n", v)
|
||||
} else {
|
||||
fmt.Printf("channel数据已读完,v=%d\n", v)
|
||||
}
|
||||
}
|
||||
ch <- 42 // 生产者:发送数据
|
||||
close(ch) // 生产完毕,关闭 channel
|
||||
}()
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
v := <-ch // 消费者:接收数据
|
||||
fmt.Println(v) // 42
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
v=1
|
||||
channel数据已读完,v=0
|
||||
channel数据已读完,v=0
|
||||
channel数据已读完,v=0
|
||||
channel数据已读完,v=0
|
||||
```
|
||||
在读取`channel`数据的时候,用`ok`做了判断,当管道内还有数据能读取的时候,`ok`为`true`,当管道关闭后,`ok`为`false`。
|
||||
|
||||
### for range读取
|
||||
在上面例子中,我们明确了读取的次数是5次,但是我们往往在更多的时候,是不明确读取次数的,只是在`channel`的一端读取数据,有数据我们就读,直到另一端关闭了这个`channel`,这样就可以用`for range`这种优雅的方式来读取`channel`中的数据了
|
||||
### 关闭 Channel 后的行为
|
||||
|
||||
> [!question] 💭 思考
|
||||
> 关闭一个 channel 后还能读吗?还能写吗?读完已有数据后再读会怎样?
|
||||
|
||||
| 操作 | 未关闭 | 已关闭(有数据) | 已关闭(空) |
|
||||
|------|--------|-----------------|-------------|
|
||||
| 发送 | ✅ 正常 | ❌ **panic** | ❌ **panic** |
|
||||
| 接收 | ✅ 正常 | ✅ 返回已有数据 | ✅ 返回零值 |
|
||||
| 关闭 | ✅ 正常 | ✅ 可重复关闭检查 | ❌ panic(重复关闭) |
|
||||
|
||||
```go
|
||||
package main
|
||||
ch := make(chan int, 5)
|
||||
ch <- 1
|
||||
close(ch)
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ch := make(chan int, 5)
|
||||
ch <- 1
|
||||
ch <- 2
|
||||
close(ch)
|
||||
go func() {
|
||||
for v := range ch {
|
||||
fmt.Printf("v=%d\n", v)
|
||||
}
|
||||
}()
|
||||
time.Sleep(2 * time.Second)
|
||||
// 关闭后仍可读取
|
||||
for i := 0; i < 5; i++ {
|
||||
v := <-ch
|
||||
fmt.Println(v) // 第1次: 1, 之后全是 0
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
v=1
|
||||
v=2
|
||||
```
|
||||
主`goroutine`往`channel`里写了两个数据`1`和`2`,然后关闭,子`goroutine`也只能读取到`1`和`2`。这里在主`goroutine`关闭了`channel`之后,子`goroutine`里的`for range`循环才会结束。
|
||||
|
||||
## 双向channel和单向channel
|
||||
channel根据其功能又可以分为双向`channel`和单向`channel`,双向`channel`即可发送数据又可接收数据,单向`channel`要么只能发送数据,要么只能接收数据。
|
||||
定义单向读`channel`
|
||||
#### 安全读取:ok 模式
|
||||
|
||||
当需要从已关闭的 channel 区分"有数据"和"已关闭"时,使用多返回值形式:
|
||||
|
||||
```go
|
||||
var ch = make(chan int)
|
||||
type RChannel= <-chan int // 定义类型
|
||||
var rec RChannel = ch
|
||||
```
|
||||
定义单向写`channel`
|
||||
```go
|
||||
var ch = make(chan int)
|
||||
type SChannel = chan<- int // 定义类型
|
||||
var send SChannel = ch
|
||||
```
|
||||
注意写`channel`与读`channel`在定义的时候只是`<-`的位置不同,前者在`chan`关键字后面,后者在`chan`关键字前面。
|
||||
代码示例:
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SChannel = chan<- int
|
||||
type RChannel = <-chan int
|
||||
|
||||
func main() {
|
||||
var ch = make(chan int) // 创建channel
|
||||
|
||||
go func() {
|
||||
var send SChannel = ch
|
||||
fmt.Println("send: 100")
|
||||
send <- 100
|
||||
}()
|
||||
|
||||
go func() {
|
||||
var rec RChannel = ch
|
||||
num := <- rec
|
||||
fmt.Printf("receive: %d", num)
|
||||
}()
|
||||
time.Sleep(2*time.Second)
|
||||
v, ok := <-ch
|
||||
if ok {
|
||||
fmt.Println("收到数据:", v)
|
||||
} else {
|
||||
fmt.Println("channel 已关闭,无更多数据")
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
send: 100
|
||||
receive: 100
|
||||
```
|
||||
创建一个`channel ch`,分别定义两个单向`channel`类型`SChannel`和`RChannel` ,根据别名类型给`ch`定义两个别名`send`和`rec`,一个只用于发送,一个只用于读取。
|
||||
|
||||
## 扩展
|
||||
`channel`非常重要,Go语言中有个重要思想:不以共享内存来通信,而以通信来共享内存。
|
||||
说得更直接点,协程之间可以利用`channel`来传递数据,如下的例子,可以看出父子协程如何通信的,父协程通过`channel`拿到了子协程执行的结果。
|
||||
#### for-range 读取
|
||||
|
||||
这是最优雅的 channel 消费方式——自动在 channel 关闭时退出循环:
|
||||
|
||||
```go
|
||||
package main
|
||||
go func() {
|
||||
for v := range ch { // channel 关闭后自动退出
|
||||
fmt.Println("收到:", v)
|
||||
}
|
||||
fmt.Println("channel 已关闭,循环结束")
|
||||
}()
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
close(ch) // 触发 for-range 退出
|
||||
```
|
||||
|
||||
func sum(s []int, c chan int) {
|
||||
sum := 0
|
||||
for _, v := range s {
|
||||
sum += v
|
||||
}
|
||||
c <- sum // send sum to c
|
||||
> [!tip] 💡 最佳实践
|
||||
> **发送端负责关闭,接收端不负责关闭**。如果一个 channel 有多个发送方,谁该关闭它?这种模糊场景应尽量避免。
|
||||
|
||||
### 有缓冲 vs 无缓冲
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Unbuf["无缓冲 channel"]
|
||||
S[发送方] -->|"阻塞直到接收方就绪"| R[接收方]
|
||||
end
|
||||
|
||||
subgraph Buf["有缓冲 channel (cap=3)"]
|
||||
B["缓冲区 📦📦"] -.->|剩余容量| S2[发送方]
|
||||
R2[接收方] -.->|有空位| S2
|
||||
R2 -->|"取出"| B
|
||||
end
|
||||
```
|
||||
|
||||
- **无缓冲 channel**:发送和接收同步发生——发送方阻塞直到接收方准备好,反之亦然。适用于需要严格配对的生产者-消费者场景。
|
||||
- **有缓冲 channel**:发送方在缓冲区未满时无需等待即可返回;接收方在缓冲区非空时无需等待即可拿到数据。适用于解耦生产速率和消费速率的场景。
|
||||
|
||||
> [!warning] ⚠️ 注意
|
||||
> 缓冲区满了以后,有缓冲 channel 也会退化为同步模式——发送方将被阻塞。长期满队列意味着消费者跟不上生产者的节奏,应考虑增加消费者数量或扩大缓冲区。
|
||||
|
||||
### 单向 Channel
|
||||
|
||||
有时我们希望限制 channel 的使用方向,比如在函数签名中明确表达"这个参数只用于发送"或"只用于接收":
|
||||
|
||||
```go
|
||||
// 只发送 channel —— 只能 ch<-value
|
||||
sendCh := make(chan int)
|
||||
var sendOnly chan<- int = sendCh
|
||||
|
||||
// 只接收 channel —— 只能 <-ch
|
||||
var recvOnly <-chan int = sendCh
|
||||
```
|
||||
|
||||
```go
|
||||
func producer(out chan<- int) {
|
||||
for i := 0; i < 5; i++ {
|
||||
out <- i // 只能发,不能收
|
||||
}
|
||||
close(out)
|
||||
}
|
||||
|
||||
func consumer(in <-chan int) {
|
||||
for v := range in { // 只能收,不能发
|
||||
fmt.Println("received:", v)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!note] 📝 关键规则
|
||||
> 只有**接收方**才能关闭 channel。单向 channel 的类型转换是单向的:双向可以转为单向,单向不能转回双向。
|
||||
|
||||
### 经典模式:扇入扇出
|
||||
|
||||
> [!question] 💭 思考
|
||||
> 如果有 10 个任务要并行处理,但结果需要汇总到一个地方,怎么设计?
|
||||
|
||||
```go
|
||||
func worker(id int, jobs <-chan int, results chan<- int) {
|
||||
for j := range jobs {
|
||||
fmt.Printf("worker %d processing job %d\n", id, j)
|
||||
results <- j * 2
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
s := []int{7, 2, 8, -9, 4, 0}
|
||||
jobs := make(chan int, 100)
|
||||
results := make(chan int, 100)
|
||||
|
||||
c := make(chan int)
|
||||
go func() {
|
||||
sum(s[:len(s)/2], c)
|
||||
//time.Sleep(1 * time.Second)
|
||||
}()
|
||||
go sum(s[len(s)/2:], c)
|
||||
x, y := <-c, <-c // receive from c
|
||||
// 启动 3 个 worker(扇出)
|
||||
for w := 1; w <= 3; w++ {
|
||||
go worker(w, jobs, results)
|
||||
}
|
||||
|
||||
fmt.Println(x, y, x+y)
|
||||
// 发送任务
|
||||
for j := 1; j <= 9; j++ {
|
||||
jobs <- j
|
||||
}
|
||||
close(jobs)
|
||||
|
||||
// 等待所有 worker 完成
|
||||
close(results)
|
||||
for r := range results {
|
||||
fmt.Println("result:", r)
|
||||
}
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
-5 17 12
|
||||
```
|
||||
`channel`又分为两类:有缓冲`channel` 和无缓冲`channel`,这个在前面的代码示例中也有简单的描述了。为了协程安全,无论是有无缓冲的`channel`,内部都会有一把锁来控制并发访问。同时`channel`底层一定有一个队列,来存储数据。
|
||||
无缓冲`channel`可以理解为同步模式,即写入一个,如果没有消费者在消费,写入就会阻塞。
|
||||
有缓冲`channel`可以理解为异步模式。即写入消息之后,即使还没被消费,只要队列没满,就可继续写入。如图所示:
|
||||
|
||||

|
||||
### Channel 实现互斥锁
|
||||
|
||||
这里可能会问,如果有缓冲`channel`队列满了,那不就退化到同步了么?是的,如果队列满了,发送还是会阻塞。
|
||||
> [!question] 💭 思考
|
||||
> channel 本身已经是并发安全的了,能不能利用这一点来代替 mutex?
|
||||
|
||||

|
||||
一个容量为 1 的 channel 可以充当信号量:
|
||||
|
||||
但是我们来反向思考下,如果有缓冲`channel`长期都处于满队列情况,那何必用有缓冲。所以预期在正常情况下,有缓冲`channel`都是异步交互的。
|
||||
|
||||
## channel实现锁操作
|
||||
前面分析了当缓冲队列满了以后,继续往`channel`里面写数据,就会阻塞,那么利用这个特性,我们可以实现一个`goroutine`之间的锁。(对并发安全比较模糊的可以把后面`sync`小节看完再来看这里)
|
||||
直接看示例
|
||||
```go
|
||||
package main
|
||||
ch := make(chan struct{}, 1)
|
||||
ch <- struct{}{} // 获取"锁"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
// 临界区:同时只有一个 goroutine 能执行
|
||||
*counter++
|
||||
|
||||
func add(ch chan bool, num *int) {
|
||||
ch <- true
|
||||
*num = *num + 1
|
||||
<-ch
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 创建一个size为1的channel
|
||||
ch := make(chan bool, 1)
|
||||
|
||||
var num int
|
||||
for i := 0; i < 100; i++ {
|
||||
go add(ch, &num)
|
||||
}
|
||||
|
||||
time.Sleep(2)
|
||||
fmt.Println("num 的值:", num)
|
||||
}
|
||||
<-ch // 释放"锁"
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
num 的值: 100
|
||||
```
|
||||
`ch <- true`和`<- ch`就相当于一个锁,将 `*num = *num + 1`这个操作锁住了。因为`ch`管道的容量是1,在每个`add`函数里都会往`channel`放置一个`true`,直到执行完+1操作之后才将`channel`里的`true`取出。由于`channel`的`size`是1,所以当一个`goroutine`在执行`add`函数的时候,其他`goroutine`执行`add`函数,执行到`ch <- true`的时候就会阻塞,`*num = *num + 1`不会成功,直到前一个+1操作完成,`<-ch`,读出了管道的元素,这样就实现了并发安全
|
||||
|
||||
> [!tip] 💡 何时用 Channel vs Mutex?
|
||||
> - 优先用 **channel**:goroutine 间的数据传递、事件通知、生命周期管理
|
||||
> - 优先用 **mutex**:保护共享变量的并发访问
|
||||
> - 两者不互斥,复杂场景中经常配合使用
|
||||
|
||||
## 关联笔记
|
||||
|
||||
## 小结
|
||||
- 关闭一个未初始化的`channel` 会产生`panic`
|
||||
- `channel`只能被关闭一次,对同一个`channel`重复关闭会产生`panic`
|
||||
- 向一个已关闭的 `channel` 发送消息会产生 `panic`
|
||||
- 从一个已关闭的`channel`读取消息不会发生`panic`,会一直读取所有数据,直到零值
|
||||
- `channel`可以读端和写端都可有多个`goroutine`操作,在一端关闭`channel`的时候,该`channel`读端的所有`goroutine` 都会收到`channel`已关闭的消息
|
||||
- `channel`是并发安全的,多个`goroutine`同时读取`channel`中的数据,不会产生并发安全问题
|
||||
|
||||
`channel`在我们的并发编程中发挥着巨大作用,使用起来也很方便,关于`channel`的具体实现原理可以到后面的Go语言原理篇学习。本章只介绍`channel`的具体用法
|
||||
- [[hzh/GolangStar/Go语言进阶/Select]]
|
||||
- [[hzh/GolangStar/Go语言进阶/Sync]]
|
||||
- [[hzh/GolangStar/Go语言进阶/Goroutine]]
|
||||
- [[hzh/GolangStar/Go语言原理/channel原理]]
|
||||
|
||||
Reference in New Issue
Block a user