2. Go 基础回顾:goroutine 与 channel 基础、select 多路复用、sync 包基础用法
在深入中间件链路的重试与错误处理之前,必须夯实 Go 并发编程的三大基石:goroutine(轻量级线程)、channel(通信机制)以及 sync 包(同步原语)。本章将系统梳理这些基础,为后续设计并发安全的重试队列、超时控制与熔断器打下坚实基础。
2.1 goroutine:轻量级并发单元
goroutine 是 Go 运行时管理的用户态线程,初始栈仅 2KB,可动态伸缩。通过 go 关键字即可启动一个并发任务:
go func() {
// 并发执行的逻辑
fmt.Println("hello from goroutine")
}()
goroutine 的调度由 Go 运行时完成,不依赖操作系统线程,因此可以轻松创建数十万个 goroutine 而不会耗尽系统资源。在中间件链路中,每个请求的处理、每个重试任务的执行都可以封装为一个 goroutine。
2.2 channel:goroutine 之间的通信桥梁
Go 的并发哲学是“不要通过共享内存来通信,而应通过通信来共享内存”。channel 正是这一理念的核心实现。channel 分为无缓冲(同步)和有缓冲(异步)两种:
| 类型 | 声明方式 | 行为特征 | 适用场景 |
|---|---|---|---|
| 无缓冲 channel | make(chan T) |
发送和接收必须同时就绪,否则阻塞 | 严格同步、事件通知、信号传递 |
| 有缓冲 channel | make(chan T, N) |
缓冲区未满时发送不阻塞,未空时接收不阻塞 | 任务队列、流量整形、解耦生产消费 |
基本操作示例:
ch := make(chan int, 3) // 有缓冲 channel
// 发送
ch <- 42
// 接收
val := <-ch
// 关闭
close(ch)
// 安全遍历
for v := range ch {
fmt.Println(v)
}
close(ch) 配合 for range 或 select 来安全终止 goroutine。
2.3 select 多路复用:同时监听多个 channel
select 语句允许一个 goroutine 同时等待多个 channel 的读写操作,是构建超时控制、优雅退出、多路任务调度的核心语法。其基本结构类似 switch,但每个 case 必须是一个 channel 操作:
select {
case msg1 := <-ch1:
fmt.Println("收到 ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("收到 ch2:", msg2)
case <-time.After(3 * time.Second):
fmt.Println("超时,执行降级逻辑")
default:
// 所有 channel 均未就绪时立即执行
fmt.Println("无就绪 channel,跳过")
}
在中间件链路中,select 的典型应用包括:
- 超时控制: 结合
time.After实现请求级别的超时熔断。 - 优雅退出: 监听
ctx.Done()或退出信号 channel,安全清理资源。 - 优先级调度: 通过
select配合default实现非阻塞尝试。
select 会随机选择一个执行。不要依赖 case 的书写顺序来保证优先级;如需优先级,应使用嵌套 select 或显式判断。
2.4 sync 包:同步原语
虽然 channel 能解决大部分并发通信问题,但在共享资源保护、一次性初始化、并发任务等待等场景下,sync 包提供的同步原语更加高效和直接。
2.4.1 WaitGroup:等待一组 goroutine 完成
WaitGroup 用于主 goroutine 等待一组子 goroutine 全部执行完毕。其内部维护一个计数器,Add(delta) 增加计数,Done() 减少计数,Wait() 阻塞直到计数归零。
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("worker %d 完成\n", id)
}(i)
}
wg.Wait() // 等待所有 worker 完成
fmt.Println("所有任务结束")
Add 必须在启动 goroutine 之前调用,且 Done 通常配合 defer 使用,确保即使 goroutine 发生 panic 也能正确减少计数,避免死锁。
2.4.2 Mutex 与 RWMutex:互斥锁与读写锁
当多个 goroutine 并发访问同一共享变量时,需要使用互斥锁保证数据一致性。Go 提供了两种锁:
| 锁类型 | 方法 | 特性 | 性能 | 适用场景 |
|---|---|---|---|---|
sync.Mutex |
Lock() / Unlock() |
互斥访问,同一时刻只有一个 goroutine 持有锁 | 较低(所有操作串行化) | 写多读少、临界区操作简单 |
sync.RWMutex |
RLock() / RUnlock()(读锁)Lock() / Unlock()(写锁) |
读锁可被多个 goroutine 同时持有;写锁互斥所有读锁和写锁 | 较高(读操作可并发) | 读多写少、缓存、配置读取 |
典型用法示例:
type SafeCounter struct {
mu sync.RWMutex
value int
}
func (c *SafeCounter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *SafeCounter) Value() int {
c.mu.RLock()
defer c.mu.RUnlock()
return c.value
}
RWMutex 而非 Mutex,可显著降低锁竞争。
2.5 本章小结
本章回顾了 Go 并发编程的四个核心知识点:
- goroutine:轻量级并发单元,通过
go关键字启动,是构建中间件链路并发模型的基础。 - channel:goroutine 之间的通信桥梁,无缓冲用于同步,有缓冲用于异步解耦。
- select:多路复用机制,支持超时控制、优雅退出、非阻塞尝试等高级模式。
- sync 包:
WaitGroup用于等待任务完成,Mutex和RWMutex用于保护共享资源。