Go 并发编程:Goroutine 与 Channel
Go 语言的并发模型是其最大的卖点之一。不同于传统的线程模型,Go 通过 Goroutine(轻量级协程)和 Channel(通道)实现了优雅的 CSP(Communicating Sequential Processes)并发模式。本文将深入探讨 Go 并发编程的核心概念与最佳实践。
一、Goroutine:轻量级并发
Goroutine 是 Go 运行时管理的轻量级线程,创建成本极低(约 2KB 栈空间):
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
for i := 0; i < 3; i++ {
fmt.Printf("Hello, %s! (%d)\n", name, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// 使用 go 关键字启动 Goroutine
go sayHello("Alice")
go sayHello("Bob")
// 主 Goroutine 需要等待,否则程序直接退出
time.Sleep(500 * time.Millisecond)
fmt.Println("Done!")
}
⚠️ 注意:time.Sleep 不是正确的同步方式,仅用于演示。
二、Channel:Goroutine 间的通信
Go 的哲学是"不要通过共享内存来通信,而要通过通信来共享内存":
1. 基本用法
// 创建无缓冲 Channel
ch := make(chan int)
// 发送数据(阻塞,直到有接收者)
go func() {
ch <- 42
}()
// 接收数据(阻塞,直到有数据)
value := <-ch
fmt.Println(value) // 42
2. 带缓冲的 Channel
// 创建缓冲大小为 3 的 Channel
ch := make(chan string, 3)
// 在缓冲未满时,发送不会阻塞
ch <- "A"
ch <- "B"
ch <- "C"
// ch <- "D" // 这里会阻塞,因为缓冲已满
fmt.Println(<-ch) // "A"
fmt.Println(<-ch) // "B"
3. 关闭 Channel
ch := make(chan int, 5)
// 生产者
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // 关闭 Channel,表示不再发送数据
}()
// 消费者:使用 range 遍历,Channel 关闭时自动退出
for value := range ch {
fmt.Println(value)
}
// 检测 Channel 是否关闭
value, ok := <-ch
if !ok {
fmt.Println("Channel 已关闭")
}
三、select:多路复用
select 语句用于同时监听多个 Channel:
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(100 * time.Millisecond)
ch1 <- "来自 ch1"
}()
go func() {
time.Sleep(200 * time.Millisecond)
ch2 <- "来自 ch2"
}()
// 等待多个 Channel,哪个先就绪就执行哪个
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
}
超时处理
select {
case result := <-ch:
fmt.Println("收到结果:", result)
case <-time.After(3 * time.Second):
fmt.Println("操作超时")
}
非阻塞操作
select {
case msg := <-ch:
fmt.Println("收到:", msg)
default:
fmt.Println("没有数据,继续执行其他逻辑")
}
四、sync 包:同步原语
1. WaitGroup:等待一组 Goroutine 完成
import "sync"
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // 计数器 +1
go func(id int) {
defer wg.Done() // 计数器 -1
fmt.Printf("Worker %d 完成\n", id)
}(i)
}
wg.Wait() // 阻塞直到计数器归零
fmt.Println("所有任务完成")
}
2. Mutex:互斥锁
import "sync"
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
3. Once:只执行一次
var once sync.Once
var instance *Database
func GetDatabase() *Database {
once.Do(func() {
instance = &Database{}
instance.Connect()
})
return instance
}
五、并发模式实战
1. Worker Pool(工作池)
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d 处理任务 %d\n", id, job)
time.Sleep(time.Second)
results <- job * 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 r := 1; r <= 9; r++ {
fmt.Println("结果:", <-results)
}
}
2. Fan-out/Fan-in(扇出/扇入)
// 扇出:一个输入,多个处理者
func fanOut(input <-chan int, workers int) []<-chan int {
channels := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
channels[i] = process(input)
}
return channels
}
// 扇入:多个输入,合并为一个输出
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
六、常见陷阱与最佳实践
- 避免 Goroutine 泄露:确保 Goroutine 有退出条件;
- 使用 context 控制生命周期:传递取消信号;
- Channel 方向限制:使用
chan<-和<-chan明确意图; - 优先使用 Channel:而非共享内存 + 锁;
- 使用 race detector:
go run -race main.go。
七、学习资源
- Go 并发模式:go.dev/blog/pipelines
- Effective Go:Concurrency 章节
- 《Concurrency in Go》:O'Reilly 经典书籍
Go 的并发模型优雅而强大,但也需要谨慎使用。理解 Goroutine 调度、Channel 阻塞特性、以及常见的并发模式,将帮助你写出高效、安全的并发代码。动手实践是最好的学习方式!