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。

关键特性: goroutine 不是 OS 线程,而是协作式调度的用户态协程。在 I/O 阻塞或 channel 操作时自动让出 CPU,实现高并发下的低开销。

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)
}
常见陷阱: 向已关闭的 channel 发送数据会引发 panic;从已关闭的 channel 接收数据会立即返回零值(可通过第二个返回值判断是否关闭)。在中间件链路中,务必使用 close(ch) 配合 for range 或 select 来安全终止 goroutine。
goroutine 与 channel 通信模型 goroutine A 生产者 / 发送方 channel 无缓冲 / 有缓冲 goroutine B 消费者 / 接收方 ch <- data data <- ch 通信模型要点 • 无缓冲 channel:发送与接收必须同时就绪,实现同步握手 • 有缓冲 channel:缓冲区解耦生产消费速率,允许异步处理 • 关闭 channel 后,接收方仍可读取缓冲区剩余数据

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 实现非阻塞尝试。
最佳实践: 当多个 case 同时就绪时,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,可显著降低锁竞争。
sync 包同步原语对比 WaitGroup 等待 goroutine 集合完成 wg.Add(N) → 增加计数 wg.Done() → 计数减 1 wg.Wait() → 阻塞至 0 适用:批量任务、并发聚合 Mutex 互斥锁,完全串行化 Lock() → 获取锁 Unlock() → 释放锁 同一时刻仅 1 个 goroutine 适用:写多读少、临界区小 RWMutex 读写锁,读并发 RLock() / RUnlock() 多个读锁可同时持有 写锁互斥所有读/写锁 适用:读多写少、缓存

2.5 本章小结

本章回顾了 Go 并发编程的四个核心知识点:

  1. goroutine:轻量级并发单元,通过 go 关键字启动,是构建中间件链路并发模型的基础。
  2. channel:goroutine 之间的通信桥梁,无缓冲用于同步,有缓冲用于异步解耦。
  3. select:多路复用机制,支持超时控制、优雅退出、非阻塞尝试等高级模式。
  4. sync 包:WaitGroup 用于等待任务完成,Mutex 和 RWMutex 用于保护共享资源。