Go 语言之所以在云原生时代大放异彩,很大程度上归功于其原生支持的高效并发模型。Go 采用了 Goroutine(协程)和 Channel(通道)来实现并发,这种模式被称为 CSP(通信顺序进程)。本章将带你入门 Go 的并发世界。
示例代码
package main
import (
"fmt"
"time"
)
// 一个模拟耗时任务的函数
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("Worker %d started job %d\n", id, j)
time.Sleep(time.Second) // 模拟耗时 1 秒
fmt.Printf("Worker %d finished job %d\n", id, j)
results <- j * 2 // 将结果发送回 results 通道
}
}
func main() {
// 创建两个通道:任务通道和结果通道
// 设置缓冲区大小为 10,防止阻塞
jobs := make(chan int, 10)
results := make(chan int, 10)
// 启动 3 个 Goroutine (Worker)
// 它们会并发地从 jobs 通道抢任务做
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 发送 5 个任务
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs) // 关闭任务通道,告知 Worker 没有新任务了
// 接收并打印结果
for a := 1; a <= 5; a++ {
<-results
}
fmt.Println("All jobs finished!")
}关键点解释
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,但专门用于通道。
基本语法
select {
case msg1 := <-ch1:
fmt.Println("Received from ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("Received from ch2:", msg2)
case ch3 <- value:
fmt.Println("Sent to ch3")
default:
fmt.Println("No channel ready")
}工作原理
随机选择:如果多个 case 同时就绪,
select会随机选择一个执行。阻塞等待:如果没有 case 就绪且没有
default,select会阻塞直到某个 case 可以执行。非阻塞:如果有
default分支,当所有 case 都未就绪时,会立即执行default。
常见使用场景
1. 超时控制
select {
case result := <-ch:
fmt.Println("Got result:", result)
case <-time.After(3 * time.Second):
fmt.Println("Timeout! Operation took too long")
}2. 非阻塞接收
select {
case msg := <-ch:
fmt.Println("Received:", msg)
default:
fmt.Println("No message available")
}3. 同时监听多个通道
for {
select {
case msg := <-ch1:
fmt.Println("From ch1:", msg)
case msg := <-ch2:
fmt.Println("From ch2:", msg)
case <-quit:
fmt.Println("Quit signal received")
return
}
}4. 实现通道的优先级
// 优先处理 highPriority 通道
select {
case msg := <-highPriority:
handleHighPriority(msg)
default:
select {
case msg := <-lowPriority:
handleLowPriority(msg)
default:
// 都没有消息
}
}注意事项
如果所有 case 都是
nil通道,且没有default,select会永久阻塞。select不会按顺序检查 case,而是随机选择就绪的 case。空的
select {}会永久阻塞,可用于防止 main 函数退出。
小结
Go 将并发变得像普通函数调用一样简单。通过组合 Goroutine 和 Channel,我们可以轻松构建出高并发、低耦合的系统。
练习题
编写一个程序,启动两个 Goroutine,一个打印数字 1-5,另一个打印字母 A-E。观察输出顺序是否固定。
尝试修改示例代码,去掉通道缓冲(即
make(chan int)),看看程序的行为有何变化(可能会导致死锁,思考为什么)。