以下所有观点都是个人愚见,有不同建议或补充的的欢迎emialaboutme
原文章地址
关于for语句的疑问
for语句的规范
for语句的内部实现-array
问题解答
关于for语句的疑问
我们都知道在golang中,循环语句只有for这一个,在代码中写一个循环都一般都需要用到for(当然你用goto也是可以的), 虽然golang的for语句很方便,但不少初学者一样对for语句持有不少疑问,如:
- for语句一共有多少种表达式格式?
- for语句中临时变量是怎么回事?(为什么有时遍历赋值后,所有的值都等于最后一个元素)
- range后面支持的数据类型有哪些?
- range string类型为何得到的是rune类型?
- 遍历slice的时候增加或删除数据会怎么样?
- 遍历map的时候增加或删除数据会怎么样?
其实这里的很多疑问都可以看golang编程语言规范, 有兴趣的同学完全可以自己看,然后根据自己的理解来解答这些问题。
for语句的规范
for语句的功能用来指定重复执行的语句块,for语句中的表达式有三种:
官方的规范: ForStmt = "for" [ Condition | ForClause | RangeClause ] Block .
- Condition = Expression .
- ForClause = [ InitStmt ] “;” [ Condition ] “;” [ PostStmt ] .
- RangeClause = [ ExpressionList “=” | IdentifierList “:=” ] “range” Expression .
单个条件判断
形式:
for a < b {
f(doThing)
}
// or 省略表达式,等价于true
for { // for true {
f(doThing)
}
这种格式,只有单个逻辑表达式, 逻辑表达式的值为true,则继续执行,否则停止循环。
for语句中两个分号
形式:
for i:=0; i < 10; i++ {
f(doThing)
}
// or
for i:=0; i < 10; {
i++
f(doThing)
}
// or
var i int
for ; i < 10; {
i++
f(doThing)
}
这种格式,语气被两个分号分割为3个表达式,第一个表示为初始化(只会在第一次条件表达式之计算一次
),第二个表达式为条件判断表达式,
第三个表达式一般为自增或自减,但这个表达式可以任何符合语法的表达式。而且这三个表达式,
只有第二个表达式是必须有的,其他表达式可以为空。
for和range结合的语句
形式:
for k,v := range []int{1,2,3} {
f(doThing)
}
// or
for k := range []int{1,2,3} {
f(doThing)
}
// or
for range []int{1,2,3} {
f(doThing)
}
用range来迭代数据是最常用的一种for语句,range右边的表达式叫范围表达式
,
范围表达式可以是数组,数组指针,slice,字符串,map和channel。因为要赋值,
所以左侧的操作数(也就是迭代变量)必须要可寻址的,或者是map下标的表达式。
如果迭代变量是一个channel,那么只允许一个迭代变量,除此之外迭代变量可以有一个或者两个。
范围表达式在开始循环之前只进行一次求值,只有一个例外:如果范围表达式是数组或指向数组的指针, 至多有一个迭代变量存在,只对范围表达式的长度进行求值;如果长度为常数,范围表达式本身将不被求值。
每迭代一次,左边的函数调用求值。对于每个迭代,如果相应的迭代变量存在,则迭代值如下所示生成:
Range expression 1st value 2nd value
array or slice a [n]E, *[n]E, or []E index i int a[i] E
string s string type index i int see below rune
map m map[K]V key k K m[k] V
channel c chan E, <-chan E element e E
- 对于数组、数组指针或是分片值a来说,下标迭代值升序生成,从0开始。有一种特殊场景,只有一个迭代参数存在的情况下, range循环生成0到len(a)的迭代值,而不是索引到数组或是分片。对于一个nil分片,迭代的数量为0。
- 对于字符串类型,range子句迭代字符串中每一个Unicode代码点,从下标0开始。在连续迭代中,下标值会是下一个utf-8代码点的 第一个字节的下标,而第二个值类型是rune,会是对应的代码点。如果迭代遇到了一个非法的Unicode序列,那么第二个值是0xFFFD, 也就是Unicode的替换字符,然后下一次迭代只会前进一个字节。
- map中的迭代顺序是没有指定的,也不保证两次迭代是一样的。如果map元素在迭代过程中被删掉了,那么对应的迭代值不会再产生。 如果map元素在迭代中插入了,则该元素可能在迭代过程中产生,也可能被跳过,但是每个元素的迭代值顶多出现一次。如果map是nil,那么迭代次数为0。
- 对于管道,迭代值就是下一个send到管道中的值,除非管道被关闭了。如果管道是nil,范围表达式永远阻塞。
迭代值会赋值给相应的迭代变量,就像是赋值语句。
迭代变量可以使用短变量声明(:=)。这种情况,它们的类型设置为相应迭代值的类型,它们的域是到for语句的结尾,它们在每一次迭代中复用。
如果迭代变量是在for语句外声明的,那么执行之后它们的值是最后一次迭代的值。
var testdata *struct {
a *[7]int
}
for i, _ := range testdata.a {
// testdata.a is never evaluated; len(testdata.a) is constant
// i ranges from 0 to 6
f(i)
}
var a [10]string
for i, s := range a {
// type of i is int
// type of s is string
// s == a[i]
g(i, s)
}
var key string
var val interface {} // value type of m is assignable to val
m := map[string]int{"mon":0, "tue":1, "wed":2, "thu":3, "fri":4, "sat":5, "sun":6}
for key, val = range m {
h(key, val)
}
// key == last map key encountered in iteration
// val == map[key]
var ch chan Work = producer()
for w := range ch {
doWork(w)
}
// empty a channel
for range ch {}
for语句的内部实现-array
golang的for语句,对于不同的格式会被编译器编译成不同的形式,如果要弄明白需要看 golang的编译器和相关数据结构的源码, 数据结构源码还好,但是编译器是用C++写的,本人C++是个弱鸡,这里只讲array内部实现。
// The loop we generate:
// len_temp := len(range)
// range_temp := range
// for index_temp = 0; index_temp < len_temp; index_temp++ {
// value_temp = range_temp[index_temp]
// index = index_temp
// value = value_temp
// original body
// }
// 例如代码:
array := [2]int{1,2}
for k,v := range array {
f(k,v)
}
// 会被编译成:
len_temp := len(array)
range_temp := array
for index_temp = 0; index_temp < len_temp; index_temp++ {
value_temp = range_temp[index_temp]
k = index_temp
v = value_temp
f(k,v)
}
所以像遍历一个数组,最后生成的代码很像C语言中的遍历,而且有两个临时变量index_temp
,value_temp
,
在整个遍历中一直复用这两个变量。所以会导致开头问题2的问题(详细解答会在后边)。
问题解答
for语句一共有多少种表达式格式?
这个问题应该很简单了,上面的规范中就有答案了,一共有3种:Condition = Expression . ForClause = [ InitStmt ] ";" [ Condition ] ";" [ PostStmt ] . RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .
for语句中临时变量是怎么回事?(为什么有时遍历赋值后,所有的值都等于最后一个元素)
先看这个例子:var a = make([]*int, 3) for k, v := range []int{1, 2, 3} { a[k] = &v } for i := range a { fmt.Println(*a[i]) } // result: // 3 // 3 // 3
由for语句的内部实现-array可以知道,即使是短声明的变量,在for循环中也是复用的,这里的
v
一直 都是同一个零时变量,所以&v
得到的地址一直都是相同的,如果不信,你可以打印该地址,且该地址最后存的变量等于最后一次循环得到的变量, 所以结果都是3。range后面支持的数据类型有哪些?
共5个,分别是数组,数组指针,slice,字符串,map和channelrange string类型为何得到的是rune类型?
这个问题在for规范中也有解答,对于字符串类型,在连续迭代中,下标值会是下一个utf-8代码点的第一个字节的下标,而第二个值类型是rune。 如果迭代遇到了一个非法的Unicode序列,那么第二个值是0xFFFD,也就是Unicode的替换字符,然后下一次迭代只会前进一个字节。其实看完这句话,我没理解,当然这句话告诉我们了遍历string得到的第二个值类型是rune,但是为什么是rune类型,而不是string或者其他类型? 后来在看了Rob Pike写的blogStrings, bytes, runes and characters in Go 才明白点,首先需要知道
rune
是int32
的别名,且go语言中的字符串字面量始终保存有效的UTF-8序列。而UTF-8就是用4字节来表示Unicode字符集。 所以go的设计者用rune表示单个字符的编码,则可以完成容纳所表示Unicode字符。举个例子:s := `汉语ab` fmt.Println("len of s:", len(s)) for index, runeValue := range s { fmt.Printf("%#U starts at byte position %d\n", runeValue, index) } // result // len of s: 8 // U+6C49 '汉' starts at byte position 0 // U+8BED '语' starts at byte position 3 // U+0061 'a' starts at byte position 6 // U+0062 'b' starts at byte position 7
根据结果得知,s的长度是为8字节,一个汉子占用了3个字节,一个英文字母占用一个字节,而程序go程序是怎么知道汉子占3个字节,而 英文字母占用一个字节,就需要知道utf-8代码点的概念,这里就不深究了,知道go是根据utf-8代码点来知道该字符占了多少字节就ok了。
遍历slice的时候增加或删除数据会怎么样?
由for语句的内部实现-array可以知道,获取slice的长度只在循环外执行了一次, 该长度决定了遍历的次数,不管在循环里你怎么改。但是对索引求值是在每次的迭代中求值的,如果更改了某个元素且 该元素还未遍历到,那么最终遍历得到的值是更改后的。删除元素也是属于更改元素的一种情况。在slice中增加元素,会更改slice含有的元素,但不会更改遍历次数。
a2 := []int{0, 1, 2, 3, 4} for i, v := range a2 { fmt.Println(i, v) if i == 0 { a2 = append(a2, 6) } } // result // 0 0 // 1 1 // 2 2 // 3 3 // 4 4
在slice中删除元素,能删除该元素,但不会更改遍历次数。
// 只删除该元素1,不更改slice长度 a2 := []int{0, 1, 2, 3, 4} for i, v := range a2 { fmt.Println(i, v) if i == 0 { copy(a2[1:], a2[2:]) } } // result // 0 0 // 1 2 // 2 3 // 3 4 // 4 4 // 删除该元素1,并更改slice长度 a2 := []int{0, 1, 2, 3, 4} for i, v := range a2 { fmt.Println(i, v) if i == 0 { copy(a2[1:], a2[2:]) a2 = a2[:len(a2)-2] //将a2的len设置为3,但并不会影响临时slice-range_temp } } // result // 0 0 // 1 2 // 2 3 // 3 4 // 4 4
遍历map的时候增加或删除数据会怎么样?
规范中也有答案,map元素在迭代过程中被删掉了,那么对应的迭代值不会再产生。 如果map元素在迭代中插入了,则该元素可能在迭代过程中产生,也可能被跳过。在遍历中删除元素
m := map[int]int{1: 1, 2: 2, 3: 3, 4: 4, 5: 5} del := false for k, v := range m { fmt.Println(k, v) if !del { delete(m, 2) del = true } } // result // 4 4 // 5 5 // 1 1 // 3 3
在遍历中增加元素,多执行几次看结果
m := map[int]int{1: 1, 2: 2, 3: 3, 4: 4, 5: 5} add := false for k, v := range m { fmt.Println(k, v) if !add { m[6] = 6 m[7] = 7 add = true } } // result1 // 1 1 // 2 2 // 3 3 // 4 4 // 5 5 // 6 6 // result2 // 1 1 // 2 2 // 3 3 // 4 4 // 5 5 // 6 6 // 7 7
在map遍历中删除元素,将会删除该元素,且影响遍历次数,在遍历中增加元素则会有不可控的现象出现,有时能遍历到新增的元素, 有时不能。具体原因下次分析。
参考
https://golang.org/ref/spec#For_statements
https://github.com/golang/go/wiki/Range
https://blog.golang.org/strings
有疑问加站长微信联系(非本文作者)