跳到主要内容

select 与 for-range

问题

Go 中 selectfor 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.WaitGroupsignal.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,然后自动退出循环
}

相关链接