Go 从 0 到精通 · 第 26 课:channel 入门

学习定位:这是整套 Go 教程的第 26 课,也是阶段五(并发与工程阶段)的第二课。
前置要求:已经完成第 25 课,理解 goroutine 的创建和 WaitGroup 的使用。
本课目标:理解 channel 的概念和工作机制,掌握无缓冲 channel 和有缓冲 channel 的区别,能用 channel 在 goroutine 之间安全地传递数据,理解 channel 的阻塞行为和关闭机制。


1. 本课你要解决的核心问题

上一课你学会了用 goroutine 并行执行任务。但有一个问题没解决:goroutine 之间怎么通信?

上一课我们用"预分配切片 + 按索引写入"来收集结果,这在简单场景下可以。但如果需求变复杂一点——

  • 一个 goroutine 产生数据,另一个消费数据
  • 多个 goroutine 协作完成一个流水线任务
  • 一个 goroutine 通知另一个"可以开始了"

这些都需要 goroutine 之间能安全地传递数据。Go 给出的答案就是 channel

Go 有一句著名的并发哲学:

“不要通过共享内存来通信,而要通过通信来共享内存。”

channel 就是这句话的具体实现。

你需要搞明白以下问题:

  • channel 是什么
  • 怎么创建 channel,怎么发送和接收数据
  • 无缓冲 channel 和有缓冲 channel 有什么区别
  • channel 什么时候会阻塞
  • 怎么关闭 channel,关闭后会怎样
  • 怎么用 range 遍历 channel
  • 什么是单向 channel

学完这一课,你就掌握了 Go 并发编程最核心的通信工具。


2. channel 是什么

2.1 一句话定义

channel 是 goroutine 之间传递数据的管道。

你可以把 channel 想象成一根管子:一头塞东西进去(发送),另一头把东西拿出来(接收)。

1
goroutine A  ──发送──▶  [ channel ]  ──接收──▶  goroutine B

2.2 channel 的类型

channel 是有类型的。一个 chan int 只能传 int,一个 chan string 只能传 string

1
2
3
4
var ch1 chan int      // 传 int 的 channel
var ch2 chan string // 传 string 的 channel
var ch3 chan bool // 传 bool 的 channel
var ch4 chan []byte // 传 []byte 的 channel

2.3 channel 的零值

channel 的零值是 nil。对 nil channel 发送或接收会永久阻塞

1
2
3
var ch chan int  // nil
ch <- 1 // 永久阻塞
<-ch // 永久阻塞

所以 channel 必须用 make 创建后才能使用。


3. 创建 channel

3.1 无缓冲 channel

1
ch := make(chan int)  // 创建一个无缓冲的 int channel

无缓冲 channel 没有任何存储空间。发送方发送数据后必须等接收方来取,接收方想取数据也必须等发送方来送。两方必须同时到位,数据才能传递

就像面对面交接物品:你必须等对方伸手才能把东西给出去。

3.2 有缓冲 channel

1
ch := make(chan int, 5)  // 创建一个容量为 5 的有缓冲 channel

有缓冲 channel 有一个内部队列,可以暂存数据。发送方可以往里塞数据,只要队列没满就不会阻塞。接收方从队列中取数据,只要队列不空就不会阻塞。

就像一个快递柜:寄件人放进去就走,取件人有空再来取。柜子放满了,寄件人才需要等。


4. 发送和接收

4.1 操作符 <-

channel 用 <- 操作符发送和接收数据:

1
2
3
ch <- value   // 发送:把 value 发送到 ch
v := <-ch // 接收:从 ch 接收一个值,赋给 v
<-ch // 接收:从 ch 接收一个值,丢弃

箭头方向就是数据流动方向ch <- value 是数据流入 channel,<-ch 是数据从 channel 流出。

4.2 第一个 channel 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
ch := make(chan string) // 创建无缓冲 channel

// 在新的 goroutine 中发送数据
go func() {
ch <- "你好,channel!" // 发送
}()

msg := <-ch // 接收(会阻塞,直到有数据可收)
fmt.Println(msg) // 你好,channel!
}

执行流程:

  1. 创建 channel
  2. 启动一个 goroutine,它尝试往 channel 发送数据
  3. 主 goroutine 执行 <-ch,阻塞等待数据
  4. 新 goroutine 发送 "你好,channel!"
  5. 主 goroutine 收到数据,解除阻塞,打印结果

注意:这里不需要 WaitGroup<-ch 本身就是一种等待机制——主 goroutine 会一直阻塞,直到收到数据。channel 天然地起到了同步的作用。

4.3 用 channel 替代 WaitGroup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func worker(id int, done chan bool) {
fmt.Printf("Worker %d 开始\n", id)
// 做一些工作...
fmt.Printf("Worker %d 完成\n", id)
done <- true // 通知主 goroutine:我完成了
}

func main() {
done := make(chan bool)

go worker(1, done)

<-done // 等待 worker 完成
fmt.Println("主 goroutine 结束")
}

done <- true 就是 worker 在说"我干完了",<-done 就是主 goroutine 在等这句话。


5. 无缓冲 channel 的阻塞行为

5.1 发送阻塞,直到有人接收

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
ch := make(chan int) // 无缓冲

ch <- 42 // 阻塞!没有其他 goroutine 来接收
// 永远执行不到这里

fmt.Println(<-ch)
}

运行这段代码,你会得到一个**死锁(deadlock)**错误:

1
fatal error: all goroutines are asleep - deadlock!

因为发送操作 ch <- 42 需要另一个 goroutine 来接收,但只有一个主 goroutine,没人来接收,所以永远阻塞。Go 运行时检测到所有 goroutine 都在等待,就报死锁。

5.2 接收阻塞,直到有人发送

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
ch := make(chan int)

v := <-ch // 阻塞!没有其他 goroutine 来发送
fmt.Println(v)
}

同样是死锁。接收操作需要有人发送数据。

5.3 无缓冲 channel 是同步的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"time"
)

func main() {
ch := make(chan string)

go func() {
fmt.Println("goroutine: 准备发送...")
time.Sleep(2 * time.Second) // 模拟耗时操作
ch <- "数据来了"
fmt.Println("goroutine: 发送完成")
}()

fmt.Println("main: 等待接收...")
msg := <-ch // 阻塞 2 秒,直到 goroutine 发送
fmt.Println("main: 收到:", msg)
}

输出:

1
2
3
4
5
main: 等待接收...
goroutine: 准备发送...
(等待 2 秒)
goroutine: 发送完成
main: 收到: 数据来了

无缓冲 channel 的核心特性:发送和接收必须同时就绪。这让它成为 goroutine 之间最简单的同步工具。


6. 有缓冲 channel

6.1 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
ch := make(chan int, 3) // 容量为 3

// 可以连续发送 3 个值,不会阻塞(因为缓冲区没满)
ch <- 1
ch <- 2
ch <- 3

// ch <- 4 // 这里会阻塞!缓冲区已满

fmt.Println(<-ch) // 1(先进先出)
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 3
}

有缓冲 channel 像一个队列:先进先出(FIFO)

6.2 阻塞规则

1
2
发送:缓冲区满了才阻塞
接收:缓冲区空了才阻塞
1
2
3
4
5
6
7
8
ch := make(chan int, 2)

ch <- 1 // 不阻塞(缓冲区: [1],容量 2)
ch <- 2 // 不阻塞(缓冲区: [1, 2],容量 2)
// ch <- 3 // 阻塞!缓冲区满了

<-ch // 不阻塞,取出 1(缓冲区: [2])
ch <- 3 // 不阻塞了(缓冲区: [2, 3])

6.3 lencap

1
2
3
4
5
6
7
8
ch := make(chan int, 5)

ch <- 10
ch <- 20
ch <- 30

fmt.Println(len(ch)) // 3(当前缓冲区中有 3 个元素)
fmt.Println(cap(ch)) // 5(缓冲区总容量)

6.4 无缓冲 vs 有缓冲

特性 无缓冲 make(chan T) 有缓冲 make(chan T, n)
容量 0 n
发送阻塞 直到有人接收 直到缓冲区满
接收阻塞 直到有人发送 直到缓冲区空
同步性 强同步(握手) 弱同步(排队)
类比 面对面交接 快递柜

怎么选

  • 需要严格同步(确保一方完成后另一方才继续)→ 无缓冲
  • 允许发送方先跑一段,不必等接收方 → 有缓冲
  • 不确定用哪个 → 先用无缓冲,需要优化时再加缓冲

7. 关闭 channel

7.1 为什么要关闭

当发送方不再发送数据时,需要告诉接收方"没有更多数据了"。这就是关闭 channel 的作用。

7.2 close 函数

1
2
3
4
5
6
ch := make(chan int, 3)

ch <- 1
ch <- 2
ch <- 3
close(ch) // 关闭 channel

7.3 关闭后的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
ch := make(chan int, 3)

ch <- 10
ch <- 20
close(ch)

// 关闭后仍然可以接收缓冲区中的数据
fmt.Println(<-ch) // 10
fmt.Println(<-ch) // 20

// 缓冲区空了,继续接收会得到零值
fmt.Println(<-ch) // 0(int 的零值)
fmt.Println(<-ch) // 0(不会阻塞,也不会 panic)
}

关闭后的规则

操作 结果
接收(缓冲区还有数据) 正常返回数据
接收(缓冲区空了) 返回零值,不阻塞
发送 panic!
再次关闭 panic!

7.4 判断 channel 是否已关闭

接收操作可以返回两个值:

1
v, ok := <-ch
  • ok == true:收到了正常的数据
  • ok == false:channel 已关闭且缓冲区为空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)

v, ok := <-ch
fmt.Println(v, ok) // 1 true

v, ok = <-ch
fmt.Println(v, ok) // 2 true

v, ok = <-ch
fmt.Println(v, ok) // 0 false(channel 已关闭且为空)
}

7.5 谁来关闭 channel

原则:只有发送方关闭 channel,接收方不要关闭。

因为发送方知道自己什么时候不再发送了。如果接收方关闭了 channel,发送方继续发送就会 panic。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 正确:发送方关闭
func producer(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // 发送完了,关闭
}

// 错误:接收方关闭
func consumer(ch chan int) {
for v := range ch {
fmt.Println(v)
}
// close(ch) // 不要在这里关闭!
}

8. range 遍历 channel

8.1 基本用法

for range 可以遍历 channel,它会一直接收数据,直到 channel 被关闭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
ch := make(chan int)

go func() {
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch) // 发送完毕,关闭 channel
}()

// range 会一直接收,直到 ch 被关闭
for v := range ch {
fmt.Println(v)
}
fmt.Println("接收完毕")
}

输出:

1
2
3
4
5
6
1
2
3
4
5
接收完毕

8.2 如果不关闭 channel

1
2
3
4
5
6
7
8
9
10
11
12
go func() {
for i := 1; i <= 5; i++ {
ch <- i
}
// 忘了 close(ch)
}()

for v := range ch {
fmt.Println(v)
}
// 打印完 1~5 后,range 会一直阻塞等待更多数据
// 最终报 deadlock 错误

for range 遍历 channel 时,必须有人关闭 channel,否则 range 永远不会结束。

8.3 多个发送者的情况

如果有多个 goroutine 往同一个 channel 发送数据,什么时候关闭?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"fmt"
"sync"
)

func main() {
ch := make(chan int)
var wg sync.WaitGroup

// 启动 3 个发送者
for i := 1; i <= 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 1; j <= 3; j++ {
ch <- id*10 + j
}
}(i)
}

// 用一个单独的 goroutine 等待所有发送者完成后关闭 channel
go func() {
wg.Wait()
close(ch)
}()

// 主 goroutine 接收
for v := range ch {
fmt.Println(v)
}
}

技巧:用 WaitGroup 等待所有发送者完成,然后在一个单独的 goroutine 中关闭 channel。这样 range 就能正常结束了。


9. 单向 channel

9.1 为什么需要单向 channel

有时候你希望一个函数只能往 channel 发送数据,另一个函数只能从 channel 接收数据。这样可以防止误操作。

9.2 语法

1
2
chan<- int  // 只能发送的 channel(send-only)
<-chan int // 只能接收的 channel(receive-only)

记忆方法:箭头指向 chan 就是发送,箭头从 chan 出来就是接收。

9.3 用在函数参数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

// producer 只能往 ch 发送数据
func producer(ch chan<- int) {
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch)
}

// consumer 只能从 ch 接收数据
func consumer(ch <-chan int) {
for v := range ch {
fmt.Printf("收到: %d\n", v)
}
}

func main() {
ch := make(chan int) // 双向 channel

go producer(ch) // 自动转换为 chan<- int
consumer(ch) // 自动转换为 <-chan int
}

双向 channel 可以隐式转换为单向 channel,但单向不能转回双向。这是一种类型安全机制:

1
2
3
4
5
ch := make(chan int)

var sendOnly chan<- int = ch // 可以:双向 → 只发送
var recvOnly <-chan int = ch // 可以:双向 → 只接收
// var ch2 chan int = sendOnly // 错误:只发送 → 双向,不允许

9.4 单向 channel 的好处

1
2
3
4
5
// 如果 producer 不小心写了 <-ch,编译器直接报错
func producer(ch chan<- int) {
// v := <-ch // 编译错误:cannot receive from send-only channel
ch <- 42
}

编译器帮你检查,防止在不该接收的地方接收、不该发送的地方发送。


10. 实战示例

10.1 生产者-消费者模式

这是并发编程中最经典的模式:一方生产数据,另一方消费数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"time"
)

func producer(ch chan<- int) {
for i := 1; i <= 10; i++ {
fmt.Printf("生产: %d\n", i)
ch <- i
time.Sleep(100 * time.Millisecond) // 模拟生产耗时
}
close(ch)
}

func consumer(ch <-chan int) {
for v := range ch {
fmt.Printf(" 消费: %d\n", v)
time.Sleep(200 * time.Millisecond) // 消费比生产慢
}
fmt.Println("消费者: 没有更多数据了")
}

func main() {
ch := make(chan int, 3) // 缓冲区为 3,允许生产者先跑一段

go producer(ch)
consumer(ch) // 在主 goroutine 中消费
}

可能的输出:

1
2
3
4
5
6
7
8
9
10
11
生产: 1
消费: 1
生产: 2
生产: 3
生产: 4
消费: 2
生产: 5
消费: 3
生产: 6
消费: 4
...

观察:生产者比消费者快,但缓冲区只有 3,所以生产者会被限速。这就是 channel 的背压(backpressure)机制——消费者处理不过来时,生产者会自动慢下来。

10.2 用 channel 收集 goroutine 的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"fmt"
"math/rand"
"time"
)

type Result struct {
WorkerID int
Value int
}

func worker(id int, results chan<- Result) {
// 模拟耗时计算
time.Sleep(time.Duration(100+rand.Intn(400)) * time.Millisecond)
results <- Result{WorkerID: id, Value: rand.Intn(100)}
}

func main() {
numWorkers := 5
results := make(chan Result, numWorkers)

// 启动多个 worker
for i := 1; i <= numWorkers; i++ {
go worker(i, results)
}

// 收集所有结果
for i := 0; i < numWorkers; i++ {
r := <-results
fmt.Printf("Worker %d 返回: %d\n", r.WorkerID, r.Value)
}

fmt.Println("所有结果已收集")
}

这比上一课用"预分配切片 + 按索引写入"更优雅:

  • 结果按完成顺序返回,先完成的先输出
  • 不需要预先知道有多少结果
  • channel 保证了并发安全

10.3 流水线(Pipeline)

channel 可以把多个处理步骤串起来,形成数据处理流水线:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import "fmt"

// 第一步:生成数字
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}

// 第二步:平方
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}

// 第三步:过滤(只保留大于 10 的数)
func filter(in <-chan int, threshold int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
if n > threshold {
out <- n
}
}
close(out)
}()
return out
}

func main() {
// 构建流水线:生成 → 平方 → 过滤
nums := generate(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
squared := square(nums)
result := filter(squared, 10)

// 消费最终结果
for v := range result {
fmt.Println(v)
}
}

输出:

1
2
3
4
5
6
7
16
25
36
49
64
81
100

流水线的每一步都在独立的 goroutine 中运行,通过 channel 连接。数据像水一样从一个阶段流向下一个阶段。

函数返回 <-chan int(只读 channel) 是 Go 中流水线的标准写法:每个阶段负责创建 channel、启动 goroutine、返回只读 channel。调用方只管从返回的 channel 中读取。

10.4 用 channel 做信号通知

有时候 channel 不传具体数据,只用来传递"某件事发生了"的信号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"time"
)

func main() {
done := make(chan struct{}) // 空结构体不占内存

go func() {
fmt.Println("工作中...")
time.Sleep(2 * time.Second)
fmt.Println("工作完成!")
close(done) // 通过关闭 channel 发送信号
}()

fmt.Println("等待完成...")
<-done // 阻塞,直到 done 被关闭
fmt.Println("继续后续操作")
}

chan struct{} 是信号通知的惯用类型。struct{} 不占任何内存(大小为 0),表示"我不关心传什么值,只关心有没有信号"。

close(done) 而不是 done <- struct{}{},好处是:关闭 channel 后,所有<-done 上等待的 goroutine 都会被唤醒。这是一种广播机制。


11. 常见坑总结

11.1 往已关闭的 channel 发送数据——panic

1
2
3
ch := make(chan int)
close(ch)
ch <- 1 // panic: send on closed channel

这是最常见的 channel 错误。记住:关闭 channel 是发送方的事,接收方永远不要关闭。

11.2 关闭已关闭的 channel——panic

1
2
3
ch := make(chan int)
close(ch)
close(ch) // panic: close of closed channel

不要重复关闭 channel。

11.3 死锁——所有 goroutine 都在等待

1
2
3
4
5
6
func main() {
ch := make(chan int)
ch <- 1 // 阻塞,没有其他 goroutine 来接收
fmt.Println(<-ch)
}
// fatal error: all goroutines are asleep - deadlock!

无缓冲 channel 在同一个 goroutine 中既发送又接收,必定死锁。

1
2
3
4
5
6
// 有缓冲的 channel 可以在同一个 goroutine 中操作(只要不超过缓冲区)
func main() {
ch := make(chan int, 1) // 容量为 1
ch <- 1 // 不阻塞(缓冲区未满)
fmt.Println(<-ch) // 1
}

11.4 忘记关闭 channel 导致 range 卡死

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
ch := make(chan int)

go func() {
ch <- 1
ch <- 2
// 忘了 close(ch)
}()

for v := range ch {
fmt.Println(v) // 输出 1、2 后永远阻塞
}
}

for range 会一直等到 channel 关闭才停止。不关闭就会变成死锁。

11.5 只发不收——goroutine 泄漏

1
2
3
4
5
6
7
8
9
10
func main() {
ch := make(chan int)

go func() {
ch <- 42 // 发送后一直等待接收方,但没人来收
}()

// main 直接返回了,没有 <-ch
// goroutine 永远阻塞在 ch <- 42,泄漏了
}

每个往 channel 发送的数据都要有人接收。如果没人收,发送方的 goroutine 会永远阻塞。

11.6 nil channel 的用途

1
2
3
4
5
var ch chan int  // nil

// ch <- 1 // 永久阻塞
// <-ch // 永久阻塞
// close(ch) // panic!

nil channel 看起来没用,但在 select(下一课)中有特殊用途——可以动态禁用某个 case。

11.7 缓冲区大小不是越大越好

1
ch := make(chan int, 1000000)  // 不要随便设一百万

缓冲区太大会掩盖问题——如果消费者比生产者慢,大缓冲区只是延迟了阻塞,内存会越积越多。合理的缓冲区大小通常是个位数到几十。


12. 本课练习

练习 1:乒乓球

要求:

  • 两个 goroutine 模拟乒乓球对打
  • 用一个 channel 传递"球"(一个 int,代表击球次数)
  • 每次接到球后次数加 1,打印 "Player X 击球 N 次" 后把球传回去
  • 打到 10 次后结束

练习 2:扇出(Fan-out)

要求:

  • 一个生产者 goroutine 生成 1~20 的数字,发送到 channel
  • 启动 3 个消费者 goroutine 从同一个 channel 接收并打印
  • 观察哪个消费者接收了哪些数字

提示:多个 goroutine 从同一个 channel 接收,每条数据只会被一个 goroutine 拿到。


练习 3:求和流水线

要求:

  • 第一阶段:生成 1~100 的数字
  • 第二阶段:过滤出偶数
  • 第三阶段:累加所有偶数
  • 每个阶段一个 goroutine,用 channel 连接
  • 最后输出结果(应该是 2550)

练习 4:超时处理(预习)

要求:

  • 启动一个 goroutine,模拟一个可能很慢的操作(随机 1~5 秒)
  • 主 goroutine 最多等 2 秒
  • 如果 2 秒内拿到结果就打印,否则打印"超时"

提示:用 time.After(2 * time.Second)select(这是下一课的内容,先尝试自己查资料实现)。


练习 5:安全计数器

要求:

  • 启动 100 个 goroutine,每个 goroutine 往 channel 发送 1
  • 主 goroutine 从 channel 接收 100 个值并累加
  • 输出最终结果(应该是 100)

13. 自测题

13.1 概念题

  1. channel 的零值是什么?对 nil channel 发送会怎样?
  2. 无缓冲 channel 和有缓冲 channel 的阻塞规则分别是什么?
  3. 关闭 channel 后,发送和接收分别会怎样?
  4. v, ok := <-chokfalse 代表什么?
  5. 为什么只有发送方应该关闭 channel?
  6. for range ch 什么时候结束?
  7. chan struct{} 是什么意思?什么时候用?
  8. chan<- int<-chan int 分别是什么?

13.2 代码阅读题

预测以下代码的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
ch := make(chan int, 2)

ch <- 1
ch <- 2

fmt.Println(<-ch)

ch <- 3

fmt.Println(<-ch)
fmt.Println(<-ch)
}
点击查看答案
1
2
3
1
2
3

解释:

  1. ch <- 1:缓冲区 [1]
  2. ch <- 2:缓冲区 [1, 2]
  3. <-ch:取出 1,缓冲区 [2],打印 1
  4. ch <- 3:缓冲区 [2, 3](不会阻塞,因为刚取了一个)
  5. <-ch:取出 2,打印 2
  6. <-ch:取出 3,打印 3

channel 是 FIFO(先进先出),所以输出顺序就是发送顺序。


14. 本课总结

这一课你学到了 Go 并发编程最核心的通信机制——channel。

场景 做法
创建 channel make(chan T)make(chan T, n)
发送 / 接收 ch <- v / v := <-ch
关闭 channel close(ch)(发送方关闭)
遍历 channel for v := range ch(需要关闭才能结束)
判断是否关闭 v, ok := <-chok == false 表示已关闭
信号通知 chan struct{},用 close 广播
限制方向 chan<- T(只发送)、<-chan T(只接收)

最重要的三件事:

  1. channel 是 goroutine 之间传递数据的管道——发送和接收是并发安全的
  2. 无缓冲 channel 是同步的,有缓冲 channel 有队列——按需选择
  3. 只有发送方关闭 channel——往已关闭的 channel 发送会 panic

15. 下一课预告

这一课你学会了用 channel 在两个 goroutine 之间传递数据。但如果一个 goroutine 需要同时监听多个 channel呢?

  • 同时等待用户输入和超时信号
  • 同时等待多个服务的响应
  • 处理"哪个先来就先处理哪个"的场景

下一课:select 与并发控制

会重点讲:

  • select 是什么——channel 的多路复用器
  • 怎么用 select 同时监听多个 channel
  • 超时处理——time.After 配合 select
  • default 分支——非阻塞操作
  • 常见的 select 使用模式

学完下一课,你就能处理更复杂的并发场景了。