Go select 常见问题

go select Go 中的 select 提供了方便的多路复用功能,这里记录几个常见的”坑”。

break 匹配问题

select 的每个 case (包括 default)会隐含自动使用 break 语句, 所以在其中使用 break 语句会被 select”吸收”而不会传到到外层的 for 循环,需要用 break label 解决

// 错误示例
for {
    select {
    case <-ch1:
        break   // 这个 break 无法跳出 for
    }
}

// 正确示例
outer:
for {
    select {
    case <-ch1:
        break outer // 可以跳出 for 循环
    }
}

timer.After 泄漏问题

// 错误示例
for {
    select {
    case <-ch:
        // do something...
    case <-time.After(3 * time.Minute): // 高并发下被不断执行
        // timeout
    // 这里如果不小心加上 default,会直接导致 OOM
    }
}

上面的代码在ch不断接收数据时会导致内存暴增。 原因是每次执行select时都会执行一次<-time.After(3 * time.Minute)创建一个 timer, 这个 timer 在 ch 数据到来后实际上没有用了,但是没有及释放,产生了积压

下面的方案用一个 timer 对象就实现了相同功能,并且不会造成 timer 泄漏

// 解决方案
func test(ch chan struct{}) {
    const TimeoutInterval = 3 * time.Minute
    ticker := time.NewTimer(TimeoutInterval)
    defer ticker.Stop()

    ...
    for {
        select {
        case <-ch:
            // do something...
            ticker.Reset(TimeoutInterval)
        case <-ticker.C:
            // timeout
        }
    }
}
  • 解决泄漏的方法是用 time.ticker 时及时 stop
    • 另外在1.23版本终于把这个问题修正了,系统做了特殊处理,及时释放time.After()被阻塞的线程