select语句是Go语言中用于同时监听多个通道操作的控制结构。它类似于switch,但每个case必须是一个通道的发送或接收操作。select会阻塞,直到某个case的通道操作可以执行为止;如果有多个同时就绪,则随机选择一个执行。它是实现多路复用、超时控制、非阻塞通信的核心工具。
每个case必须是一个通道操作(发送或接收),default子句可选。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "来自ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "来自ch2"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
}
输出顺序取决于哪个通道先准备好(一般是ch1先输出)。
如果没有任何case就绪且没有default,select会阻塞。当多个case同时就绪时,Go会伪随机地选择一个执行,避免饥饿。
ch := make(chan int, 1)
ch <- 1
select {
case v := <-ch:
fmt.Println("接收:", v)
case ch <- 2:
fmt.Println("发送成功")
default:
fmt.Println("都不就绪")
}
// 可能输出"接收: 1"或"发送成功",取决于随机选择
结合time.After通道,可以轻松为通道操作设置超时。
func main() {
ch := make(chan int)
go func() {
time.Sleep(3 * time.Second)
ch <- 42
}()
select {
case v := <-ch:
fmt.Println("收到:", v)
case <-time.After(2 * time.Second):
fmt.Println("超时,未收到数据")
}
}
// 输出: 超时,未收到数据
通过添加default分支,select不再阻塞,可以用于尝试收发数据而不等待。
ch := make(chan int)
select {
case v := <-ch:
fmt.Println("接收到:", v)
default:
fmt.Println("通道为空,不阻塞")
}
select {
case ch <- 100:
fmt.Println("发送成功")
default:
fmt.Println("通道已满(或不存在接收者),不阻塞")
}
最常用的并发模式之一,用于持续监听通道事件,直到满足退出条件。
func main() {
messages := make(chan string)
quit := make(chan bool)
go func() {
for i := 0; i < 3; i++ {
messages <- fmt.Sprintf("消息%d", i)
}
quit <- true
}()
for {
select {
case msg := <-messages:
fmt.Println("处理:", msg)
case <-quit:
fmt.Println("收到退出信号,结束循环")
return
}
}
}
这种模式可用于worker池、事件循环、任务队列等。
select {}没有任何case,会永久阻塞当前goroutine。常用于主goroutine需要永远运行而不退出的场景。
func main() {
go func() {
// 后台工作
for {
time.Sleep(1 * time.Second)
fmt.Println("后台运行中...")
}
}()
select {} // 主goroutine阻塞,程序不退出
}
从已关闭的通道接收会立即返回零值,通常通过第二个返回值判断。在select中,如果某个通道被关闭,对应的case会持续立即就绪,可能导致忙循环。解决方案是显式检查或重新设计。
func main() {
ch := make(chan int)
close(ch)
for {
select {
case v, ok := <-ch:
if !ok {
fmt.Println("通道已关闭,退出")
return
}
fmt.Println(v)
}
}
}
解决方案:合理使用default、确保有对应的并发goroutine、在适当时候return或break。
对一个nil通道执行发送或接收会永久阻塞,因此可以通过将通道置为nil来临时禁用某个case。
func main() {
ch := make(chan int)
go func() {
time.Sleep(2 * time.Second)
ch <- 100
}()
var timeoutCh <-chan time.Time = nil // 初始禁用超时
for {
select {
case v := <-ch:
fmt.Println("收到:", v)
return
case <-timeoutCh:
fmt.Println("超时退出")
return
default:
fmt.Println("等待中...")
time.Sleep(500 * time.Millisecond)
if timeoutCh == nil {
timeoutCh = time.After(3 * time.Second) // 3秒后启用超时
}
}
}
}
package main
import (
"fmt"
"time"
)
func worker(heartbeat chan bool, work chan int) {
for {
select {
case task := <-work:
fmt.Printf("处理任务: %d\n", task)
time.Sleep(1 * time.Second) // 模拟处理
heartbeat <- true
case <-time.After(2 * time.Second):
fmt.Println("worker心跳超时,疑似死亡")
return
}
}
}
func main() {
heartbeat := make(chan bool)
work := make(chan int)
go worker(heartbeat, work)
// 发送任务
work <- 1
work <- 2
// 监控心跳
for i := 0; i < 2; i++ {
select {
case <-heartbeat:
fmt.Println("收到心跳")
case <-time.After(3 * time.Second):
fmt.Println("心跳检测超时")
}
}
close(work)
}
select是Go并发编程中不可或缺的工具。它使得单个goroutine能够高效地等待多个通道事件,轻松实现超时控制、非阻塞操作、心跳检测等。掌握select与channel、goroutine的组合,可以构造出复杂且稳健的并发系统。下一章将介绍同步原语(sync.Mutex等),用于保护共享数据。