if语句、for语句和switch语句都属于Go语言的基本流程控制语句。
下面主要讨论for语句和switch语句,不过不讲语法规则,而是一些要注意的细节,就是“坑”。

带range子句的for语句

下面会分别讲解迭代变量和range表达式的问题。

迭代变量

对于不同种类的range表达式结果值,for语句的迭代变量的数量可以不同,下面是一个迭代变量的例子:

func main() {
	nums := []string{"A", "B", "C", "D", "E", "F"}
	for i := range nums {
		fmt.Println(i)
	}
}

当只有一个迭代变量的时候,数组、数组的指针、切片和字符串的元素值都是无处安放的,只能拿到按照从小到大顺序给出的一个个索引值。
如果是两个迭代变量,就可以去到元素值了:

for i, v := range nums {
    fmt.Println(i, v)
}

如果只要取元素只,可以用占位符:

for _, v := range nums {
    fmt.Println(v)
}

迭代数组和切片

这里主要讲range表达式的特性,下面是例子:

package main

import "fmt"

func main() {
	nums := [...]int{1, 2, 3, 4, 5, 6}
	for i, e := range nums {
		if i == len(nums) -1 {
			nums[0] += e
			fmt.Println(e, nums[0])
		} else {
			nums[i+1] += e
			fmt.Println(e, nums[i+1])
		}
		
	}
	fmt.Println(nums)
}
/* 执行结果
[7 3 5 7 9 11]
*/

首先要注意,这里要迭代的是一个数组(不是切片了)。
上面的代码是在每次迭代的时候,都会去修改下一个元素的值,把下一个元素的值加上当前位置的元素值。问题是在下次迭代的时候,取到的值不是之前上次迭代后修改的值,而是数组原始的那个值。结论概括如下:

  • range表达式只会在for语句开始执行时被求值一次,无论后边会有多少次迭代
  • range表达式的求值结果会被复制,就是说,被迭代的对象是range表达式结果值的副本而不是原值

我的理解是,for i, e := range nums这句,拿去给range表达式处理的是nums的副本。
接下来把上面操作的数组改成切片,就是改成这样:nums := []int{1, 2, 3, 4, 5, 6}。之前的数组是值类型,而现在切片是引用类型。再次运行后的结果就不一样了。结果应该是:[22 3 6 10 15 21]。因为是引用类型,range表达式传递还是副本,不过这次是地址的副本,所以每次迭代对下一个元素的修改会在之后的迭代中取出上次修改后的值。

switch语句

在switch语句中,有switch表达式和case表达式。

判等操作

下面这段代码会有编译错误:

package main

import "fmt"

func main() {
	v := [...]int8{1, 2, 3, 4, 5, 6}
	switch 4 {
	case v[0], v[1]:
		fmt.Println("1 or 2")
	case v[2], v[3]:
		fmt.Println("3 or 4")
	case v[4], v[5]:
		fmt.Println("5 or 6")
	}
}

switch表达式需要做判等操作,switch表达式的结果类型,以及各个case表达式的结果类型是要求一样的。因为,在Go语言中,只有类型相同的值之间才被允许进行判等操作。
上面的例子中,数组中元素的值是int8类型,而switch表达式的结果是数字常量,如果switch表达式的结果值是无类型的常量,则会被自动断转换成此种常量的默认类型值。这里的常量是整数4,会转成int类型。然后两个类型是不同的,所以无法通过编译。

交换switch表达式和case表达式的类型
下面这段代码,和上面的例子差不多,只是交换了switch和case里的类型:

func main() {
	v := [...]int8{1, 2, 3, 4, 5, 6}
	switch v[4] {
	case 1, 2:
		fmt.Println("1 or 2")
	case 3, 4:
		fmt.Println("3 or 4")
	case 5, 6:
		fmt.Println("5 or 6")
	}
}

这次switch表达式是int8类型,而case表达式是常量。如果case表达式的结果值是无类型的常量,则会被自动的转换为switch表达式的结果类型。所以这里case表达式的结果会转为int8类型,可以进行判断操作。

小结
如果switch表达式的结果值是无类型的常量,那这个常量会被自动的转换为此种常量的默认类型的值。
如果case表达式的结果值是无类型的常量,那么这个常量会被自动的转换为switch表达式的结果类型。

最后还要注意,如果表达式的结果类型是接口类型,那么要小心检查他们的动态值是否允许判等操作。因为这个错误不会被编译器发现,也就是不会造成编译错误。但是这样的后果更加严重,会在运行时引发Panic。

case表达式的约束

switch语句在case子句的选择上是具有唯一性的,也就是switch语句不允许case表达式中的子表达式结果值存在相等的情况,不了这些值是否是在不同的case表达式中。下面的代码就是因为有重复的值存在产生了编译错误:

func main() {
	v := [...]int{1, 2, 3, 4, 5, 6}
	switch v[4] {
	case 1, 2 ,3:
		fmt.Println("1 or 2")
	case 3, 4, 7-1:
		fmt.Println("3 or 4")
	case 5, 6:
		fmt.Println("5 or 6")
	}
}

绕过约束
不过上面的这种约束只能在常量里发现。也就是说有办法可以绕过这个限制:

package main

import "fmt"

func main() {
	v := [...]int{1, 2, 3, 4, 5, 6}
	switch v[4] {
	case v[0], v[1], v[2]:
		fmt.Println("1 or 2")
	case v[2], v[3], v[4]:
		fmt.Println("3 or 4")
	case v[3], v[4], v[5]:
		fmt.Println("5 or 6")
	}
}

上面用case表达式的常量换成了一个索引表达式,就绕过了编译的检查,并且程序也能正确的执行并返回。

类型判断的switch语句
上面这种绕过约束的方式,对这种switch语句是无效的。因为,类型switch语句中的case表达式的子表达式,都必须直接由类型字面量表示,而无法通过间接的方式表示。下面是编程会出错的例子:

func main() {
	v := interface{}(byte(127))
	switch t := v.(type) {
	case uint8, uint16:
		fmt.Println("uint8 or uint16")
	case byte:
		fmt.Println("byte")
	default:
		fmt.Printf("%T\n", t)
	}
}

这里要知道byte类型是uint8类型的别名类型。这两个本质上是同一个类型,只是名称不同。这种情况是无法通过编译的,因为子表达式byte和uint8算是重复了。

类型判断

switch还可以用来做类型判断,这里直接给出例子:

package main

import "fmt"

func classifier(items ...interface{}) {
    for i, v := range items {
        switch v.(type) {
        case bool:
            fmt.Println("bool", i)
        case float64:
            fmt.Println("float64", i)
        case int:
            fmt.Println("int", i)
        case nil:
            fmt.Println("nil", i)
        case string:
            fmt.Println("string", i)
        default:
            fmt.Println("unknow", i)
        }
    }
}

func main() {
    classifier(1, "", nil, 1.234, true, int32(5))
}