七叶笔记 » golang编程 » golang 如何学习for语句

golang 如何学习for语句

关于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 省略表达式,等价于truefor { // for true {
 f(doThing)
} 

这种格式,只有单个逻辑表达式, 逻辑表达式的值为true,则继续执行,否则停止循环。

for语句中两个分号

形式:

for i:=0; i < 10; i++ {
 f(doThing)
}// orfor i:=0; i < 10; {
 i++
 f(doThing)
}// or var i intfor ; 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)
}// orfor 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 runemap 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]stringfor i, s := range a {// type of i is int
// type of s is string
// s == a[i]
g(i, s)
}var key stringvar val interface {} // value type of m is assignable to valm := 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 channelfor 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 := arrayfor 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 才明白点,首先需要知道 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了。

  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 := falsefor 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 := falsefor 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遍历中删除元素,将会删除该元素,且影响遍历次数,在遍历中增加元素则会有不可控的现象出现,有时能遍历到新增的元素, 有时不能。具体原因下次分析。

相关文章