select 是什么?
-
select是 Go 提供的 多路复用原语,只用于 channel 的读写选择。 -
用来等待多个 channel 的读写操作,只要其中任意一个可以执行,就立即执行该分支。
-
是协程并发调度里不可或缺的工具,常用于超时控制、退出信号、流量控制。
语法结构
select {
case val := <-ch1:
fmt.Println("Received:", val)
case ch2 <- 42:
fmt.Println("Sent")
default:
fmt.Println("No channel ready")
}
关键点:
- 多个
case可以同时存在(读/写不限) - 如果多个分支都 ready → 随机选一个执行(公平性)
- 如果没有
default且都阻塞 →select会挂起当前 goroutine - 如果有
default且都阻塞 → 执行default,绝不阻塞
select 的典型场景
🕒 超时控制
select {
case msg := <-ch:
fmt.Println("Received:", msg)
case <-time.After(3 * time.Second):
fmt.Println("Timeout!")
}
👉 time.After 返回一个 channel,超时后自动触发。
❌ 非阻塞操作
select {
case ch <- value:
fmt.Println("Sent")
default:
fmt.Println("Channel not ready, skip sending")
}
如果 channel 满了,不阻塞,直接走 default。
优雅退出
select {
case data := <-dataCh:
// 处理数据
case <-quitCh:
fmt.Println("Received quit signal")
return
}
常用于生产者-消费者模式里的优雅关闭。
** 底层原理 **
- 编译时
select会被编译器编译为对多个 channel 的非阻塞尝试。 - 如果所有都阻塞,则 runtime 会把当前 goroutine 挂起,放进所有 channel 的等待队列(
sendq/recvq)。 - 有新数据到来时,唤醒队列里的 goroutine。
- runtime 会在有多个分支可执行时做伪随机选择,保证公平性(避免饿死)。
关键源码:
runtime/chan.goselectgo函数sudog结构体(等待在 channel 上的 goroutine)
select 的公平性
面试常考点:
多个 case 同时可用,Go 按什么顺序选?
答:
- Go 运行时会伪随机 shuffle case 顺序,然后遍历判断可执行。
- 保证不会固定执行第一个分支 → 防止饿死。
select 的常见陷阱
| 陷阱 | 解释 |
|---|---|
select {} | 空的 select 会永远阻塞,相当于 <-make(chan struct{}) |
Scan | select 只能用于 channel,不支持普通条件 |
| 跨协程 | 不能跨协程写 select case,除非用 chan |
| 已关闭的 channel | 如果从已关闭的 channel 读,永远可读且会读到零值 |
面试扩展:select + closed channel
如果从已关闭的 channel 读,
select会怎么选?
答:
- 已关闭的 channel 读操作总是 ready,会立即触发对应的
case,读到零值。 - 如果多个分支都有可能选,则已关闭的分支会成为 always ready 的候选。
面试高频问题总结
| 问题 | 关键答点 |
|---|---|
select 用来做什么? | 多路 channel I/O 复用 |
没有 default 会发生什么? | 所有 channel 阻塞时,select 会阻塞当前 goroutine |
| 有多个分支同时满足呢? | runtime 随机选一个执行,保证公平性 |
select 和 epoll 有什么异同? | 都是多路复用,但 select 是编译期 + runtime 支持,面向 channel;epoll 面向文件描述符 |
| 如何实现超时? | time.After + select |
核心面试示例
func worker(stop chan struct{}) {
for {
select {
case <-stop:
fmt.Println("Stopping")
return
default:
fmt.Println("Working")
time.Sleep(500 * time.Millisecond)
}
}
}
一行结论
select= Go 的 channel 多路复用调度器,本质是编译器 + runtime 对 goroutine 的阻塞与唤醒调度。
经典面试延伸
1️⃣ select + context 超时怎么实现?
ctx, cancel := context.WithTimeout(...),然后select { case <-ctx.Done(): }。
2️⃣ 如何用 select 实现 fan-in?
多个生产者写入同一个 channel,
select在消费者处多路接收。
3️⃣ 如何做多路 timeout?
select里case <-time.After(...)可以同时放多个。
总结
必须掌握的 5 句话:
select是 channel 多路复用的原语。- 会阻塞,除非有
default。 - 同时可用时随机选,避免饿死。
- 底层是
sendq/recvq和sudog。 - 核心场景:超时、非阻塞 IO、优雅退出。
select 典型实战示例合集
- 基本读写
- 超时控制
- 优雅退出
- 非阻塞发送
- fan-in(多路合并)
- worker pool + graceful shutdown
可本地执行的 Go 示例👇
✅ 示例 1:基本读写
go
复制编辑
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "hello from ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "hello from ch2"
}()
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
default:
fmt.Println("No channel ready immediately")
}
}
✅ 示例 2:超时控制
go
复制编辑
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
time.Sleep(2 * time.Second)
ch <- 42
}()
select {
case val := <-ch:
fmt.Println("Got:", val)
case <-time.After(1 * time.Second):
fmt.Println("Timeout!")
}
}
✅ 示例 3:非阻塞发送
go
复制编辑
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 1)
select {
case ch <- 10:
fmt.Println("Sent 10")
default:
fmt.Println("Channel full, skip sending")
}
val := <-ch
fmt.Println("Received:", val)
}
✅ 示例 4:优雅退出
go
复制编辑
package main
import (
"fmt"
"time"
)
func worker(stop chan struct{}) {
for {
select {
case <-stop:
fmt.Println("Worker stopping...")
return
default:
fmt.Println("Working...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
stop := make(chan struct{})
go worker(stop)
time.Sleep(2 * time.Second)
close(stop)
time.Sleep(1 * time.Second)
fmt.Println("Main exit.")
}
✅ 示例 5:fan-in(多路合并)
go
复制编辑
package main
import (
"fmt"
"time"
)
func producer(ch chan<- string, msg string, delay time.Duration) {
for i := 0; i < 3; i++ {
time.Sleep(delay)
ch <- fmt.Sprintf("%s %d", msg, i)
}
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go producer(ch1, "P1", 1*time.Second)
go producer(ch2, "P2", 500*time.Millisecond)
for i := 0; i < 6; i++ {
select {
case msg1 := <-ch1:
fmt.Println("From ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("From ch2:", msg2)
}
}
}
✅ 示例 6:Worker Pool + Graceful Shutdown
go
复制编辑
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int, jobs <-chan int, results chan<- int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d exiting...\n", id)
return
case job, ok := <-jobs:
if !ok {
fmt.Printf("Worker %d: jobs closed\n", id)
return
}
fmt.Printf("Worker %d processing job %d\n", id, job)
time.Sleep(500 * time.Millisecond)
results <- job * 2
}
}
}
func main() {
jobs := make(chan int, 10)
results := make(chan int, 10)
ctx, cancel := context.WithCancel(context.Background())
for w := 1; w <= 3; w++ {
go worker(ctx, w, jobs, results)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
time.Sleep(2 * time.Second)
cancel()
for i := 0; i < 5; i++ {
fmt.Println("Result:", <-results)
}
}