for(循环结构)
# 3. for(循环结构)
# 3.1 经典形式
var sum int
for i := 0; i < 10; i++ {
sum += i
}
println(sum)
2
3
4
5
执行流程图:
经典 for 循环语句有四个组成部分(分别对应图中的①~④):
图中①对应的组成部分执行于循环体(③ )之前,并且在整个 for 循环语句中仅会被执行一次,它也被称为循环前置语句。我们通常会在这个部分声明一些循环体(③ )或循环控制条件(② )会用到的自用变量,也称循环变量或迭代变量,比如这里声明的整型变量 i。与 if 语句中的自用变量一样,for 循环变量也采用短变量声明的形式,循环变量的作用域仅限于 for 语句隐式代码块范围内。
图中②对应的组成部分,是用来决定循环是否要继续进行下去的条件判断表达式。和 if 语句的一样,这个用于条件判断的表达式必须为布尔表达式,如果有多个判断条件,我们一样可以由逻辑操作符进行连接。当表达式的求值结果为 true 时,代码将进入循环体(③)继续执行,相反则循环直接结束,循环体(③)与组成部分④都不会被执行。
图中③对应的组成部分是 for 循环语句的循环体。如果相关的判断条件表达式求值结构为 true 时,循环体就会被执行一次,这样的一次执行也被称为一次迭代(Iteration)。在上面例子中,循环体执行的动作是将这次迭代中变量 i 的值累加到变量 sum 中。
图中④对应的组成部分会在每次循环体迭代之后执行,也被称为循环后置语句。这个部分通常用于更新 for 循环语句组成部分①中声明的循环变量,比如在这个例子中,我们在这个组成部分对循环变量 i 进行加 1 操作。
Go 语言的 for 循环支持声明多循环变量,并且可以应用在循环体以及判断条件中。
for i, j, k := 0, 1, 2; (i < 20) && (j < 10) && (k < 30); i, j, k = i+1, j+1, k+5 {
sum += (i + j + k)
println(sum)
}
2
3
4
普通for循环:
package main
import "fmt"
func main() {
// 打印:0~9的数字
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
/*
0
1
2
3
4
5
6
7
8
9
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 3.1.1 语句省略
除了循环体部分(③)之外,其余的三个部分都是可选的。
省略了循环后置语句④,将对循环变量的更新操作放在了循环体中:
for i := 0; i < 10; {
i++
}
2
3
省略循环前置语句①,直接使用了已声明的变量 i 充当循环变量的作用:
i := 0
for ; i < 10; i++{
println(i)
}
2
3
4
省略循环前置①与后置语句④:
i := 0
for ; i < 10; {
println(i)
i++
}
2
3
4
5
虽然对前置语句或后置语句进行了省略,但经典 for 循环形式中的分号依然被保留着,这是 Go 语法的要求。
# 3.2 无前置后置语句形式
当循环前置与后置语句都省略掉,仅保留循环判断条件表达式时,可以省略经典 for 循环形式中的分号:
i := 0
for i < 10 {
println(i)
i++
}
2
3
4
5
package main
import "fmt"
func main() {
// 打印:0~9的数字
i := 0
for i < 10 {
fmt.Println(i)
i++
}
}
/*
0
1
2
3
4
5
6
7
8
9
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 3.3 无限循环(while循环)
当 for 循环语句的循环判断条件表达式的求值结果始终为 true 时,我们就可以将它(true)省略掉了:
for {
// 循环体代码
}
// 这个 for 循环就是我们通常所说的“无限循环”。它的形式等价于:
for true {
// 循环体代码
}
// 或者:
for ; ; {
// 循环体代码
}
2
3
4
5
6
7
8
9
10
11
12
13
Go 语言中是没有 while 语句的,可以通过 for 代替。
package main
import "fmt"
func main() {
k := 1
for { // 这里也等价 for ; ; {
if k <= 10 {
fmt.Println("ok~~", k)
} else {
break //break 就是跳出这个 for 循环
}
k++
}
}
/*
ok~~ 1
ok~~ 2
ok~~ 3
ok~~ 4
ok~~ 5
ok~~ 6
ok~~ 7
ok~~ 8
ok~~ 9
ok~~ 10
*/
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
# 3.4 for range(键值循环)
如果要使用 for 经典形式遍历一个切片中的元素,可以这样做:
var sl = []int{1, 2, 3, 4, 5}
for i := 0; i < len(sl); i++ {
fmt.Printf("sl[%d] = %d\n", i, sl[i])
}
2
3
4
Go 语言提供了一个更方便的“语法糖”形式:for range。等价于上面代码的 for range 循环:
for i, v := range sl {
fmt.Printf("sl[%d] = %d\n", i, v)
}
2
3
这里的 i 和 v 对应的是经典 for 语句形式中循环前置语句的循环变量,它们的初值分别为切片 sl 的第一个元素的下标值和元素值。并且,隐含在 for range 语义中的循环控制条件判断为:是否已经遍历完 sl 的所有元素,等价于i < len(sl)这个布尔表达式。另外,每次迭代后,for range 会取出切片 sl 的下一个元素的下标和值,分别赋值给循环变量 i 和 v,这与 for 经典形式下的循环后置语句执行的逻辑是相同的。
# 3.4.1 for range 变种
**变种一:**当不关心元素的值时,我们可以省略代表元素值的变量 v,只声明代表下标值的变量 i:
for i := range sl {
// ...
}
2
3
**变种二:**如果我们不关心元素下标,只关心元素值,那么我们可以用空标识符(这个空标识符不能省略)替代代表下标值的变量 i。
for _, v := range sl {
// ...
}
2
3
**变种三:**既不关心下标值,也不关心元素值。
for range sl {
// ...
}
// 等价于:
for _, _ = range sl {
// ...
}
2
3
4
5
6
7
8
# 3.4.2 操作 string 类型
var s = "中国人"
for i, v := range s {
fmt.Printf("%d %s 0x%x\n", i, string(v), v)
}
/*
0 中 0x4e2d
3 国 0x56fd
6 人 0x4eba
*/
2
3
4
5
6
7
8
9
10
for range 对于 string 类型来说,每次循环得到的 v 值是一个 Unicode 字符码点,也就是 rune 类型值,而不是一个字节,返回的第一个值 i 为该 Unicode 字符码点的内存编码(UTF-8)的第一个字节在字符串内存序列中的位置。
package main
import "fmt"
func main() {
str := "abc上海"
for index, val := range str {
fmt.Printf("索引=%d,值=%c \n", index, val)
}
}
/*
索引=0, 值=a
索引=1, 值=b
索引=2, 值=c
索引=3, 值=上
索引=6, 值=海
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 3.4.3 操作map类型
map 就是一个键值对(key-value)集合,最常见的对 map 的操作,就是通过 key 获取其对应的 value 值。
对 map 进行循环操作,for range 是唯一的方法,for 经典循环形式是不支持对 map 类型变量的循环控制的。
var m = map[string]int {
"Rob" : 67,
"Russ" : 39,
"John" : 29,
}
for k, v := range m {
println(k, v)
}
/*
John 29
Rob 67
Russ 39
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
每次循环,循环变量 k 和 v 分别会被赋值为 map 键值对集合中一个元素的 key 值和 value 值。而且,map 类型中没有下标的概念,通过 key 和 value 来循环操作 map 类型变量也就十分自然了。
# 3.4.4 操作channel类型
channel 是 Go 语言提供的并发设计的原语,它用于多个 Goroutine 之间的通信。
当 channel 类型变量作为 for range 语句的迭代对象时,for range 会尝试从 channel 中读取数据,使用形式是这样的:
var c = make(chan int)
for v := range c {
// ...
}
2
3
4
for range 每次从 channel 中读取一个元素后,会把它赋值给循环变量 v,并进入循环体。当 channel 中没有数据可读的时候,for range 循环会阻塞在对 channel 的读操作上。直到 channel 关闭时,for range 循环才会结束,这也是 for range 循环与 channel 配合时隐含的循环判断条件。
# 3.5 for 语句的常见“坑”与避坑方法
# 3.5.1 循环变量的使用
for range 形式的循环语句,使用短变量声明的方式来声明循环变量,循环体将使用这些循环变量实现特定的逻辑。
不过可能会发现循环变量的值与之前的“预期”不符,:
func main() {
var m = []int{1, 2, 3, 4, 5}
for i, v := range m {
go func() {
time.Sleep(time.Second * 3)
fmt.Println(i, v)
}()
}
time.Sleep(time.Second * 10)
}
// 这个示例是对一个整型切片进行遍历,并且在每次循环体的迭代中都会创建一个新的 Goroutine(Go 中的轻量级协程),输出这次迭代的元素的下标值与元素值。
// 预期输出结果:
/*
0 1
1 2
2 3
3 4
4 5
*/
// 实际输出结果
/*
4 5
4 5
4 5
4 5
4 5
*/
// Goroutine 中输出的循环变量,也就是 i 和 v 的值都是 for range 循环结束后的最终值,而不是各个 Goroutine 启动时变量 i 和 v 的值,与最初的“预期”不符.
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
这是因为最初的“预期”本身就是错的。这里,初学者很可能会被 for range 语句中的短声明变量形式“迷惑”,简单地认为每次迭代都会重新声明两个新的变量 i 和 v。但事实上,这些循环变量在 for range 语句中仅会被声明一次,且在每次迭代中都会被重用。
将上面的 for range 语句做一个等价转换:
func main() {
var m = []int{1, 2, 3, 4, 5}
{
i, v := 0, 0
for i, v = range m {
go func() {
time.Sleep(time.Second * 3)
fmt.Println(i, v)
}()
}
}
time.Sleep(time.Second * 10)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
通过等价转换后的代码,我们可以清晰地看到循环变量 i 和 v 在每次迭代时的重用。而 Goroutine 执行的闭包函数引用了它的外层包裹函数中的变量 i、v,这样,变量 i、v 在主 Goroutine 和新启动的 Goroutine 之间实现了共享,而 i, v 值在整个循环过程中是重用的,仅有一份。在 for range 循环结束后,i = 4, v = 5,因此各个 Goroutine 在等待 3 秒后进行输出的时候,输出的是 i, v 的最终值。
修改代码,让实际输出和最初的预期输出一致:
// 可以为闭包函数增加参数,并且在创建 Goroutine 时将参数与 i、v 的当时值进行绑定
func main() {
var m = []int{1, 2, 3, 4, 5}
for i, v := range m {
go func(i, v int) {
time.Sleep(time.Second * 3)
fmt.Println(i, v)
}(i, v)
}
time.Sleep(time.Second * 10)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 3.5.2 参与循环的是 range 表达式的副本
在 for range 语句中,range 后面接受的表达式的类型可以是数组、指向数组的指针、切片、字符串,还有 map 和 channel(需具有读权限)。
以数组为例:
func main() {
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int
fmt.Println("original a =", a)
for i, v := range a {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
fmt.Println("after for range loop, r =", r)
fmt.Println("after for range loop, a =", a)
}
// 对一个数组 a 的元素进行遍历操作,当处理下标为 0 的元素时,我们修改了数组 a 的第二个和第三个元素的值,并且在每个迭代中,我们都将从 a 中取得的元素值赋值给新数组 r。
// 预期输出:
/*
original a = [1 2 3 4 5]
after for range loop, r = [1 12 13 4 5]
after for range loop, a = [1 12 13 4 5]
*/
// 实际输出:
/*
original a = [1 2 3 4 5]
after for range loop, r = [1 2 3 4 5]
after for range loop, a = [1 12 13 4 5]
*/
// 原以为在第一次迭代过程,也就是 i = 0 时,我们对 a 的修改 (a[1] =12,a[2] = 13) 会在第二次、第三次迭代中被 v 取出,但从结果来看,v 取出的依旧是 a 被修改前的值:2 和 3。
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
原因就是参与 for range 循环的是 range 表达式的副本。也就是说,在上面这个例子中,真正参与循环的是 a 的副本,而不是真正的 a。
用一个等价的伪代码形式重写一下:
for i, v := range a' { //a'是a的一个值拷贝
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
2
3
4
5
6
7
这个例子中,每次迭代的都是从数组 a 的值拷贝 a’中得到的元素。a’是 Go 临时分配的连续字节序列,与 a 完全不是一块内存区域。因此无论 a 被如何修改,它参与循环的副本 a’依旧保持原值,因此 v 从 a’中取出的仍旧是 a 的原值,而不是修改后的值。
在 Go 中,大多数应用数组的场景我们都可以用切片替代。使用切片来修改代码,让实际输出和最初的预期输出一致:
// 在 range 表达式中,用 a[:]替代原先的 a,也就是将数组 a 转换为一个切片,作为 range 表达式的循环对象。
func main() {
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int
fmt.Println("original a =", a)
for i, v := range a[:] {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
fmt.Println("after for range loop, r =", r)
fmt.Println("after for range loop, a =", a)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
当进行 range 表达式复制时,我们实际上复制的是一个切片,也就是表示切片的结构体。表示切片副本的结构体中的 array,依旧指向原切片对应的底层数组,所以我们对切片副本的修改也都会反映到底层数组 a 上去。而 v 再从切片副本结构体中 array 指向的底层数组中,获取数组元素,也就得到了被修改后的元素值。
# 3.5.3 遍历 map 中元素的随机性
当 map 类型变量作为 range 表达式时,得到的 map 变量的副本与原变量指向同一个 map。如果在循环的过程中,对 map 进行了修改,那么这样修改的结果是否会影响后续迭代呢?这个结果和遍历 map 一样,具有随机性。
// 例1:在 map 循环过程中,当 counter 值为 0 时,删除了变量 m 中的一个元素:
var m = map[string]int{
"tony": 21,
"tom": 22,
"jim": 23,
}
counter := 0
for k, v := range m {
if counter == 0 {
delete(m, "tony")
}
counter++
fmt.Println(k, v)
}
fmt.Println("counter is ", counter)
// 反复运行这个例子多次,会得到两个不同的结果。当 k="tony"作为第一个迭代的元素时,将得到如下结果:
/*
tony 21
tom 22
jim 23
counter is 3
*/
// 之所以还能输出tony,是因为k, v从map中获取值的操作发生在delete之前。如果k="tony"作为第一个迭代的元素时,我们用k,v从map中取出了tony, 21。然后delete掉tony,此时k, v的值已经是tony, 21了,输出就正常了。如果tony不是第一个迭代元素,那么已经被删除掉了,后续迭代就不会输出它了。
// 否则
/*
tom 22
jim 23
counter is 2
*/
// 例2:如果在针对 map 类型的循环体中,新创建了一个 map 元素项,那这项元素可能出现在后续循环中,也可能不出现:
var m = map[string]int{
"tony": 21,
"tom": 22,
"jim": 23,
}
counter := 0
for k, v := range m {
if counter == 0 {
m["lucy"] = 24
}
counter++
fmt.Println(k, v)
}
fmt.Println("counter is ", counter)
// 这个例子的执行结果也会有两个:
/*
tony 21
tom 22
jim 23
lucy 24
counter is 4
*/
// 或者输出:
/*
tony 21
tom 22
jim 23
counter is 3
*/
// 之所以 lucky =24 赋值不成功,是因为map迭代的实质是按顺序逐个bucket的遍历,每个bucket也是逐个遍历其中的key。如果luckey创建在第一个被遍历的元素之前了,那么后续就不会遍历它了。key存储在哪里是根据hash值来定的。
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
考虑到上述这种随机性,在日常编码遇到遍历 map 的同时,还需要对 map 进行修改的场景的时候,要格外小心。