learn-tech/专栏/Go语言核心36讲/18if语句、for语句和switch语句.md
2024-10-16 00:01:16 +08:00

249 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
18 if语句、for语句和switch语句
在上两篇文章中我主要为你讲解了与go语句、goroutine和Go语言调度器有关的知识和技法。
内容很多,你不用急于完全消化,可以在编程实践过程中逐步理解和感悟,争取夯实它们。
现在让我们暂时走下神坛回归民间。我今天要讲的if语句、for语句和switch语句都属于Go语言的基本流程控制语句。它们的语法看起来很朴素但实际上也会有一些使用技巧和注意事项。我在本篇文章中会以一系列面试题为线索为你讲述它们的用法。
那么今天的问题是使用携带range子句的for语句时需要注意哪些细节 这是一个比较笼统的问题。我还是通过编程题来讲解吧。
本问题中的代码都被放在了命令源码文件demo41.go的main函数中的。为了专注问题本身本篇文章中展示的编程题会省略掉一部分代码包声明语句、代码包导入语句和main函数本身的声明部分。
numbers1 := []int{1, 2, 3, 4, 5, 6}
for i := range numbers1 {
if i == 3 {
numbers1[i] |= i
}
}
fmt.Println(numbers1)
我先声明了一个元素类型为int的切片类型的变量numbers1在该切片中有6个元素值分别是从1到6的整数。我用一条携带range子句的for语句去迭代numbers1变量中的所有元素值。
在这条for语句中只有一个迭代变量i。我在每次迭代时都会先去判断i的值是否等于3如果结果为true那么就让numbers1的第i个元素值与i本身做按位或的操作再把操作结果作为numbers1的新的第i个元素值。最后我会打印出numbers1的值。
所以具体的问题就是,这段代码执行后会打印出什么内容?
这里的典型回答是:打印的内容会是[1 2 3 7 5 6]。
问题解析
你心算得到的答案是这样吗?让我们一起来复现一下这个计算过程。
当for语句被执行的时候在range关键字右边的numbers1会先被求值。
这个位置上的代码被称为range表达式。range表达式的结果值可以是数组、数组的指针、切片、字符串、字典或者允许接收操作的通道中的某一个并且结果值只能有一个。
对于不同种类的range表达式结果值for语句的迭代变量的数量可以有所不同。
就拿我们这里的numbers1来说它是一个切片那么迭代变量就可以有两个右边的迭代变量代表当次迭代对应的某一个元素值而左边的迭代变量则代表该元素值在切片中的索引值。
那么如果像本题代码中的for语句那样只有一个迭代变量的情况意味着什么呢这意味着该迭代变量只会代表当次迭代对应的元素值的索引值。
更宽泛地讲,当只有一个迭代变量的时候,数组、数组的指针、切片和字符串的元素值都是无处安放的,我们只能拿到按照从小到大顺序给出的一个个索引值。
因此这里的迭代变量i的值会依次是从0到5的整数。当i的值等于3的时候与之对应的是切片中的第4个元素值4。对4和3进行按位或操作得到的结果是7。这就是答案中的第4个整数是7的原因了。
现在,我稍稍修改一下上面的代码。我们再来估算一下打印内容。
numbers2 := [...]int{1, 2, 3, 4, 5, 6}
maxIndex2 := len(numbers2) - 1
for i, e := range numbers2 {
if i == maxIndex2 {
numbers2[0] += e
} else {
numbers2[i+1] += e
}
}
fmt.Println(numbers2)
注意我把迭代的对象换成了numbers2。numbers2中的元素值同样是从1到6的6个整数并且元素类型同样是int但它是一个数组而不是一个切片。
在for语句中我总是会对紧挨在当次迭代对应的元素后边的那个元素进行重新赋值新的值会是这两个元素的值之和。当迭代到最后一个元素时我会把此range表达式结果值中的第一个元素值替换为它的原值与最后一个元素值的和最后我会打印出numbers2的值。
对于这段代码,我的问题依旧是:打印的内容会是什么?你可以先思考一下。
好了,我要公布答案了。打印的内容会是[7 3 5 7 9 11]。我先来重现一下计算过程。当for语句被执行的时候在range关键字右边的numbers2会先被求值。
这里需要注意两点:
range表达式只会在for语句开始执行时被求值一次无论后边会有多少次迭代
range表达式的求值结果会被复制也就是说被迭代的对象是range表达式结果值的副本而不是原值。
基于这两个规则我们接着往下看。在第一次迭代时我改变的是numbers2的第二个元素的值新值为3也就是1和2之和。
但是被迭代的对象的第二个元素却没有任何改变毕竟它与numbers2已经是毫不相关的两个数组了。因此在第二次迭代时我会把numbers2的第三个元素的值修改为5即被迭代对象的第二个元素值2和第三个元素值3的和。
以此类推之后的numbers2的元素值依次会是7、9和11。当迭代到最后一个元素时我会把numbers2的第一个元素的值修改为1和6之和。
好了现在该你操刀了。你需要把numbers2的值由一个数组改成一个切片其中的元素值都不要变。为了避免混淆你还要把这个切片值赋给变量numbers3并且把后边代码中所有的numbers2都改为numbers3。
问题是不变的,执行这段修改版的代码后打印的内容会是什么呢?如果你实在估算不出来,可以先实际执行一下,然后再尝试解释看到的答案。提示一下,切片与数组是不同的,前者是引用类型的,而后者是值类型的。
我们可以先接着讨论后边的内容,但是我强烈建议你一定要回来,再看看我留给你的这个问题,认真地思考和计算一下。
知识扩展
问题1switch语句中的switch表达式和case表达式之间有着怎样的联系
先来看一段代码。
value1 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch 1 + 3 {
case value1[0], value1[1]:
fmt.Println("0 or 1")
case value1[2], value1[3]:
fmt.Println("2 or 3")
case value1[4], value1[5], value1[6]:
fmt.Println("4 or 5 or 6")
}
我先声明了一个数组类型的变量value1该变量的元素类型是int8。在后边的switch语句中被夹在switch关键字和左花括号{之间的是1 + 3这个位置上的代码被称为switch表达式。这个switch语句还包含了三个case子句而每个case子句又各包含了一个case表达式和一条打印语句。
所谓的case表达式一般由case关键字和一个表达式列表组成表达式列表中的多个表达式之间需要有英文逗号,分割比如上面代码中的case value1[0], value1[1]就是一个case表达式其中的两个子表达式都是由索引表达式表示的。
另外的两个case表达式分别是case value1[2], value1[3]和case value1[4], value1[5], value1[6]。
此外在这里的每个case子句中的那些打印语句会分别打印出不同的内容这些内容用于表示case子句被选中的原因比如打印内容0 or 1表示当前case子句被选中是因为switch表达式的结果值等于0或1中的某一个。另外两条打印语句会分别打印出2 or 3和4 or 5 or 6。
现在问题来了拥有这样三个case表达式的switch语句可以成功通过编译吗如果不可以原因是什么如果可以那么该switch语句被执行后会打印出什么内容。
我刚才说过只要switch表达式的结果值与某个case表达式中的任意一个子表达式的结果值相等该case表达式所属的case子句就会被选中。
并且一旦某个case子句被选中其中的附带在case表达式后边的那些语句就会被执行。与此同时其他的所有case子句都会被忽略。
当然了如果被选中的case子句附带的语句列表中包含了fallthrough语句那么紧挨在它下边的那个case子句附带的语句也会被执行。
正因为存在上述判断相等的操作以下简称判等操作switch语句对switch表达式的结果类型以及各个case表达式中子表达式的结果类型都是有要求的。毕竟在Go语言中只有类型相同的值之间才有可能被允许进行判等操作。
如果switch表达式的结果值是无类型的常量比如1 + 3的求值结果就是无类型的常量4那么这个常量会被自动地转换为此种常量的默认类型的值比如整数4的默认类型是int又比如浮点数3.14的默认类型是float64。
因此由于上述代码中的switch表达式的结果类型是int而那些case表达式中子表达式的结果类型却是int8它们的类型并不相同所以这条switch语句是无法通过编译的。
再来看一段很类似的代码:
value2 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch value2[4] {
case 0, 1:
fmt.Println("0 or 1")
case 2, 3:
fmt.Println("2 or 3")
case 4, 5, 6:
fmt.Println("4 or 5 or 6")
}
其中的变量value2与value1的值是完全相同的。但不同的是我把switch表达式换成了value2[4]并把下边那三个case表达式分别换为了case 0, 1、case 2, 3和case 4, 5, 6。
如此一来switch表达式的结果值是int8类型的而那些case表达式中子表达式的结果值却是无类型的常量了。这与之前的情况恰恰相反。那么这样的switch语句可以通过编译吗
答案是肯定的。因为如果case表达式中子表达式的结果值是无类型的常量那么它的类型会被自动地转换为switch表达式的结果类型又由于上述那几个整数都可以被转换为int8类型的值所以对这些表达式的结果值进行判等操作是没有问题的。
当然了如果这里说的自动转换没能成功那么switch语句照样通不过编译。
switch语句中的自动类型转换
通过上面这两道题你应该可以搞清楚switch表达式和case表达式之间的联系了。由于需要进行判等操作所以前者和后者中的子表达式的结果类型需要相同。
switch语句会进行有限的类型转换但肯定不能保证这种转换可以统一它们的类型。还要注意如果这些表达式的结果类型有某个接口类型那么一定要小心检查它们的动态值是否都具有可比性或者说是否允许判等操作
因为如果答案是否定的虽然不会造成编译错误但是后果会更加严重引发panic也就是运行时恐慌
问题2switch语句对它的case表达式有哪些约束
我在上一个问题的阐述中还重点表达了一点不知你注意到了没有那就是switch语句在case子句的选择上是具有唯一性的。
正因为如此switch语句不允许case表达式中的子表达式结果值存在相等的情况不论这些结果值相等的子表达式是否存在于不同的case表达式中都会是这样的结果。具体请看这段代码
value3 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch value3[4] {
case 0, 1, 2:
fmt.Println("0 or 1 or 2")
case 2, 3, 4:
fmt.Println("2 or 3 or 4")
case 4, 5, 6:
fmt.Println("4 or 5 or 6")
}
变量value3的值同value1依然是由从0到6的7个整数组成的数组元素类型是int8。switch表达式是value3[4]三个case表达式分别是case 0, 1, 2、case 2, 3, 4和case 4, 5, 6。
由于在这三个case表达式中存在结果值相等的子表达式所以这个switch语句无法通过编译。不过好在这个约束本身还有个约束那就是只针对结果值为常量的子表达式。
比如子表达式1+1和2不能同时出现1+3和4也不能同时出现。有了这个约束的约束我们就可以想办法绕过这个对子表达式的限制了。再看一段代码
value5 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch value5[4] {
case value5[0], value5[1], value5[2]:
fmt.Println("0 or 1 or 2")
case value5[2], value5[3], value5[4]:
fmt.Println("2 or 3 or 4")
case value5[4], value5[5], value5[6]:
fmt.Println("4 or 5 or 6")
}
变量名换成了value5但这不是重点。重点是我把case表达式中的常量都换成了诸如value5[0]这样的索引表达式。
虽然第一个case表达式和第二个case表达式都包含了value5[2]并且第二个case表达式和第三个case表达式都包含了value5[4]但这已经不是问题了。这条switch语句可以成功通过编译。
不过这种绕过方式对用于类型判断的switch语句以下简称为类型switch语句就无效了。因为类型switch语句中的case表达式的子表达式都必须直接由类型字面量表示而无法通过间接的方式表示。代码如下
value6 := interface{}(byte(127))
switch t := value6.(type) {
case uint8, uint16:
fmt.Println("uint8 or uint16")
case byte:
fmt.Printf("byte")
default:
fmt.Printf("unsupported type: %T", t)
}
变量value6的值是空接口类型的。该值包装了一个byte类型的值127。我在后面使用类型switch语句来判断value6的实际类型并打印相应的内容。
这里有两个普通的case子句还有一个default case子句。前者的case表达式分别是case uint8, uint16和case byte。你还记得吗byte类型是uint8类型的别名类型。
因此它们两个本质上是同一个类型只是类型名称不同罢了。在这种情况下这个类型switch语句是无法通过编译的因为子表达式byte和uint8重复了。好了以上说的就是case表达式的约束以及绕过方式你学会了吗。
总结
我们今天主要讨论了for语句和switch语句不过我并没有说明那些语法规则因为它们太简单了。我们需要多加注意的往往是那些隐藏在Go语言规范和最佳实践里的细节。
这些细节其实就是我们很多技术初学者所谓的“坑”。比如我在讲for语句的时候交代了携带range子句时只有一个迭代变量意味着什么。你必须知道在迭代数组或切片时只有一个迭代变量的话是无法迭代出其中的元素值的否则你的程序可能就不会像你预期的那样运行了。
还有range表达式的结果值是会被复制的实际迭代时并不会使用原值。至于会影响到什么那就要看这个结果值的类型是值类型还是引用类型了。
说到switch语句你要明白其中的case表达式的所有子表达式的结果值都是要与switch表达式的结果值判等的因此它们的类型必须相同或者能够都统一到switch表达式的结果类型。如果无法做到那么这条switch语句就不能通过编译。
最后同一条switch语句中的所有case表达式的子表达式的结果值不能重复不过好在这只是对于由字面量直接表示的子表达式而言的。
请记住普通case子句的编写顺序很重要最上边的case子句中的子表达式总是会被最先求值在判等的时候顺序也是这样。因此如果某些子表达式的结果值有重复并且它们与switch表达式的结果值相等那么位置靠上的case子句总会被选中。
思考题
在类型switch语句中我们怎样对被判断类型的那个值做相应的类型转换
在if语句中初始化子句声明的变量的作用域是什么
戳此查看Go语言专栏文章配套详细代码。