引入

一个 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 同时等 ch1ch2,哪个先有数据就先处理哪个。

超时处理

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 设为 nilselect 不再监听它。

随机选择

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 并发模式,学习 WaitGroupMutexcontext 等并发模式。