Go 并发编程:goroutine 与 channel
Go 语言的并发模型基于两个核心概念:goroutine 和 channel。goroutine 是轻量级线程,由 Go 运行时自动管理;channel 是 goroutine 之间通信的管道,用于安全地传递数据。掌握这两者,就能高效编写并发程序。
启动一个 goroutine
- 定义一个普通函数,例如
func task() { ... }。 - 在函数调用前加上
go关键字,即可将其作为 goroutine 异步执行。
package main
import (
"fmt"
"time"
)
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")
}
上述代码中,go say("world") 会立即返回,主 goroutine 继续执行 say("hello")。两个任务并发运行,输出顺序不固定。
⚠️ 注意:如果主函数提前退出,所有 goroutine 会被强制终止。确保主 goroutine 等待其他 goroutine 完成。
使用 channel 通信
channel 是类型安全的队列,用于在 goroutine 之间发送和接收数据。
创建 channel
使用 make 函数创建 channel:
ch := make(chan int) // 创建一个传递 int 类型的 channel
发送与接收
- 向 channel 发送数据:
ch <- value - 从 channel 接收数据:
value := <-ch
func main() {
ch := make(chan string)
go func() {
ch <- "done" // 发送字符串到 channel
}()
msg := <-ch // 阻塞等待接收
fmt.Println(msg)
}
此例中,主 goroutine 在 <-ch 处阻塞,直到匿名 goroutine 发送 "done"。
控制并发执行流程
使用带缓冲的 channel
默认 channel 是无缓冲的(同步),发送和接收必须同时就绪。创建带缓冲的 channel 可以解耦发送与接收:
ch := make(chan int, 2) // 缓冲区大小为 2
ch <- 1
ch <- 2
// 此时再发送会阻塞,因为缓冲区已满
fmt.Println(<-ch) // 接收一个,缓冲区空出一个位置
缓冲 channel 允许发送方在接收方未就绪时继续运行,最多可缓存指定数量的值。
使用 select 多路复用
当需要监听多个 channel 时,使用 select 语句:
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() { ch1 <- "one" }()
go func() { ch2 <- "two" }()
select {
case msg1 := <-ch1:
fmt.Println("Received", msg1)
case msg2 := <-ch2:
fmt.Println("Received", msg2)
}
}
select 会随机选择一个已就绪的 case 执行。若多个 channel 同时就绪,Go 运行时会公平调度。
安全关闭 channel
只有发送方应关闭 channel,表示“不再发送数据”。接收方可通过“逗号 ok”模式检测 channel 是否关闭。
ch := make(chan int)
go func() {
for i := 0; i < 3; i++ {
ch <- i
}
close(ch) // 发送完毕后关闭
}()
for {
if val, ok := <-ch; ok {
fmt.Println(val)
} else {
break // channel 已关闭,退出循环
}
}
或者更简洁地使用 range 遍历 channel,自动在关闭后退出:
for val := range ch {
fmt.Println(val)
}
❌ 错误做法:从多个 goroutine 同时关闭同一个 channel,会导致 panic。
常见模式:工作池(Worker Pool)
利用 goroutine 和 channel 实现并发任务处理。
- 创建任务 channel:用于分发任务。
- 启动多个 worker goroutine:每个 worker 从任务 channel 读取并处理。
- 主 goroutine 发送任务,完成后关闭 channel。
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
results <- job * 2 // 模拟处理
}
}
func main() {
const numJobs = 5
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
// 启动 3 个 worker
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 发送任务
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs) // 所有任务已发送
// 收集结果
for a := 1; a <= numJobs; a++ {
<-results
}
}
该模式实现了任务的并发处理与结果收集,适用于批量作业场景。
避免常见陷阱
| 问题 | 表现 | 解决方法 |
|---|---|---|
| 主 goroutine 提前退出 | 子 goroutine 未执行完就被终止 | 使用 sync.WaitGroup 或 channel 同步 |
| 向已关闭 channel 发送 | panic: send on closed channel | 确保只有发送方关闭,且关闭前不再发送 |
| 从 nil channel 接收/发送 | 永久阻塞 | 初始化 channel 或检查是否为 nil |
| 忘记关闭 channel | range 循环无法退出 | 明确责任方,在数据发送完毕后关闭 |
例如,使用 sync.WaitGroup 等待所有 goroutine 完成:
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// 执行任务
}()
wg.Wait() // 阻塞直到所有 Done 被调用
性能与调试建议
-
不要盲目创建大量 goroutine:虽然 goroutine 开销小(初始栈约 2KB),但过多仍会消耗内存。
-
使用
GOMAXPROCS控制并行度:默认等于 CPU 核心数,可通过runtime.GOMAXPROCS(n)调整。 -
启用竞态检测:编译时添加
-race标志,可发现数据竞争问题:go run -race main.go -
避免共享内存:优先通过 channel 传递数据,而非共享变量加锁。
Go 的并发哲学是:“不要通过共享内存来通信,而应通过通信来共享内存”。坚持这一原则,能写出更清晰、更安全的并发代码。

暂无评论,快来抢沙发吧!