引入
一个 goroutine 只等一个 channel 还好说,但实际场景中经常要同时监听多个 channel——哪个先到就处理哪个。switch 判断的是值,select 判断的是 channel 操作,是 Go 里处理多路复用的核心语法。
正文
定义
定义
select是 Go 的多路复用语句,同时监听多个 channel 操作(发送或接收),哪个先就绪就执行哪个。如果多个同时就绪,随机选一个。如果全部阻塞,执行default分支(有的话)或阻塞等待。
语法
select {
case v := <-ch1:
fmt.Println("ch1:", v)
case ch2 <- data:
fmt.Println("发送到 ch2")
case v := <-ch3:
fmt.Println("ch3:", v)
default:
fmt.Println("都没有就绪")
}例子
基本用法
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(100 * time.Millisecond)
ch1 <- "来自 ch1"
}()
go func() {
time.Sleep(200 * time.Millisecond)
ch2 <- "来自 ch2"
}()
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
}
}select 同时等 ch1 和 ch2,哪个先有数据就先处理哪个。
超时处理
ch := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch <- "结果"
}()
select {
case result := <-ch:
fmt.Println("收到:", result)
case <-time.After(1 * time.Second):
fmt.Println("超时了")
}
// 输出:超时了time.After(d) 返回一个 channel,d 时间后会发送一个值。配合 select 就能实现超时控制,防止无限等待。
default 非阻塞操作
ch := make(chan int)
select {
case v := <-ch:
fmt.Println("收到:", v)
default:
fmt.Println("channel 没有数据,不等待")
}加了 default 分支后,如果所有 channel 都没就绪,不会阻塞,直接走 default。适合做非阻塞检查。
非阻塞发送
ch := make(chan int) // 无缓冲
select {
case ch <- 1:
fmt.Println("发送成功")
default:
fmt.Println("没人接收,发送失败")
}
// 输出:没人接收,发送失败无缓冲 channel 没人接收时,发送会阻塞。加 default 就能尝试发送,发不出去就走默认分支。
select + for 循环
ch := make(chan int)
go func() {
for i := 1; i <= 3; i++ {
ch <- i
}
close(ch)
}()
for {
select {
case v, ok := <-ch:
if !ok {
fmt.Println("channel 已关闭")
return
}
fmt.Println("收到:", v)
}
}select 套在 for 里可以持续监听,配合逗号 ok 模式检测 channel 是否关闭。
多路合并
func merge(ch1, ch2 <-chan int) <-chan int {
out := make(chan int)
go func() {
for {
select {
case v, ok := <-ch1:
if !ok {
ch1 = nil
continue
}
out <- v
case v, ok := <-ch2:
if !ok {
ch2 = nil
continue
}
out <- v
}
if ch1 == nil && ch2 == nil {
close(out)
return
}
}
}()
return out
}把两个 channel 的数据合并到一个输出 channel,关闭的 channel 设为 nil 让 select 不再监听它。
随机选择
ch := make(chan int, 1)
ch <- 1
select {
case <-ch:
fmt.Println("分支 1")
case ch <- 2:
fmt.Println("分支 2")
}如果多个 case 同时就绪,select 随机选一个执行,不保证顺序。
常见写法
| 写法 | 说明 |
|---|---|
select { case … } | 多路复用,阻塞等待 |
case <-time.After(d) | 超时控制 |
default: | 非阻塞分支 |
for { select { … } } | 持续监听 |
ch = nil | 让 select 不再监听某个 channel |
case v, ok := <-ch | 接收并判断是否关闭 |
特点
select同时监听多个 channel 操作,哪个先就绪执行哪个- 多个 case 同时就绪时随机选择
- 没有
default且全部阻塞时会永久阻塞 - 有
default时所有 channel 阻塞会走default,不会阻塞 time.After()配合select实现超时控制- 把 channel 设为
nil可以让select忽略该分支 select只能处理 channel 操作,不能判断普通变量
理解
select 就像一个多路开关,同时盯着好几个 channel,谁先到就处理谁。加上 default 就变成非阻塞模式,没人理我就不等了。配合 time.After() 还能做超时保护,防止程序卡死。在 Go 并发编程里,select 几乎无处不在。
引出
select 解决了多 channel 的协调问题,但实际并发场景还需要更高级的工具来管理 goroutine 的生命周期和共享资源。接下来看 Go 并发模式,学习 WaitGroup、Mutex、context 等并发模式。