Go 语言范围(Range)
range 在各种类型上的行为、值拷贝、以及修改元素的正确姿势。
Key Points`for _, v := range s` 里的 `v` 是**副本**,改它不会改到切片元素。
各类型上的行为
| Type | First | Second |
|---|---|---|
| 数组 / 切片 | 索引 int | 元素副本 |
| 字符串 | 字节偏移 int | rune |
| map | 键 | 值副本 |
| channel | 元素值 | 无 |
| 整数(1.22+) | 从 0 开始 | 无 |
| 函数(1.23+) | 每次调用返回值 | 无 |
Go
// 整数 range(Go 1.22+)
for i := range 3 {
fmt.Print(i, " ")
}
fmt.Println()Output
0 1 2 值拷贝的坑
Go
type Item struct{ N int }
items := []Item{{1}, {2}, {3}}
// 错:改的是副本
for _, it := range items {
it.N *= 10
}
fmt.Println(items)
// 对:按下标改
for i := range items {
items[i].N *= 10
}
fmt.Println(items)Output
[{1} {2} {3}]
[{10} {20} {30}]If elements are pointers ([]*Item), the copy still points at the same object, so it.N = ... works. Relying on that distinction hurts readability — prefer indexing.
map 的顺序
Go
m := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4}
for k, v := range m {
fmt.Print(k, v, " ")
}
fmt.Println()
// 需要固定顺序就先排序键
keys := slices.Sorted(maps.Keys(m))
for _, k := range keys {
fmt.Print(k, m[k], " ")
}
fmt.Println()Output
c 3 a 1 d 4 b 2
a 1 b 2 c 3 d 4 Go deliberately randomises map iteration order to stop programs depending on it. "Passes locally, fails intermittently in production" plus map iteration is almost always this.
遍历时修改
Go
m := map[string]int{"a": 1, "b": 2, "c": 3}
// 遍历中删除:安全
for k := range m {
if k == "b" {
delete(m, k)
}
}
fmt.Println(len(m))
// 遍历中新增:新元素可能被遍历到,也可能不被
// 行为未定义,不要依赖Output
2提前退出
Go
found := false
for i, v := range []int{3, 1, 4, 1, 5} {
if v == 4 {
fmt.Println("在下标", i)
found = true
break
}
}
fmt.Println(found)Output
在下标 2
trueGo's range has no return false early-exit like forEach. Use slices.Index to find and slices.DeleteFunc to filter — clearer than hand-written loops.
各类型上的行为
| 类型 | 第一个值 | 第二个值 |
|---|---|---|
| 数组 / 切片 | 索引 int | 元素副本 |
| 字符串 | 字节偏移 int | rune |
| map | 键 | 值副本 |
| channel | 元素值 | 无 |
| 整数(1.22+) | 从 0 开始 | 无 |
| 函数(1.23+) | 每次调用返回值 | 无 |
Go
// 整数 range(Go 1.22+)
for i := range 3 {
fmt.Print(i, " ")
}
fmt.Println()Output
0 1 2 值拷贝的坑
Go
type Item struct{ N int }
items := []Item{{1}, {2}, {3}}
// 错:改的是副本
for _, it := range items {
it.N *= 10
}
fmt.Println(items)
// 对:按下标改
for i := range items {
items[i].N *= 10
}
fmt.Println(items)Output
[{1} {2} {3}]
[{10} {20} {30}]如果元素是指针([]*Item),for _, it := range items 拿到的副本仍指向同一个对象,改 it.N 是有效的。但依赖这个差别写代码容易误读,建议统一用下标。
map 的顺序
Go
m := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4}
for k, v := range m {
fmt.Print(k, v, " ")
}
fmt.Println()
// 需要固定顺序就先排序键
keys := slices.Sorted(maps.Keys(m))
for _, k := range keys {
fmt.Print(k, m[k], " ")
}
fmt.Println()Output
c 3 a 1 d 4 b 2
a 1 b 2 c 3 d 4 Go 故意让 map 遍历顺序随机,就是为了防止程序依赖顺序。看到「本地测试通过、线上偶发失败」且涉及 map 遍历,基本就是这个原因。
遍历时修改
Go
m := map[string]int{"a": 1, "b": 2, "c": 3}
// 遍历中删除:安全
for k := range m {
if k == "b" {
delete(m, k)
}
}
fmt.Println(len(m))
// 遍历中新增:新元素可能被遍历到,也可能不被
// 行为未定义,不要依赖Output
2提前退出
Go
found := false
for i, v := range []int{3, 1, 4, 1, 5} {
if v == 4 {
fmt.Println("在下标", i)
found = true
break
}
}
fmt.Println(found)Output
在下标 2
trueGo 的 range 没有 forEach 那样的 return false 中断机制。要找元素用 slices.Index,要过滤用 slices.DeleteFunc,都比手写循环清楚。