select 与 for-range
问题
Go 中 select 和 for range 有哪些特性和常见陷阱?
答案
select 多路复用
select 用于同时监听多个 channel 操作,类似于 switch 但专用于 channel:
select {
case msg := <-ch1:
fmt.Println("received from ch1:", msg)
case msg := <-ch2:
fmt.Println("received from ch2:", msg)
case ch3 <- "hello":
fmt.Println("sent to ch3")
case <-time.After(3 * time.Second):
fmt.Println("timeout")
default:
fmt.Println("no channel ready") // 非阻塞
}
select 规则:
| 规则 | 说明 |
|---|---|
| 多个 case 就绪 | 随机选择一个执行(公平性) |
| 没有 case 就绪 | 阻塞等待(除非有 default) |
| 有 default | 没有 case 就绪时执行 default(非阻塞) |
空 select select{} | 永久阻塞(常用于 main 中等待) |
常见模式:
// 1. 超时控制
select {
case result := <-ch:
return result, nil
case <-time.After(5 * time.Second):
return nil, errors.New("timeout")
}
// 2. 非阻塞读取
select {
case msg := <-ch:
process(msg)
default:
// channel 没数据,继续做其他事
}
// 3. 退出信号
for {
select {
case <-done:
return // 收到退出信号
case msg := <-ch:
process(msg)
}
}
for range 遍历
for range 可以遍历数组、切片、map、字符串、channel:
// 遍历切片
for i, v := range []int{10, 20, 30} {
fmt.Println(i, v) // 0 10, 1 20, 2 30
}
// 只要索引
for i := range slice { }
// 遍历 map(顺序随机)
for k, v := range m { }
// 遍历字符串(按 rune)
for i, r := range "Hello你" {
fmt.Printf("%d: %c\n", i, r)
}
// 0: H, 1: e, ..., 5: 你(索引是字节偏移)
// 遍历 channel(直到 channel 关闭)
for msg := range ch {
process(msg)
}
for range 常见陷阱
陷阱 1:循环变量是副本
type User struct{ Name string }
users := []User{{"Alice"}, {"Bob"}}
// ❌ v 是副本,修改无效
for _, v := range users {
v.Name = "Modified" // 修改的是副本
}
// users 不变:[{Alice} {Bob}]
// ✅ 用索引修改
for i := range users {
users[i].Name = "Modified"
}
陷阱 2:循环中取地址(Go 1.22 前)
// ❌ Go 1.21 及之前:所有指针指向同一个变量
var ptrs []*int
for _, v := range []int{1, 2, 3} {
ptrs = append(ptrs, &v) // 都指向同一个 v
}
// ptrs 全部指向 3
// ✅ Go 1.22+:每次迭代创建新变量,此问题已修复
// ✅ Go 1.21 及之前的修复方式:
for _, v := range []int{1, 2, 3} {
v := v // 新变量
ptrs = append(ptrs, &v)
}
陷阱 3:range 表达式在循环开始前求值
s := []int{1, 2, 3}
for i, v := range s { // s 在这里被复制了 slice header
if i == 0 {
s = append(s, 4) // 修改 s 不影响遍历(header 已复制)
}
fmt.Println(v) // 1, 2, 3(不会输出 4)
}
break 和 label
// break 在 select 和 for 嵌套时的歧义
for {
select {
case <-done:
break // ⚠️ 只 break 了 select,不是 for!
case msg := <-ch:
process(msg)
}
}
// ✅ 使用 label break 跳出外层循环
Loop:
for {
select {
case <-done:
break Loop // 跳出 for 循环
case msg := <-ch:
process(msg)
}
}
Go 1.22 的 range 整数
// Go 1.22+:range 可以直接遍历整数
for i := range 5 {
fmt.Println(i) // 0, 1, 2, 3, 4
}
// 等价于
for i := 0; i < 5; i++ {
fmt.Println(i)
}
常见面试问题
Q1: 空 select 有什么用?
答案:
select{} 永久阻塞当前 goroutine,常用于 main 函数中等待其他 goroutine 完成工作(当不需要明确的退出信号时):
func main() {
go serve()
select{} // 永久阻塞,让 serve 一直运行
}
但更推荐使用 sync.WaitGroup 或 signal.Notify 来优雅处理退出。
Q2: for range 遍历 map 时可以删除元素吗?
答案:
可以,Go 官方明确允许在 for range 遍历 map 期间删除元素。但新插入的元素可能被遍历到也可能不被遍历到,行为不确定。
Q3: for range channel 什么时候结束?
答案:
当 channel 被关闭且缓冲区中的数据都被读完时,for range 自动结束。如果 channel 没有关闭,for range 会一直阻塞等待。
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)
for v := range ch {
fmt.Println(v) // 1, 2, 3,然后自动退出循环
}