Go 语言之所以在云原生时代大放异彩,很大程度上归功于其原生支持的高效并发模型。Go 采用了 Goroutine(协程)和 Channel(通道)来实现并发,这种模式被称为 CSP(通信顺序进程)。本章将带你入门 Go 的并发世界。
示例代码
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8// 一个模拟耗时任务的函数
9func worker(id int, jobs <-chan int, results chan<- int) {
10 for j := range jobs {
11 fmt.Printf("Worker %d started job %d\n", id, j)
12 time.Sleep(time.Second) // 模拟耗时 1 秒
13 fmt.Printf("Worker %d finished job %d\n", id, j)
14 results <- j * 2 // 将结果发送回 results 通道
15 }
16}
17
18func main() {
19 // 创建两个通道:任务通道和结果通道
20 // 设置缓冲区大小为 10,防止阻塞
21 jobs := make(chan int, 10)
22 results := make(chan int, 10)
23
24 // 启动 3 个 Goroutine (Worker)
25 // 它们会并发地从 jobs 通道抢任务做
26 for w := 1; w <= 3; w++ {
27 go worker(w, jobs, results)
28 }
29
30 // 发送 5 个任务
31 for j := 1; j <= 5; j++ {
32 jobs <- j
33 }
34 close(jobs) // 关闭任务通道,告知 Worker 没有新任务了
35
36 // 接收并打印结果
37 for a := 1; a <= 5; a++ {
38 <-results
39 }
40
41 fmt.Println("All jobs finished!")
42}
关键点解释
Goroutine (协程)
- 使用
go关键字 即可启动一个新的协程。例如go funcName()。 - 协程比线程更轻量,启动成本极低,Go 运行时可以在少数几个 OS 线程上调度成千上万个 Goroutine。
main函数本身也是一个 Goroutine。当main结束时,所有其他 Goroutine 都会被强制终止。
Channel (通道)
- 通道是 Goroutine 之间通信的管道。遵循“不要通过共享内存来通信,而要通过通信来共享内存”的原则。
- 定义:
make(chan Type, capacity)。 - 发送:
ch <- value。 - 接收:
value := <-ch。 - 关闭:
close(ch)。关闭后不能再发送,但可以继续接收已有的数据。 - Range:
for x := range ch可以不断从通道读取数据,直到通道被关闭。
Select 选择器
select 语句是 Go 并发编程中的重要工具,用于同时等待多个通道操作。它的语法类似于 switch,但专门用于通道。
基本语法
1select {
2case msg1 := <-ch1:
3 fmt.Println("Received from ch1:", msg1)
4case msg2 := <-ch2:
5 fmt.Println("Received from ch2:", msg2)
6case ch3 <- value:
7 fmt.Println("Sent to ch3")
8default:
9 fmt.Println("No channel ready")
10}
工作原理
- 随机选择:如果多个 case 同时就绪,
select会随机选择一个执行。 - 阻塞等待:如果没有 case 就绪且没有
default,select会阻塞直到某个 case 可以执行。 - 非阻塞:如果有
default分支,当所有 case 都未就绪时,会立即执行default。
常见使用场景
===== 1. 超时控制
1select {
2case result := <-ch:
3 fmt.Println("Got result:", result)
4case <-time.After(3 * time.Second):
5 fmt.Println("Timeout! Operation took too long")
6}
===== 2. 非阻塞接收
1select {
2case msg := <-ch:
3 fmt.Println("Received:", msg)
4default:
5 fmt.Println("No message available")
6}
===== 3. 同时监听多个通道
1for {
2 select {
3 case msg := <-ch1:
4 fmt.Println("From ch1:", msg)
5 case msg := <-ch2:
6 fmt.Println("From ch2:", msg)
7 case <-quit:
8 fmt.Println("Quit signal received")
9 return
10 }
11}
===== 4. 实现通道的优先级
1// 优先处理 highPriority 通道
2select {
3case msg := <-highPriority:
4 handleHighPriority(msg)
5default:
6 select {
7 case msg := <-lowPriority:
8 handleLowPriority(msg)
9 default:
10 // 都没有消息
11 }
12}
注意事项
- 如果所有 case 都是
nil通道,且没有default,select会永久阻塞。 select不会按顺序检查 case,而是随机选择就绪的 case。- 空的
select {}会永久阻塞,可用于防止 main 函数退出。
小结
Go 将并发变得像普通函数调用一样简单。通过组合 Goroutine 和 Channel,我们可以轻松构建出高并发、低耦合的系统。
练习题
- 编写一个程序,启动两个 Goroutine,一个打印数字 1-5,另一个打印字母 A-E。观察输出顺序是否固定。
- 尝试修改示例代码,去掉通道缓冲(即
make(chan int)),看看程序的行为有何变化(可能会导致死锁,思考为什么)。