























并发是 Go 最引以为傲的特性。本章深入讲解 goroutine 和 channel——Go 并发编程的两大基石,以及基于它们的 CSP 并发模型。
首先厘清两个概念:
Rob Pike 有句名言:"并发不是并行(Concurrency is not parallelism)"。并发是一种程序结构的设计方式,使程序能够处理多个独立任务;并行是这种结构在多核硬件上的运行表现。Go 让你用并发的方式编写程序,运行时再将其映射到可用的 CPU 核心上并行执行。
goroutine 是 Go 运行时管理的轻量级线程。在函数调用前加上 go 关键字,即可在新的 goroutine 中并发执行该函数:
func say(s string) {
for i := 0; i < 3; i++ {
fmt.Println(s)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go say("world") // 在新 goroutine 中执行
say("hello") // 在主 goroutine 中执行
}
go 关键字即可。main 函数本身运行在主 goroutine 中。当主 goroutine 结束时,整个程序立即退出,不会等待其他 goroutine 完成:
func main() {
go fmt.Println("这行可能不会被打印")
// main 立即结束,程序退出
}
因此需要某种同步机制确保主 goroutine 等待子 goroutine 完成,这正是 channel 和 sync.WaitGroup 的用武之地。
channel 是 goroutine 之间通信的管道,遵循 CSP 模型"通过通信共享内存"的理念。channel 是类型化的,只能传递特定类型的数据。
ch := make(chan int) // 创建一个传递 int 的无缓冲 channel
ch <- 42 // 发送:将 42 发送到 channel
value := <-ch // 接收:从 channel 接收数据
close(ch) // 关闭 channel
<- 是 channel 操作符,箭头方向表示数据流向。
无缓冲 channel 的发送和接收是同步的:发送操作会阻塞,直到有另一个 goroutine 执行接收;反之亦然。这种特性天然提供了 goroutine 之间的同步:
func main() {
ch := make(chan string)
go func() {
time.Sleep(time.Second)
ch <- "任务完成" // 发送,阻塞直到被接收
}()
msg := <-ch // 接收,阻塞直到有数据,实现了等待
fmt.Println(msg)
}
有缓冲 channel 拥有一个容量,只要缓冲区未满,发送就不会阻塞;只要缓冲区非空,接收就不会阻塞:
ch := make(chan int, 3) // 容量为 3 的缓冲 channel
ch <- 1 // 不阻塞
ch <- 2 // 不阻塞
ch <- 3 // 不阻塞
ch <- 4 // 阻塞!缓冲区已满
有缓冲 channel 常用于限流、解耦生产者和消费者的速度差异。
发送方负责关闭 channel,表示"不会再有数据了"。接收方可以通过第二个返回值判断 channel 是否已关闭:
v, ok := <-ch
// ok 为 false 表示 channel 已关闭且数据已取完
重要规则:
- 只有发送方应该关闭 channel,接收方不应关闭。
- 向已关闭的 channel 发送数据会 panic。
- 从已关闭的 channel 接收数据会立即返回零值。
- 重复关闭 channel 会 panic。
range 会持续从 channel 接收数据,直到 channel 被关闭:
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // 必须关闭,否则 range 会永久阻塞
}
func main() {
ch := make(chan int)
go producer(ch)
for v := range ch { // 自动接收直到 channel 关闭
fmt.Println(v)
}
}
可以限定 channel 只能发送或只能接收,增强类型安全和代码可读性:
func send(ch chan<- int) { // 只能发送
ch <- 1
}
func receive(ch <-chan int) { // 只能接收
fmt.Println(<-ch)
}
select 语句让一个 goroutine 可以同时等待多个 channel 操作,哪个 channel 就绪就执行对应分支:
select {
case msg1 := <-ch1:
fmt.Println("收到 ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("收到 ch2:", msg2)
case ch3 <- "data":
fmt.Println("发送到 ch3")
default:
fmt.Println("没有就绪的 channel") // 非阻塞
}
select 会随机选择一个执行。default 分支使 select 变为非阻塞;没有 default 时,select 会阻塞直到某个 case 就绪。select 配合 time.After 是实现超时的经典模式:
select {
case result := <-ch:
fmt.Println("收到结果:", result)
case <-time.After(2 * time.Second):
fmt.Println("超时!")
}
通过一个专门的 done channel 通知 goroutine 退出:
func worker(done <-chan struct{}) {
for {
select {
case <-done:
fmt.Println("收到退出信号")
return
default:
// 执行工作
}
}
}
固定数量的 worker goroutine 从任务 channel 中取任务处理,控制并发度:
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j * 2 // 处理任务
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// 启动 3 个 worker
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 发送 9 个任务
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
// 收集结果
for a := 1; a <= 9; a++ {
fmt.Println(<-results)
}
}
将一系列处理阶段串联,每个阶段是一个 goroutine,通过 channel 连接,形成数据处理流水线:
func gen(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
}
// 使用: gen -> square
for n := range square(gen(2, 3, 4)) {
fmt.Println(n) // 4 9 16
}
本章深入讲解了 Go 并发编程的核心:goroutine 是极轻量的并发执行单元,channel 是它们之间类型安全的通信管道。我们学习了无缓冲/有缓冲 channel 的语义、channel 的关闭与遍历、select 多路复用与超时控制,以及 Worker Pool、Pipeline 等经典并发模式。掌握"通过通信共享内存"的思想,是写好 Go 并发程序的关键。
下一章我们将学习并发进阶内容:sync 包提供的传统同步原语及更多并发控制工具。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。