for计数器迭代

1
for 初始化语句; 条件语句; 修饰语句 {}
  1. 由三部分组成循环的头部,相互之间使用英文分号(;)隔开,但并不需要括号将它们括起来。
  2. 区别其他语言形式如【for (初始化语句; 条件语句; 修饰语句) {} 】其实使用括号包起来也可以。

同时使用多个计数器

  1. 这得益于Go语言具有平行赋值的特性。
  2. 区别总结:
    • for关键字后面不需要括号。
    • 初始化语句修饰语句可以使用平行赋值的特性。
1
2
3
4
// 注意这里的 【初始化语句】 和 【修饰语句】
// 初始化语句:i, j := 0, N
// 修饰语句:i, j = i+1, j-1
for i, j := 0, N; i < j; i, j = i+1, j-1 {}

for{}

  1. 可以认为这是没有【初始化语句】和【修饰语句】的for结构,因此;;便是多余的了。
  2. 即使是条件语句也可以省略,如【i: = 0; ;i++】或【for {} 或 for ;; {}】多余的;;会在使用时移除,这些循环的本质就是无限循环。
  3. 也可以写成【for true {}】一般都是直接写成【for {}】。
  4. 如果for循环的头部没有条件语句,默认为trueswitch没有表达式类似默认为true
    • 一般Go处理类似情况的常用法则,因此循环体内必须有相关的条件判断以确保会在某个时刻退出循环。
  5. 区别总结:
    • for {}】我们可以理解为【for 条件语句 {}】这种形式,省略了初始化语句和修饰语句,与其他语言【while (true) {}】用法类似。
1
for {}  // 等价于 for true {}

使用示例

 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
package main

import (
    "fmt"
)

func main()  {
    // 创建[]int切片
    a := []int{1,2,3,4,5,6}
    // 交换切片首尾数据
    for i,j := 0,len(a)-1; i < j; i,j = i+1,j-1 {
        a[i], a[j] = a[j], a[i]
    }

    fmt.Println(a)  // [6 5 4 3 2 1]

    // 多重循环满足条件退出    
    for j := 0; j < 5; j++ {
        for i := 0; i < 10; i++ {
            if i > 5 {
                break
            }
            fmt.Printf("%d ", i)
        }
        fmt.Println()
    }
    
    // Output:
    // [6 5 4 3 2 1]
    // 0 1 2 3 4 5
    // 0 1 2 3 4 5
    // 0 1 2 3 4 5
    // 0 1 2 3 4 5
    // 0 1 2 3 4 5
}
 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
s := "abc"

// 1) 常见的 for 循环,支持初始化语句
for i,n := 0,len(s); i < n; i++ {
    fmt.Printf("%c\n", s[i])
}

// Output:
// a
// b
// c

n := len(s)
// 2) 替代 while (n > 0) {}
for n > 0 {
    fmt.Println(s[n-1])
    n--
}

// Output:
// 99
// 98
// 97

// 3) 替换 while (true) {} 或 for (;;) {}
for {
    fmt.Println(s)
}

// Output:
// abc
// ...
 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package main

import "fmt"

func main() {
    var b int = 15
    var a int

    // [1, 2, 3, 5, 0, 0]
    numbers := [6]int{1, 2, 3, 5}

    // 1) for 循环
    for a := 0; a < 10; a++ {
        fmt.Printf("a 的值为:%d\n", a)
    }
    // 2) for true
    for a < b {
        a++
        fmt.Printf("a 的值为:%d\n", a)
    }
    // 3) for range
    for i, x := range numbers {
        fmt.Printf("第 %d 位 x 的值 = %d\n", i, x)
    }

    // Output:
    // a 的值为:0
    // a 的值为:1
    // a 的值为:2
    // a 的值为:3
    // a 的值为:4
    // a 的值为:5
    // a 的值为:6
    // a 的值为:7
    // a 的值为:8
    // a 的值为:9
    // a 的值为:1
    // a 的值为:2
    // a 的值为:3
    // a 的值为:4
    // a 的值为:5
    // a 的值为:6
    // a 的值为:7
    // a 的值为:8
    // a 的值为:9
    // a 的值为:10
    // a 的值为:11
    // a 的值为:12
    // a 的值为:13
    // a 的值为:14
    // a 的值为:15
    // 第 0 位 x 的值 = 1
    // 第 1 位 x 的值 = 2
    // 第 2 位 x 的值 = 3
    // 第 3 位 x 的值 = 5
    // 第 4 位 x 的值 = 0
    // 第 5 位 x 的值 = 0
}

判断一个数是否是质数

 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
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package main

import (
    "fmt"
    "math"
)

func main()  {
    for i := uint64(2) ; i < 100; i++ {
        if isPrime(i) {
            fmt.Printf("%d 是素数\n", i)
        }
    }
}

// isPrime 判断num是否是素数(质数) 素数只能被1和本身整除
func isPrime(num uint64) bool {
    // 假设A是条件,B是结论
    //      由A可以推出B,由B可以推出A,则A是B的 (充要条件)
    //      由A可以推出B,由B不可以推出A,则A是B的 (充分不必要条件)
    //      由A不可以推出B,由B可以推出A,则A是B的 (必要不充分条件)
    //      由A不可以推出B,由B不可以推出A,则A是B的 (既不充分也不必要条件)
    
    // 大于等于5的质数一定和6的倍数相邻 (充分不必要条件)
    //      与6的倍数相邻的数不一定是大于等于5的质数
    // 证明:(n >= 1, n属于自然数)
    // 6n + 0	=> 2*3*n	=> 合数
    // 6n + 1   -----> 可能是素数(7),也可能是合数(25)
    // 6n + 2	=> 2*(3n+1)	=> 合数
    // 6n + 3	=> 3*(2n+1)	=> 合数
    // 6n + 4	=> 2*(3n+2)	=> 合数
    // 6n + 5   -----> 可能是素数(11),也可能是合数(35)
    // 上面的列表能表示所有>=5的自然数,因此大于等于5的质数一定和6的倍数相邻
    
    // 5以下的质数分别为 2和3    
    if num == 2 || num == 3 {
        return true
    }

    // 大于等于5的质数一定和6的倍数相邻,相反不在6的倍数两侧的一定是合数
    // 这里排除了所有被2和3整除的合数,因此后面的for循环只需验证是否能被其他质数整除即可
    if num % 6 != 1 && num % 6 != 5 {
        return false
    }

    // 一个数能进行因式分解,那么分解时得到的两个数
    // 一定是一个小于等于 sqrt(n) 和 一个大于等于 sqrt(n)
    // 故遍历循环的次数就是sqrt(n)向上取整次数
    tmp := uint64(math.Ceil(math.Sqrt(float64(num))))

    // 与6的倍数相邻的数不一定是大于等于5的质数(质数)
    var i uint64 = 5    // 这里i表示第一个素数5
    // 这里的i += 6包含所有当前num因式分解的所有公因式,也就是所有的素数
    for ; i <= tmp; i += 6 { 
        // i 和 i + 2 是6的倍数前后两个数字
        // 判断num是否能因式分解,能进行因式分解则是合数,否则是素数
        if num % i == 0 || num % (i + 2) == 0 {
            return false
        }
    }

    return true
}

/* Output:
2 是素数
3 是素数
5 是素数
7 是素数
11 是素数
13 是素数
17 是素数
19 是素数
23 是素数
29 是素数
31 是素数
37 是素数
41 是素数
43 是素数
47 是素数
53 是素数
59 是素数
61 是素数
67 是素数
71 是素数
73 是素数
79 是素数
83 是素数
89 是素数
97 是素数
 */

for-range

  1. for - range】结构是Go语言特有的一种迭代结构,它在许多情况下都非常有用。
  2. 可以迭代任何一个集合,也包括数组(array)和字典(map)和字符串(string)和通道(channel)和切片(slice),同时可以获得每次迭代所对应的索引和值。
1
2
// ix:索引 val:值
for ix, val := range coll {}
  1. 如果只需要range里的索引值,可以只写key省略value
1
for key := range coll {}
  1. val值始终为集合中对应索引的副本,因此它一般只具有只读性质。
    • 对它所有的任何修改都不会影响到集合中原有的值。
    • 如果val为指针,则会产生指针的副本,依旧可以修改集合中的原值。
    • range遍历的也是副本。
  2. for循环的range格式可以对slicemaparraystringchan等进行迭代循环。
    • Golangrange类似迭代器操作,返回【(索引, 值) 】或【(键, 值)】。
类型 key value 描述
string indexint 类型 s[index]rune 类型 字符串
array/slice index int 类型 s[index] 是存储的元素类型 数组/切片
map key m[key] map存储类型 字典,遍历顺序是随机
channel elementchan存储类型 通道
  1. 可以忽略不想要的返回值,或使用_这个特殊变量。注意_是内置已经声明的,因此不能使用这种形式_:=1,不能使用:=应该使用=

使用示例

 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
38
39
40
41
42
43
44
45
46
package main

import "fmt"

func main() {
    s := "abc"  // range 变量字符串是按照Unicode遍历的

    // 1) 忽略第二个参数, 支持 string/array/slice/map
    for i := range s { // 【int, rune】
        fmt.Printf("%d %c\n", i, s[i])
    }
    
    // Output:
    // 0 a
    // 1 b
    // 2 c

    // 2) 忽略 第一个参数
    for _, c := range s { // 【int, rune】
        fmt.Println(c)
    }
    
    // Output:
    // 97
    // 98
    // 99

    // 3) 忽略全部返回值,仅迭代
    n := 0
    // s为字符串时,统计rune字符数量
    for range s { // 【int, rune】
        n++
    }
    fmt.Println(n)

    m := map[string]int{"a":1, "b":2}
    // 返回 (key, value)
    // 遍历map,顺序是随机的
    for k, v := range m { // 【string, int】
        fmt.Println(k, v)
    }
    
    // Output:
    // b 2
    // a 1
}

for-range会拷贝遍历对象

  1. 注意下面的代码range复制了a对象数据所以输出结果和预期的不同。
 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
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    // 数组布局 内存中连续分配
    // 地址 0 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	字节B
    //     |<---a[0]---->| |<-------a[1]------>|  |<--------a[2]------>|
    a := [3]int{0, 1, 2}
    
    // 查看a的内存占用大小 int 在64位系统下占8字节 3*8 = 24字节
    fmt.Println(unsafe.Sizeof(a))   // 24

    // index、value 都是从复制品中取出
    for i, v := range a { // 【int, int】
        // 在修改前,我们先修改原数组
        if i == 0 {
            a[1], a[2] = 999, 999
            // 确认修改有效,输出[0, 999, 999]
            fmt.Println(a)  // [0 999 999]
        }

        // 使用复制品中取出的 value 修改原数组
        a[i] = v + 100
    }

    // 注意这里的输出
    fmt.Println(a)  // [100 101 102]
    
    // Output:
    // 24
    // [0 999 999]
    // [100 101 102]
}

for-range遍历切片

  1. 改用引用类型,其底层数据不会被复制,注意下面代码。
  2. 另外两种引用类型mapchannel是指针包装,而不像slicestruct
 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
package main

import "fmt"

func main() {
    // 布局 type slice struct { pointer, len, cap }
    // slice           pointer             len             cap
    //                    |                 5               5
    //                    v
    // array              0 1 2 3 4 5 ...
    // 假设以下切片的结构如下struct {0x01f00, 5, 5}
    s := []int{1, 2, 3, 4, 5}

    // range遍历切片,复制结构体struct {0x01f00, 5, 5}用于遍历
    // 遍历时指向pointer指针移动获取数据
    for i, v := range s { // 【int, int】
        if i == 0 {
            // 修改s的切片结构为struct {0x01f00, 3, 5},而复制的副本不变struct {0x01f00, 5, 5}
            s = s[:3]   // [low:high:max]   len=high-low、cap=max-low
            // 修改[0x01f00+2*8,0x01f00+3*8)地址位置从3修改为100
            s[2] = 100
        }

        fmt.Println(i, v)
    }

    fmt.Println(s)
    
    // Output:
    // 0 1
    // 1 2
    // 2 100
    // 3 4
    // 4 5
    // [1 2 100]
}

for-range遍历字符串

  1. 字符串是Unicode编码的字符集合使用for-range结构迭代字符串。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
    "fmt"
)

func main()  {
    for pos, char := range "语言\x80雨" { // 【int, rune】
        fmt.Printf("%d character %#U starts at byte position %d\n", char, char, pos)
    }
    
    // Output:
    // 35821 character U+8BED '语' starts at byte position 0
    // 35328 character U+8A00 '言' starts at byte position 3
    // 65533 character U+FFFD '�' starts at byte position 6
    // 38632 character U+96E8 '雨' starts at byte position 7
}

注意

  1. 遍历切片:下面程序上有没有可优化的空间?
1
2
3
4
5
func rangeTest(slice []int) {
  for index, value := range slice {
    _, _ = index, value
  }
}
  • 解析:使用 range 遍历,每次迭代会对 index,value 进行赋值,若数据很大或 value 类型为 string 时,对 value 的赋值操作可以进行优化,即忽略 value 值,使用 slice[index] 来获取 value 的值。
  • 解析:使用 range 遍历,每次迭代会对 index,value 进行赋值,若数据很大或 value 类型为 string 时,对 value 的赋值操作可以进行优化,即忽略 value 值,使用 slice[index] 来获取 value 的值。
1
2
3
4
5
func rangeTest(slice []int) {
  for index, _ := range slice {
    _, _ = index, slice[index]
  }
}
  1. 动态遍历:下面程序上能否正常结束?
1
2
3
4
5
6
7
8
9
func main() {
    v := []int{1,2,3}
    // 我们知道range遍历的是v的副本,也就是 v1 := v 遍历的是v1 所以下面只会循环3次
    for i := range v {
        v = append(v, i)
    }
    fmt.Println(v) // [1 2 3 0 1 2]
    // 最后变量完 v = []int{1,2,3,0,1,2}
}
  • 解析:会正常结束。循环内再改变切片的长度,不影响循环次数,循环次数在循环开始前就已经是确定了的。
  1. 遍历Map:下面程序上有没有可优化的空间?
1
2
3
4
5
func rangeTest(mapTest map[int]string) {
  for key, _ := range mapTest {
    _, _ = key, mapTest[key]
  }
}
  • 解析:使用 range 遍历,根据第一题经验,我们根据 key 值来获取value 的值,看似减少了一次赋值,但使用 mapTest[key] 来获取 value 值的性能消耗可能高于赋值消耗。能否优化取决于 map 所存储数据结构特征,应结合实际情况进行。
  • 我们知道mapTest[key]的取值是一个非常复杂的函数调用,mapTest[key]的使用反而会增加负担。

参考

  1. range 实现原理