理解Go的Range 循环
Wisdom, compassion, and courage are the three universally recognized moral qualities of men.
— Confucius
1.1 for range 概述及使用
介绍 for range循环的基本语法和使用场景。
for range
是 Go 语言中一个非常常用的循环结构,用于遍历各种集合类型的数据结构,例如数组、slice、map、字符串以及channel。for range
循环可以简洁地迭代这些数据结构,并且能在迭代过程中同时获得索引和值。
基本语法
1 | for index, value := range collection {} |
- collection: 代表你要遍历的数据结构,比如数组、切片、映射、字符串或通道(only value)。
- index: 代表当前元素的索引。对于映射,索引就是键 (key)。
- value: 代表当前索引位置的值。
如果你不需要索引,可以使用下划线 (_
) 来忽略它:
1 | for _, value := range collection {} |
1.2 for range 使用场景
- 遍历数组和slice: 用于按顺序访问每一个元素。
- 遍历map: 用于按键值对的形式访问映射中的元素。
- 遍历字符串: 用于按字符(包括 Unicode 字符)访问字符串的每个字节或字符。
- **遍历channel: 用于从通道接收数据,直到通道关闭。
遍历数组
数组是固定大小的序列,for range
可以用来遍历数组的每个元素。
1 | arr := [5]int{10, 20, 30, 40, 50} |
注意:数组的长度是固定的,在遍历过程中不会改变。
遍历切片
slice可以看作是对数组的引用。for range
在切片上的应用和数组类似,但切片的长度可以动态变化。
1 | slice := []int{100, 200, 300, 400, 500} |
注意:切片可以动态扩展或缩减,因此在遍历过程中如果切片的内容发生变化,可能会影响循环结果。
关于slice的更多注意事项,可以参考文章:
Slice Notions You Should Know in Golang | by Wesley Wei | Jul, 2024 | Programmer’s Career
遍历map
map是键值对的集合,for range
可以遍历映射中的每个键值对。
1 | m := map[string]int{"a": 1, "b": 2, "c": 3} |
注意:
- 遍历顺序:映射的遍历顺序是随机的,不保证每次运行的顺序相同。
遍历字符串
字符串在 Go 中是不可变的字节序列。for range
可以用于遍历字符串中的每个字符,支持 Unicode 字符。
1 | str := "Hello, 世界" |
注意:
for range
在遍历字符串时,处理的是 Unicode 字符(rune),而不是字节,这对于处理多字节字符(如中文)非常重要。
遍历channel
通道是 Go 中的一个并发原语,用于在 goroutine 之间传递数据。for range
可以用于在通道关闭前遍历其接收到的值。
1 | ch := make(chan int, 5) |
注意事项:
- 通道必须在生产者结束发送数据时关闭,否则
for range
循环会陷入阻塞。 for range
会在通道关闭后自动退出循环。
通过 for range
遍历这些数据结构,Go 可以方便地处理不同类型的数据。上文提到了一些注意事项,接下来我们重点看一下相关内容。
1.3 for range 使用中的常见错误
让我们看两个经典的、不符合直觉、新手很容易出错的例子。
1 | // pointer example |
在Go 版本小于1.22,例子的输出结果分别是:
1 | // Pointer example output |
这是为什么呢?
对于所有的 range 循环,Go 语言都会在编译期将原切片或者数组赋值给一个新变量 ha
,在赋值的过程中就发生了拷贝。而其中的每个元素都会赋值给一个临时变量。大致逻辑如下:
1 | ha := a |
而当Go版本大于等于1.22,这个容易出错的问题已经被”修复“,输出结果如下:
1 | // pointer example output |
具体请参考 Fixing For Loops in Go 1.22 - The Go Programming Language。对应的相关逻辑应该在 GitHub 附近。
1.4 range
的特殊用法
for range
循环遍历 map
时,是可以安全地删除 map
中的元素的。这种特性在 Go 中得益于 range
和 map
的实现。在许多其他编程语言中,类似的操作可能会导致迭代器无效、抛出异常或引发未定义行为。
1 | func main() { |
- 在 Python 中,如果你在遍历字典(类似于 Go 的
map
)时尝试删除元素,通常会抛出一个RuntimeError
,因为 Python 的迭代器机制不允许在遍历时修改字典。 - 在 Java 中,使用
Iterator
遍历HashMap
时,可以通过Iterator
的remove()
方法删除当前遍历的元素。但如果直接调用Map
的remove()
方法而不使用Iterator
,将会抛出ConcurrentModificationException
。
这两种语言要实现golang range map中delete的逻辑,稍显麻烦:
1 | my_dict = {'a': 1, 'b': 2, 'c': 3} |
1 | Map<String, Integer> map = new HashMap<>(); |
既然range 中可以delete元素,那么add也可以使用,结合map 哈希表的随机性,下面的例子会输出什么结果呢?会不会无限循环下去呢?
1 | func main() { |
参考
- redefining for loop variable semantics · golang/go · Discussion #56010 · GitHub
- Go Range Loop Internals
更多该系列文章,参考medium链接:
https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a
English post: https://programmerscareer.com/golang-range/
作者:Wesley Wei – Twitter Wesley Wei – Medium
注意:原文在 2024-07-21 18:32 时创作于 https://programmerscareer.com/golang-range/. 本文为作者原创,转载请注明出处。
评论