golang for语句完全指南

sheepbao.github.io · · 23013 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

以下所有观点都是个人愚见,有不同建议或补充的的欢迎emialaboutme
原文章地址

关于for语句的疑问
for语句的规范
for语句的内部实现-array
问题解答

关于for语句的疑问

我们都知道在golang中,循环语句只有for这一个,在代码中写一个循环都一般都需要用到for(当然你用goto也是可以的), 虽然golang的for语句很方便,但不少初学者一样对for语句持有不少疑问,如:

  1. for语句一共有多少种表达式格式?
  2. for语句中临时变量是怎么回事?(为什么有时遍历赋值后,所有的值都等于最后一个元素)
  3. range后面支持的数据类型有哪些?
  4. range string类型为何得到的是rune类型?
  5. 遍历slice的时候增加或删除数据会怎么样?
  6. 遍历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
  1. 对于数组、数组指针或是分片值a来说,下标迭代值升序生成,从0开始。有一种特殊场景,只有一个迭代参数存在的情况下, range循环生成0到len(a)的迭代值,而不是索引到数组或是分片。对于一个nil分片,迭代的数量为0。
  2. 对于字符串类型,range子句迭代字符串中每一个Unicode代码点,从下标0开始。在连续迭代中,下标值会是下一个utf-8代码点的 第一个字节的下标,而第二个值类型是rune,会是对应的代码点。如果迭代遇到了一个非法的Unicode序列,那么第二个值是0xFFFD, 也就是Unicode的替换字符,然后下一次迭代只会前进一个字节。
  3. map中的迭代顺序是没有指定的,也不保证两次迭代是一样的。如果map元素在迭代过程中被删掉了,那么对应的迭代值不会再产生。 如果map元素在迭代中插入了,则该元素可能在迭代过程中产生,也可能被跳过,但是每个元素的迭代值顶多出现一次。如果map是nil,那么迭代次数为0。
  4. 对于管道,迭代值就是下一个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的问题(详细解答会在后边)。

问题解答

  1. for语句一共有多少种表达式格式?
    这个问题应该很简单了,上面的规范中就有答案了,一共有3种:

    Condition = Expression .
    ForClause = [ InitStmt ] ";" [ Condition ] ";" [ PostStmt ] .
    RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .
    
  2. 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。

  3. range后面支持的数据类型有哪些?
    共5个,分别是数组,数组指针,slice,字符串,map和channel

  4. range string类型为何得到的是rune类型?
    这个问题在for规范中也有解答,对于字符串类型,在连续迭代中,下标值会是下一个utf-8代码点的第一个字节的下标,而第二个值类型是rune。 如果迭代遇到了一个非法的Unicode序列,那么第二个值是0xFFFD,也就是Unicode的替换字符,然后下一次迭代只会前进一个字节。

    其实看完这句话,我没理解,当然这句话告诉我们了遍历string得到的第二个值类型是rune,但是为什么是rune类型,而不是string或者其他类型? 后来在看了Rob Pike写的blogStrings, bytes, runes and characters in Go 才明白点,首先需要知道runeint32的别名,且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了。

  5. 遍历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
    
  6. 遍历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


有疑问加站长微信联系(非本文作者)

本文来自:sheepbao.github.io

感谢作者:sheepbao.github.io

查看原文:golang for语句完全指南

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

23013 次点击  
加入收藏 微博
2 回复  |  直到 2018-01-17 12:02:22
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传