golang 中你应该知道的 range 知识

理解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
2
3
4
arr := [5]int{10, 20, 30, 40, 50}
for i, v := range arr {
fmt.Printf("Index: %d, Value: %d\n", i, v)
}

注意:数组的长度是固定的,在遍历过程中不会改变。

遍历切片

slice可以看作是对数组的引用。for range 在切片上的应用和数组类似,但切片的长度可以动态变化。

1
2
3
4
5
slice := []int{100, 200, 300, 400, 500}
for i, v := range slice {
fmt.Printf("Index: %d, Value: %d\n", i, v)
}

注意:切片可以动态扩展或缩减,因此在遍历过程中如果切片的内容发生变化,可能会影响循环结果。
关于slice的更多注意事项,可以参考文章:

Slice Notions You Should Know in Golang | by Wesley Wei | Jul, 2024 | Programmer’s Career

遍历map

map是键值对的集合,for range 可以遍历映射中的每个键值对。

1
2
3
4
m := map[string]int{"a": 1, "b": 2, "c": 3}
for key, value := range m {
fmt.Printf("Key: %s, Value: %d\n", key, value)
}

注意

  • 遍历顺序:映射的遍历顺序是随机的,不保证每次运行的顺序相同。

遍历字符串

字符串在 Go 中是不可变的字节序列。for range 可以用于遍历字符串中的每个字符,支持 Unicode 字符。

1
2
3
4
str := "Hello, 世界"
for i, r := range str {
fmt.Printf("Index: %d, Rune: %c\n", i, r)
}

注意

  • for range 在遍历字符串时,处理的是 Unicode 字符(rune),而不是字节,这对于处理多字节字符(如中文)非常重要。

遍历channel

通道是 Go 中的一个并发原语,用于在 goroutine 之间传递数据。for range 可以用于在通道关闭前遍历其接收到的值。

1
2
3
4
5
6
7
8
9
10
11
ch := make(chan int, 5)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}()

for v := range ch {
fmt.Println(v)
}

注意事项

  • 通道必须在生产者结束发送数据时关闭,否则 for range 循环会陷入阻塞。
  • for range 会在通道关闭后自动退出循环。

通过 for range 遍历这些数据结构,Go 可以方便地处理不同类型的数据。上文提到了一些注意事项,接下来我们重点看一下相关内容。

1.3 for range 使用中的常见错误

让我们看两个经典的、不符合直觉、新手很容易出错的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// pointer example
type user struct {
name string
age uint64
}

func main() {
u := []user{
{"wesley", 20},
{"wei", 30},
{"wesleywei", 40},
}
n := make([]*user, 0, len(u))
for _, v := range u {
n = append(n, &v)
}
fmt.Println(n)
for _, v := range n {
fmt.Println(v)
}
}

// Closure example
func main() {
var funcs []func()

for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Println(i)
})
}

for _, f := range funcs {
f()
}
}

在Go 版本小于1.22,例子的输出结果分别是:

1
2
3
4
5
6
7
8
9
10
// Pointer example output
[0xc00000c048 0xc00000c048 0xc00000c048]
&{wesleywei 40}
&{wesleywei 40}
&{wesleywei 40}

// Closure example output:
3
3
3

这是为什么呢?

对于所有的 range 循环,Go 语言都会在编译期将原切片或者数组赋值给一个新变量 ha,在赋值的过程中就发生了拷贝。而其中的每个元素都会赋值给一个临时变量。大致逻辑如下:

1
2
3
4
5
6
7
8
9
10
ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
v2 := nil
for ; hv1 < hn; hv1++ {
tmp := ha[hv1]
v1, v2 = hv1, tmp
...
}

而当Go版本大于等于1.22,这个容易出错的问题已经被”修复“,输出结果如下:

1
2
3
4
5
6
7
8
9
10
// pointer example output
[0xc000010030 0xc000010048 0xc000010060]
&{wesley 20}
&{wei 30}
&{wesleywei 40}

// Closure example output:
0
1
2

具体请参考 Fixing For Loops in Go 1.22 - The Go Programming Language。对应的相关逻辑应该在 GitHub 附近。

1.4 range的特殊用法

for range 循环遍历 map 时,是可以安全地删除 map 中的元素的。这种特性在 Go 中得益于 rangemap的实现。在许多其他编程语言中,类似的操作可能会导致迭代器无效、抛出异常或引发未定义行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
d := map[string]string{
"wesley": "wei",
"medium": "blog",
}
for k := range d {
if k == "wesley" {
delete(d, k)
}
}
fmt.Println(d)
}

// output
map[medium:blog]
  1. 在 Python 中,如果你在遍历字典(类似于 Go 的 map)时尝试删除元素,通常会抛出一个 RuntimeError,因为 Python 的迭代器机制不允许在遍历时修改字典。
  2. 在 Java 中,使用 Iterator 遍历 HashMap 时,可以通过 Iteratorremove() 方法删除当前遍历的元素。但如果直接调用 Mapremove() 方法而不使用 Iterator,将会抛出 ConcurrentModificationException

这两种语言要实现golang range map中delete的逻辑,稍显麻烦:

1
2
3
4
5
my_dict = {'a': 1, 'b': 2, 'c': 3}
for key in list(my_dict.keys()):
if some_condition(key):
del my_dict[key]

1
2
3
4
5
6
7
8
Map<String, Integer> map = new HashMap<>();
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
if (someCondition(entry)) {
iterator.remove();
}
}

既然range 中可以delete元素,那么add也可以使用,结合map 哈希表的随机性,下面的例子会输出什么结果呢?会不会无限循环下去呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
var addTomap = func() {
var t = map[string]string{
"wesley": "handsome",
"wesley1": "handsome1",
"wesley2": "handsome2",
}
for k := range t {
t["wesley3"] = "handsome3"
fmt.Printf("%s-%s ", k, t[k])
}
}
for i := 0; i < 10; i++ {
addTomap()
fmt.Println()
}
}

参考

  1. redefining for loop variable semantics · golang/go · Discussion #56010 · GitHub
  2. 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/. 本文为作者原创,转载请注明出处。

Golang 高效处理集合 (Collection) 的库 收集信息,建立个人灵感图书馆

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×