首页 / Go 语言入门教程 / select 语句

Go 语言入门教程

select 语句

本教程共 80 篇 · 第 58 篇 · 更新于 2026-07-27 · 约 7 分钟阅读

GoGo 入门教程select多路复用channel超时并发

58. select 语句

本节目标:学会用 select 同时监听多个 channel,掌握随机选择、default 非阻塞和超时模式。

多路复用

当你有多个 channel 要同时盯,用 select。它的语法和 switch 很像,但 case 只能是 channel 操作:

select {
case v := <-ch1:
	fmt.Println("从 ch1 收到:", v)
case v := <-ch2:
	fmt.Println("从 ch2 收到:", v)
}

select 会阻塞,直到某一个 case 可以执行。如果多个 case 同时就绪,会随机挑一个执行,不是按顺序。

随机选择的意义

为什么是随机而不是从上到下?为了避免某个 channel 总是饥饿。如果按顺序,第一个就绪的 channel 永远优先,后面的可能一直轮不到。随机选择让每个 channel 都有机会。

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)

	go func() {
		ch1 <- "A"
	}()
	go func() {
		ch2 <- "B"
	}()

	for i := 0; i < 2; i++ {
		select {
		case v := <-ch1:
			fmt.Println(v)
		case v := <-ch2:
			fmt.Println(v)
		}
	}
}

每次运行,先打印 A 还是 B 不一定。

配合 for 循环

单次 select 只执行一个 case。要持续监听,套一层 for

for {
	select {
	case v := <-ch1:
		fmt.Println("ch1:", v)
	case v := <-ch2:
		fmt.Println("ch2:", v)
	case <-time.After(time.Second):
		fmt.Println("超时退出")
		return
	}
}

这是 Go 里最常见的消息循环写法。

default 非阻塞

没有 default 时,所有 case 都不就绪就阻塞。加上 default,不就绪就走默认分支,不阻塞:

select {
case v := <-ch:
	fmt.Println("收到:", v)
default:
	fmt.Println("没数据,干别的去")
}

利用这点可以实现非阻塞的发送和接收:

// 非阻塞发送
func trySend(ch chan<- int, v int) bool {
	select {
	case ch <- v:
		return true
	default:
		return false
	}
}

// 非阻塞接收
func tryRecv(ch <-chan int) (int, bool) {
	select {
	case v := <-ch:
		return v, true
	default:
		return 0, false
	}
}
Tip

检查 context 是否已取消也可以非阻塞:

select {
case <-ctx.Done():
	// 已取消
default:
	// 未取消
}

超时模式

time.After 返回一个只读 channel,指定时间后会往里发一个值。配合 select 实现超时:

func main() {
	ch := make(chan int)

	go func() {
		time.Sleep(2 * time.Second)
		ch <- 42
	}()

	select {
	case v := <-ch:
		fmt.Println("收到:", v)
	case <-time.After(time.Second):
		fmt.Println("超时了,不等了")
	}
}

这个模式很常用,比如 HTTP 请求超时控制、任务限时执行。

Warning

time.After 每次调用都会创建一个新的 timer。如果在高频循环里用,可能导致 timer 堆积。长时间运行的服务建议用 time.NewTimer 手动管理,或者用 context.WithTimeout

nil channel 会被跳过

selectcase 如果操作的是 nil channel,不会阻塞,而是直接被跳过:

var nilCh chan int // nil

select {
case <-nilCh:
	fmt.Println("这行永远不会执行")
case <-time.After(time.Second):
	fmt.Println("timeout")
}

利用这个特性,可以在运行时动态启用或禁用某个 case:把 channel 设成 nil 就禁用,赋个真 channel 就启用。

永久阻塞

空的 select{} 会永久阻塞,因为没有 case 也没有 default

func main() {
	fmt.Println("start")
	select {}
	fmt.Println("这行永远执行不到")
}

偶尔有用,比如让主 goroutine 卡住等信号。

小结

  • select 同时监听多个 channel,谁先就绪执行谁
  • 多个就绪时随机选一个,避免饥饿
  • defaultselect 非阻塞
  • time.After 配合 select 实现超时
  • nil channel 的 case 会被跳过
  • select{} 永久阻塞

下一节讲 channel 的关闭和 range 遍历。