Golang Select 深度解析

基于 Go 1.26.4 源码,源码路径:github.com/go-go1.26.4
核心源文件:runtime/select.goruntime/chan.gocmd/compile/internal/walk/select.go


select 是 Go 语言与switch相似但本质不同的控制结构。每一个 case 必须是 channel 的发送或者接收操作,当多个 channel 同时就绪的时候,select 会随机选择其中一个来执行,这种随机性是为了保证公平——没有任何一个 channel 会被饥饿。

1 select 功能完整介绍

1.1 语法与核心概念

Go 的 select 语句是专门为 channel 设计的多路复用器,类似于 Unix 的 select/poll/epoll

select {
case v := <-ch1:          // 接收 case
    fmt.Println("received:", v)
case ch2 <- value:         // 发送 case
    fmt.Println("sent:", value)
case v, ok := <-ch3:       // 接收 + ok 判断
    if !ok { fmt.Println("ch3 closed") }
default:                   // 非阻塞
    fmt.Println("no channel ready")
}

1.2 核心语义规则

规则 说明
随机选择 多个 case 同时就绪时,随机选择一个执行
阻塞等待 没有 case 就绪且无 default → 阻塞当前 goroutine
非阻塞 有 default → 无 case 就绪时执行 default
空 select select{} → 永久阻塞(等同于死锁)
nil channel nil channel 的 case 永远不会被选中
close 优先 已关闭的 channel 的接收 case 立刻就绪

多个就绪

仅一个就绪

无就绪

Yes

No

select 语句

有 case 就绪?

随机选一个执行
pollorder 随机化

执行该 case

有 default?

执行 default

★ 阻塞当前 G
注册到所有 channel
gopark 挂起

某个 channel 就绪
唤醒 G

从其他 channel 注销
执行选中 case

1.3 适用场景

Go Select 适用场景

超时控制

select + time.After

context.Done

非阻塞收发

default 分支

尝试性操作

多路复用

监听多个 channel

合并数据流

退出信号

done channel

context cancel

限流

ticker + quit

定时任务


2 编译器对 select 的重写优化

源码位置:cmd/compile/internal/walk/select.go

编译器在编译期对 select 做了优化重写,只有通用情况才调用 selectgo

在这里插入图片描述

2.1 优化1:零 case select → block()

// 源码
select {}

// 编译器重写为
runtime.block()
// → gopark(nil, nil, waitReasonSelectNoCases, traceBlockForever, 1)
// 永久阻塞,等价于死锁

2.2 优化2:单 case + 无 default → 直接 channel 操作

// 源码
select {
case v := <-ch:
    fmt.Println(v)
}

// 编译器重写为:直接调用 chanrecv
v := <-ch
fmt.Println(v)
// 无需 selectgo 的开销

2.3 优化3:单 case + default → 非阻塞操作

// 源码
select {
case v := <-ch:
    fmt.Println("received:", v)
default:
    fmt.Println("no data")
}

// 编译器重写为:调用 selectnbrecv(非阻塞接收)
if selected, ok := selectnbrecv(&v, ch); selected {
    fmt.Println("received:", v)
} else {
    fmt.Println("no data")
}

// selectnbrecv 内部就是 chanrecv(c, elem, block=false)
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected, received bool) {
    return chanrecv(c, elem, false)
}
// 发送 + default 的重写
select {
case ch <- value:
    fmt.Println("sent")
default:
    fmt.Println("channel full")
}

// 编译器重写为
if selectnbsend(ch, value) {
    fmt.Println("sent")
} else {
    fmt.Println("channel full")
}

// selectnbsend 内部
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
    return chansend(c, elem, false, sys.GetCallerPC())
}

2.4 通用情况:多 case → selectgo

3 个及以上 case(或有 default 的 2 case)编译器调用 selectgo

// 编译器生成的伪代码:

// 1. 分配 scase 数组(栈上)
selv := [ncases]scase{
    {c: ch1, elem: &v1},  // case 0: 接收
    {c: ch2, elem: &v2},  // case 1: 发送
    ...
}

// 2. 分配 order 数组(栈上)
order := [2*ncases]uint16{}

// 3. 调用 selectgo
chosen, recvOK := selectgo(&selv[0], &order[0], pc0, nsends, nrecvs, dflt==nil)

// 4. 根据 chosen 分发到对应 case
switch chosen {
case 0:  // 第一个 case 被选中
    // ...
case 1:  // 第二个 case 被选中
    // ...
case -1: // default 被选中
    // ...
}

0

1 + 无 default

1 + default

≥2 或 2+default

select 语句

case 数量?

block() 永久阻塞

直接 channel 操作
chansend/chanrecv

selectnbsend/selectnbrecv
非阻塞操作

selectgo 通用路径


3 selectgo 完整源码逐行解析

源码位置:runtime/select.go:116

3.1 scase 结构体

type scase struct {
    c    *hchan         // ★ 关联的 channel
    elem unsafe.Pointer // ★ 数据元素指针
                          // 发送 case: 指向待发送的值
                          // 接收 case: 指向接收变量的地址
}
scase 内存布局 (16 bytes on 64-bit)
┌─────────────────────────────────┐
│  c     *hchan      (8B)         │ → channel 指针
├─────────────────────────────────┤
│  elem  unsafe.P    (8B)         │ → 数据指针
└─────────────────────────────────┘

编译器分配的 scase 数组(栈上):
  case 0: recv  → scase{c: ch1, elem: &v}
  case 1: send  → scase{c: ch2, elem: &val}
  case 2: recv  → scase{c: ch3, elem: nil}  // v, ok:= <-ch3 的 ok 分支
  default: 不在 scase 数组中(dflt 单独处理)

3.2 selectgo 参数与返回值

func selectgo(
    cas0    *scase,     // scase 数组首地址
    order0  *uint16,    // order 数组首地址(2*ncases 大小)
    pc0     *uintptr,   // race detector PC 数组
    nsends  int,        // 发送 case 数量
    nrecvs  int,        // 接收 case 数量
    block   bool,       // 是否阻塞(无 default 时 block=true)
) (int, bool)           // 返回: (选中的 case 索引, 接收是否成功)

关键设计:case 编排顺序

scase 数组布局:
  [0]       [1]       ... [nsends-1]  [nsends]      ... [ncases-1]
  send0     send1     ... send_last   recv0         ... recv_last
  ↑─────── nsends ──────────↑↑──────── nrecvs ───────────────↑

发送 case 排在前面,接收 case 排在后面
这样 casi < nsends 表示是发送 case,否则是接收 case

3.3 Phase 0: 初始化与 pollorder 随机化

在这里插入图片描述
现在我们进入了运行时的核心。selectgo 函数首先做初始化工作。它从输入的 scase 数组中创建两个辅助数组——pollorder 和 lockorder。pollorder 是通过 fastrandn 随机打乱的,决定了下一次遍历 case 的顺序,这样保证了公平性,没有任何 channel 会被饿死。lockorder 则是按照 channel 的地址进行排序的,这样就保证了多个 goroutine 同时对同一批 channel 加锁的时候锁定顺序是一致的,从根本上避免了死锁的可能。这里有一个很重要的细节,你可以看到代码中使用了一个 for 循环配合 fastrandn 来做 Fisher-Yates 洗牌算法,每次循环随机交换两个元素的位置。初始化完毕后,sel lock 会按 lockorder 的顺序依次锁定所有 channel,然后程序进入主循环。

func selectgo(...) (int, bool) {
    // 解析参数
    ncases := nsends + nrecvs
    scases := cas1[:ncases:ncases]
    pollorder := order1[:ncases:ncases]       // 前半段: 轮询顺序
    lockorder := order1[ncases:][:ncases:ncases]  // 后半段: 加锁顺序

    // ★★★ 生成随机轮询顺序(Fisher-Yates 洗牌算法)★★★
    norder := 0
    for i := range scases {
        cas := &scases[i]
        if cas.c == nil {
            cas.elem = nil    // nil channel → 跳过
            continue          // 不加入 pollorder 和 lockorder
        }
        // ★ 随机插入位置 j
        j := cheaprandn(uint32(norder + 1))
        pollorder[norder] = pollorder[j]  // 移动 j 位置的值到末尾
        pollorder[j] = uint16(i)          // 新值插入到 j 位置
        norder++
    }
    pollorder = pollorder[:norder]
    // 结果: pollorder 是 [0,1,2,...,norder-1] 的随机排列
    // 例: ncases=4, pollorder 可能是 [2, 0, 3, 1]

Fisher-Yates 洗牌过程图示

初始: pollorder = []

i=0: j=random(1)=0  → pollorder = [0]
i=1: j=random(2)=1  → pollorder = [0, 1]
i=2: j=random(3)=0  → pollorder = [2, 1, 0]   ← 0 被移到末尾,2 插入位置 0
i=3: j=random(4)=2  → pollorder = [2, 1, 3, 0] ← 0 被移到末尾,3 插入位置 2

最终轮询顺序: 2 → 1 → 3 → 0
★ 这保证了多个 case 同时就绪时,被选中的概率均等

3.4 Phase 0 续: lockorder 排序

    // ★★★ 按 channel 地址排序 lockorder(堆排序)★★★
    // 原因: 防止死锁!
    //   如果两个 G 同时 select 多个 channel,
    //   加锁顺序不一致可能导致死锁
    //   按地址排序保证了所有 G 的加锁顺序一致

    // 堆排序建立最大堆
    for i := range lockorder {
        j := i
        c := scases[pollorder[i]].c
        for j > 0 && scases[lockorder[(j-1)/2]].c.sortkey() < c.sortkey() {
            k := (j - 1) / 2
            lockorder[j] = lockorder[k]
            j = k
        }
        lockorder[j] = pollorder[i]
    }
    // 堆排序提取
    for i := len(lockorder) - 1; i >= 0; i-- {
        // ... 标准堆排序 ...
    }
    // 结果: lockorder 按 channel 地址从小到大排列

lockorder (排序)

0 → 2 → 1 → 3
ch0.addr < ch2.addr < ch1.addr < ch3.addr
决定加锁顺序

pollorder (随机)

2 → 1 → 3 → 0
决定检查顺序

为什么需要两种顺序?

顺序 作用 生成方式
pollorder 决定检查 case 的顺序 → 保证公平性 随机洗牌
lockorder 决定加锁 channel 的顺序 → 防止死锁 按地址排序

3.5 sellock:按 lockorder 依次加锁

func sellock(scases []scase, lockorder []uint16) {
    var c *hchan
    for _, o := range lockorder {
        c0 := scases[o].c
        if c0 != c {         // ★ 同一个 channel 不重复加锁
            c = c0
            lock(&c.lock)
        }
    }
}

// ★★★ 防死锁原理 ★★★
// 假设:
//   G1: select { case ch1: ... case ch2: ... }
//   G2: select { case ch2: ... case ch1: ... }
//
// 不排序:
//   G1 先锁 ch1, G2 先锁 ch2 → 死锁!
//
// lockorder 排序后(ch1.addr < ch2.addr):
//   G1 和 G2 都先锁 ch1, 再锁 ch2 → 不会死锁 ✅

在这里插入图片描述
主循环是 select 的核心,分为三个阶段。

  • 第一阶段,按照 pollorder 的顺序遍历所有 case,检查每个 channel 的状态。对于接收操作,如果发送等待队列里有 goroutine 在等待,就跳到 recv 标签;如果缓冲区有数据,就跳到 bufrecv 标签;如果 channel 已经关闭了,就跳到 rclose 标签。对于发送操作,如果 channel 已经关闭,直接跳到 sclose 标签会触发 panic;如果接收等待队列里有 goroutine 在等待,就跳到 send 标签;如果缓冲区有空位,就跳到 bufsend 标签。如果没有任何 case 就绪,就进入第二阶段。
  • 第二阶段,为每一个 case 创建一个 sudog 结构体,这个结构体代表了当前 goroutine 的等待状态。sudog 会被加入到对应 channel 的发送或者接收等待队列中,所有的 sudog 会串成一个链表挂在当前 goroutine 的 waiting 字段上。然后调用 gopark 把当前 goroutine 挂起。
  • 第三阶段,当 goroutine 被唤醒的时候,它从 gp.param 中取回 sudog,然后遍历 lockorder 找到匹配的 case,清理其他 channel 上未使用的 sudog,释放不再需要的结构体,最后跳转到对应的操作标签执行数据的收发。

3.6 Pass 1: 按随机顺序检查是否有 case 就绪

    // ★★★ Pass 1: 按 pollorder 检查 ★★★
    for _, casei := range pollorder {
        casi = int(casei)
        cas = &scases[casi]
        c = cas.c

        if casi >= nsends {
            // ──── 接收 case ────
            
            // 检查1: 有等待的发送者?
            sg = c.sendq.dequeue()
            if sg != nil { goto recv }
            
            // 检查2: 缓冲区有数据?
            if c.qcount > 0 { goto bufrecv }
            
            // 检查3: 通道已关闭?
            if c.closed != 0 { goto rclose }
            
        } else {
            // ──── 发送 case ────
            
            // 检查1: 通道已关闭?(发送到已关闭通道 → panic)
            if c.closed != 0 { goto sclose }
            
            // 检查2: 有等待的接收者?
            sg = c.recvq.dequeue()
            if sg != nil { goto send }
            
            // 检查3: 缓冲区有空间?
            if c.qcount < c.dataqsiz { goto bufsend }
        }
    }

    // Pass 1 没有找到就绪的 case
    if !block {
        selunlock(scases, lockorder)
        casi = -1
        goto retc              // 有 default → 返回 -1
    }

接收 case

Yes

No

Yes

No

Yes

No

发送 case

Yes

No

Yes

No

Yes

No

Pass 1: 按 pollorder 检查每个 case

casi >= nsends?
(接收 vs 发送)

sendq 有等待者?

goto recv
直接从发送者接收

qcount > 0?

goto bufrecv
从缓冲区接收

closed != 0?

goto rclose
通道已关闭

此 case 未就绪

closed != 0?

goto sclose
panic!

recvq 有等待者?

goto send
直接发给接收者

qcount < dataqsiz?

goto bufsend
写入缓冲区

检查下一个 case

3.7 Pass 2: 注册到所有 channel 的等待队列

    // ★★★ Pass 2: 没有 case 就绪 + 需要阻塞 → 注册等待 ★★★
    nextp = &gp.waiting
    for _, casei := range lockorder {    // ★ 按 lockorder(非 pollorder)遍历
        casi = int(casei)
        cas = &scases[casi]
        c = cas.c

        sg := acquireSudog()            // 从池中获取 sudog
        sg.g = gp
        sg.isSelect = true              // ★ 标记来自 select
        sg.elem.set(cas.elem)
        sg.c.set(c)

        // 构建 gp.waiting 链表(按 lockorder 排列)
        *nextp = sg
        nextp = &sg.waitlink

        if casi < nsends {
            c.sendq.enqueue(sg)         // 发送 case → 加入 sendq
        } else {
            c.recvq.enqueue(sg)         // 接收 case → 加入 recvq
        }
    }

    // ★ 挂起当前 goroutine
    gp.parkingOnChan.Store(true)
    gopark(selparkcommit, nil, waitReason, traceBlockSelect, 1)
    // ★★★ 此时当前 G 完全停止,等待某个 channel 操作唤醒它 ★★★

selparkcommit——挂起前的解锁

func selparkcommit(gp *g, _ unsafe.Pointer) bool {
    gp.activeStackChans = true
    gp.parkingOnChan.Store(false)

    // ★★★ 在挂起前释放所有 channel 锁 ★★★
    // 必须在 gopark 内部解锁,否则:
    //   1. 其他 G 无法操作这些 channel
    //   2. 可能死锁(持有锁 + 挂起)
    var lastc *hchan
    for sg := gp.waiting; sg != nil; sg = sg.waitlink {
        if sg.c.get() != lastc && lastc != nil {
            unlock(&lastc.lock)     // 按链表顺序解锁
        }
        lastc = sg.c.get()
    }
    if lastc != nil {
        unlock(&lastc.lock)
    }
    return true
}

3.8 被唤醒后的 Pass 3: 清理未成功的注册

    // ★ 被某个 channel 操作唤醒 ★
    gp.activeStackChans = false
    
    // ★★★ 重新加锁所有 channel ★★★
    sellock(scases, lockorder)

    // 找到唤醒我们的那个 sudog
    sg = (*sudog)(gp.param)

    // ★★★ Pass 3: 从所有其他 channel 的等待队列中注销 ★★★
    casi = -1
    cas = nil
    caseSuccess = false
    sglist = gp.waiting

    // 先清空所有 elem(帮助 GC)
    for sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink {
        sg1.isSelect = false
        sg1.elem.set(nil)
        sg1.c.set(nil)
    }
    gp.waiting = nil

    // ★ 遍历所有注册的 sudog
    for _, casei := range lockorder {
        k = &scases[casei]
        if sg == sglist {
            // ★ 这就是唤醒我们的那个 sudog → 记录选中 case
            casi = int(casei)
            cas = k
            caseSuccess = sglist.success
        } else {
            // ★ 不是唤醒我们的 → 从等待队列中移除
            c = k.c
            if int(casei) < nsends {
                c.sendq.dequeueSudoG(sglist)
            } else {
                c.recvq.dequeueSudoG(sglist)
            }
        }
        sgnext = sglist.waitlink
        sglist.waitlink = nil
        releaseSudog(sglist)     // 归还到池
        sglist = sgnext
    }
其他 Goroutine Channel 2 Channel 1 当前 Goroutine 其他 Goroutine Channel 2 Channel 1 当前 Goroutine Pass 1: 检查就绪 → 无 注册 sudog 到 recvq 注册 sudog 到 sendq gopark 挂起 向 Ch1 发送数据 唤醒(通过 send 直接拷贝) 被唤醒,重新加锁 Pass 3: 找到唤醒者(Ch1) 从 sendq 移除 sudog 执行 Ch1 对应的 case

3.9 就绪 case 的具体执行路径

recv——从等待的发送者直接接收

recv:
    // 可以从沉睡的发送者接收(同步接收)
    recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
    recvOK = true
    goto retc
    // recv 内部:
    //   无缓冲通道: recvDirect → 直接从发送者栈拷贝到接收者
    //   有缓冲通道: 从缓冲区头部取 + 发送者数据放入尾部
    //   然后 goready 唤醒发送者

bufrecv——从缓冲区接收

bufrecv:
    recvOK = true
    qp = chanbuf(c, c.recvx)                    // 缓冲区当前读取位置
    if cas.elem != nil {
        typedmemmove(c.elemtype, cas.elem, qp)  // 拷贝到接收变量
    }
    typedmemclr(c.elemtype, qp)                 // 清空缓冲区 slot
    c.recvx++
    if c.recvx == c.dataqsiz { c.recvx = 0 }    // 环形回绕
    c.qcount--
    selunlock(scases, lockorder)
    goto retc

send——直接发给等待的接收者

send:
    send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
    goto retc
    // send 内部:
    //   sendDirect → 直接从发送者栈拷贝到接收者栈
    //   goready 唤醒接收者

bufsend——写入缓冲区

bufsend:
    typedmemmove(c.elemtype, chanbuf(c, c.sendx), cas.elem)  // 写入缓冲区
    c.sendx++
    if c.sendx == c.dataqsiz { c.sendx = 0 }
    c.qcount++
    selunlock(scases, lockorder)
    goto retc

rclose——从已关闭通道接收

rclose:
    selunlock(scases, lockorder)
    recvOK = false                         // ★ received=false
    if cas.elem != nil {
        typedmemclr(c.elemtype, cas.elem)  // 零值
    }
    goto retc

sclose——向已关闭通道发送

sclose:
    selunlock(scases, lockorder)
    panic(plainError("send on closed channel"))  // ★ panic!

4 selectgo 完整流程总图

Yes

No

false

true

selectgo 入口

Phase 0: 初始化

pollorder 随机洗牌
Fisher-Yates

lockorder 按 ch 地址排序
堆排序

sellock 按 lockorder 加锁

Pass 1: 按 pollorder 检查就绪

有 case 就绪?

执行就绪 case
解锁返回

block=true?
(无 default)

返回 casi=-1
(default)

Pass 2: 注册等待

为每个 case 创建 sudog
加入 channel 等待队列

gopark 挂起
selparkcommit 解锁

被唤醒

sellock 重新加锁

Pass 3: 清理注销

找到唤醒者 case

从其他 channel 移除 sudog

releaseSudog 归还

selunlock 解锁

返回 (casi, recvOK)


5 nil channel 在 select 中的行为

5.1 nil channel 永远不被选中

var ch1 chan int   // nil
ch2 := make(chan int, 1)

select {
case v := <-ch1:     // ★ 永远不会被选中!
    fmt.Println(v)
case v := <-ch2:
    fmt.Println(v)   // 只会走这个
}

源码原因cas.c == nil 的 case 被跳过,不加入 pollorder 和 lockorder:

for i := range scases {
    cas := &scases[i]
    if cas.c == nil {
        cas.elem = nil    // allow GC
        continue          // ★ 跳过!不加入任何顺序数组
    }
    // ... 加入 pollorder ...
}

5.2 利用 nil channel 禁用 select 分支

func merge(ch1, ch2 <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for ch1 != nil || ch2 != nil {
            select {
            case v, ok := <-ch1:
                if !ok { ch1 = nil; continue }  // ★ 设为 nil 禁用此分支
                out <- v
            case v, ok := <-ch2:
                if !ok { ch2 = nil; continue }  // ★ 设为 nil 禁用此分支
                out <- v
            }
        }
    }()
    return out
}

都关闭后

ch1 关闭后

ch1 关闭前

ch1!=nil → 可选中

ch2!=nil → 可选中

ch1=nil → 永不选中

ch2!=nil → 唯一选项

ch1=nil → 退出

ch2=nil → 退出


6 select 的公平性保证

6.1 pollorder 随机化的必要性

// 没有 pollorder 随机化时的问题:
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch1 <- 1
ch2 <- 2

select {
case v := <-ch1:   // 总是先检查 ch1
    fmt.Println(v)  // 永远输出 1,ch2 饥饿!
case v := <-ch2:
    fmt.Println(v)  // 永远不会执行
}

// 有 pollorder 随机化:
// 每次执行 select 时,检查顺序随机
// ch1 和 ch2 被选中的概率均等

6.2 isSelect 标志与 CAS 竞争

// waitq.dequeue 中的特殊处理:
func (q *waitq) dequeue() *sudog {
    for {
        sgp := q.first
        // ...
        if sgp.isSelect {
            // ★ 来自 select 的 sudog 可能在多个 channel 的等待队列中
            // 使用 CAS 确保只有一个 channel 能唤醒它
            if !sgp.g.selectDone.CompareAndSwap(0, 1) {
                // 另一个 channel 已经赢得了竞争
                continue  // 跳过,尝试下一个
            }
        }
        return sgp
    }
}

场景

一个 G 在 select 中同时注册到 ch1.recvq 和 ch2.recvq

如果 ch1 和 ch2 同时有数据可读:
  1. ch1 的发送者调用 send() → dequeue ch1.recvq 中的 sudog
     → selectDone.CAS(0,1) 成功 → 唤醒 G
  2. ch2 的发送者调用 send() → dequeue ch2.recvq 中的 sudog
     → selectDone.CAS(0,1) 失败(已经是1)→ 跳过
     → 继续找下一个等待者

★ CAS 确保一个 G 只被一个 channel 唤醒

7 完整使用示例合集

7.1 超时控制

func doWithTimeout(ch <-chan int, timeout time.Duration) (int, error) {
    select {
    case v := <-ch:
        return v, nil
    case <-time.After(timeout):
        return 0, fmt.Errorf("timeout after %v", timeout)
    }
}

7.2 非阻塞发送/接收

// 非阻塞接收
select {
case v := <-ch:
    fmt.Println("received:", v)
default:
    fmt.Println("no data available")
}

// 非阻塞发送
select {
case ch <- value:
    fmt.Println("sent")
default:
    fmt.Println("channel full, dropped")
}

7.3 优雅退出(done channel 模式)

func worker(done <-chan struct{}, jobs <-chan int) {
    for {
        select {
        case <-done:
            fmt.Println("worker exiting")
            return
        case j, ok := <-jobs:
            if !ok {
                return  // jobs channel 已关闭
            }
            process(j)
        }
    }
}

7.4 定时器 + 退出

func tickerDemo(done <-chan struct{}) {
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()
    
    for {
        select {
        case <-done:
            return
        case t := <-ticker.C:
            fmt.Println("tick at", t)
        }
    }
}

7.5 扇入(多 channel 合并)

func fanIn(ch1, ch2 <-chan string) <-chan string {
    merged := make(chan string)
    go func() {
        defer close(merged)
        for ch1 != nil || ch2 != nil {
            select {
            case s, ok := <-ch1:
                if !ok { ch1 = nil; continue }
                merged <- s
            case s, ok := <-ch2:
                if !ok { ch2 = nil; continue }
                merged <- s
            }
        }
    }()
    return merged
}

8 经典问题与优化

8.1 select 中 for range 的空循环问题

// ❌ 错误:空 select 导致 CPU 100%
for {
    select {
    case v := <-ch:
        process(v)
    default:
        // ★ 空的 default → 非阻塞 → 无限循环!CPU 飙升!
    }
}

// ✅ 正确:无 default → 阻塞等待
for {
    select {
    case v := <-ch:
        process(v)
    }
}

// ✅ 或添加 sleep
for {
    select {
    case v := <-ch:
        process(v)
    default:
        time.Sleep(10 * time.Millisecond)  // 让出 CPU
    }
}

8.2 select 死锁场景

// ❌ 死锁1: 空 select
select {}   // 永久阻塞,没有任何 goroutine 能唤醒

// ❌ 死锁2: 循环等待
// G1: select { case <-ch2: ... case ch1<-: ... }  等 ch2 才发 ch1
// G2: select { case <-ch1: ... case ch2<-: ... }  等 ch1 才发 ch2
// ★ 虽然 lockorder 防止了 channel 锁死锁,但逻辑层面仍可能死锁

// ✅ 使用 context 打破循环
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
select {
case <-ctx.Done():
    return ctx.Err()
case v := <-ch:
    process(v)
}

8.3 time.After 内存泄漏

// ❌ 泄漏:每次循环创建新 timer
for {
    select {
    case v := <-ch:
        process(v)
    case <-time.After(5 * time.Second):
        // ★ 每次 select 都创建新 timer
        // 如果 ch 频繁有数据,timer 不会被 GC 直到触发
        fmt.Println("timeout")
    }
}

// ✅ 修复:复用 timer
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
for {
    select {
    case v := <-ch:
        process(v)
        if !timer.Stop() {
            <-timer.C  // 排空 channel
        }
        timer.Reset(5 * time.Second)
    case <-timer.C:
        fmt.Println("timeout")
        timer.Reset(5 * time.Second)
    }
}

8.4 select case 数量限制

// ★ 最多 65536 个 case(uint16 索引限制)
// 实际上超过几十个 case 就应该重构代码

// ❌ 过多 case
select {
case <-ch1: ...
case <-ch2: ...
// ... 1000 个 case ...
}

// ✅ 重构:使用 reflect.Select 或 map 分发

9 reflect.Select 动态 select

// reflect.Select 允许运行时动态构建 select
cases := []reflect.SelectCase{
    {Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch1)},
    {Dir: reflect.SelectSend, Chan: reflect.ValueOf(ch2), Send: reflect.ValueOf(42)},
    {Dir: reflect.SelectDefault},
}

chosen, value, ok := reflect.Select(cases)
// chosen: 选中 case 的索引
// value: 接收到的值
// ok: 接收是否成功

底层调用

// reflect.rselect → runtime.reflect_rselect
func reflect_rselect(cases []runtimeSelect) (int, bool) {
    if len(cases) == 0 { block() }
    // 构建 scase 数组
    // 调用 selectgo
    // 返回选中的索引
}

10 总结:Select 核心知识图谱

Select 核心知识

编译器三种优化
0case/1case/2case

selectgo 三阶段
Pass1/Pass2/Pass3

pollorder 随机化
Fisher-Yates 洗牌

lockorder 排序
按 ch 地址 防死锁

公平性保证
随机轮询顺序

nil channel
永不选中

isSelect + CAS
防重复唤醒

selparkcommit
挂起前解锁

常见陷阱
空default/time.After泄漏

selectgo 三阶段一图总结

┌────────────────────────────────────────────────────────────┐
│                   selectgo 执行流程                         │
├──────────┬─────────────────────────────────────────────────┤
│ Phase 0  │ pollorder 随机洗牌 + lockorder 堆排序 + sellock │
├──────────┼─────────────────────────────────────────────────┤
│ Pass 1   │ 按 pollorder 检查就绪                            │
│          │ recv: sendq? → bufrecv? → closed?               │
│          │ send: closed? → recvq? → bufsend?               │
│          │ 有就绪 → 执行 → 返回                              │
│          │ 无就绪 + default → 返回 -1                       │
├──────────┼─────────────────────────────────────────────────┤
│ Pass 2   │ 无就绪 + 无 default → 注册等待                    │
│          │ 每个 case 创建 sudog → 加入 channel 等待队列       │
│          │ gopark 挂起 → selparkcommit 解锁                  │
├──────────┼─────────────────────────────────────────────────┤
│ Pass 3   │ 被唤醒 → sellock 重新加锁                         │
│          │ 找到唤醒者 sudog → 记录 casi                       │
│          │ 从其他 channel 移除 sudog → releaseSudog          │
│          │ selunlock → 返回 (casi, recvOK)                  │
└──────────┴─────────────────────────────────────────────────┘

源码索引

文件 关键内容
runtime/select.go:16 scase 结构体、sellock/selunlockselparkcommit
runtime/select.go:116 selectgo——三阶段核心实现
runtime/select.go:556 reflect_rselect——reflect 动态 select
runtime/select.go:624 dequeueSudoG——从等待队列移除指定 sudog
runtime/chan.go:159 chansend——channel 发送
runtime/chan.go:462 chanrecv——channel 接收
cmd/compile/internal/walk/select.go 编译器三种优化重写 + scase 类型定义

更多推荐