Go select 面试手册

162 阅读4分钟

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.go
  • selectgo 函数
  • sudog 结构体(等待在 channel 上的 goroutine)

select 的公平性

面试常考点:

多个 case 同时可用,Go 按什么顺序选?

答:

  • Go 运行时会伪随机 shuffle case 顺序,然后遍历判断可执行。
  • 保证不会固定执行第一个分支 → 防止饿死。

select 的常见陷阱

陷阱解释
select {}空的 select 会永远阻塞,相当于 <-make(chan struct{})
Scanselect 只能用于 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 随机选一个执行,保证公平性
selectepoll 有什么异同?都是多路复用,但 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?

selectcase <-time.After(...) 可以同时放多个。

总结

必须掌握的 5 句话:

  • select 是 channel 多路复用的原语。
  • 会阻塞,除非有 default
  • 同时可用时随机选,避免饿死。
  • 底层是 sendq / recvqsudog
  • 核心场景:超时、非阻塞 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)
	}
}