基本流程控制
if语句
Go语言的流程控制主要包括条件分支、循环和并发。在本章,我们先来谈谈基本的流程控制语句。具体到本小节,我们将提及if
语句。
if
语句一般会由关键字if
、条件表达式和由花括号包裹的代码块组成。所谓代码块,即是包含了若干表达式和语句的序列。在Go语言中,代码块必须由花括号包裹。另外,这里的条件表达式是指其结果类型是bool
的表达式。一条最简单的if
语句可以是:
这里的标识符number
可以代表一个int
类型的值。这条if
语句的意思是:如果number
的值小于100
,那么就把其值增加3
。我还可以在此之上添加else
分支,就像这样:
else
分支的含义是,提供在条件不成立(具体到这里是number
的值不小于100
)的情况下需要执行的操作。除此之外,if
语句还支持串联。请看下面的例子:
可以看到,上述代码很像是把多条if
语句串接在一起了一样。这样的if
语句用于对多个条件的综合判断。上述语句的意思是,若number
的值小于100则将其加3,若number
的值大于100
则将其减2
,若number
的值等于100
则打印OK!。
注意,我们至此还未对number
变量进行声明。上面的示例也因此不能通过编译。我们当然可以用单独的语句来声明该变量并为它赋值。但是我们也可以把这样的变量赋值直接加入到if
子句中。示例如下:
这里的number := 4
被叫做if
语句的初始化子句。它应被放置在if
关键字和条件表达式之间,并与前者由空格分隔、与后者由英文分号;
分隔。注意,我们在这里使用了短变量声明语句,即:在声明变量number
的同时为它赋值。这意味着这里的number
被视为一个新的变量。它的作用域仅在这条i语句所代表的代码块中。也可以说,变量number
对于该if
语句之外的代码来说是不可见的。我们若要在该if
语句以外使用number
变量就会造成编译错误。
另外还要注意,即使我们已经在这条if
语句所代表的代码块之外声明了number
变量,这里的语句number := 4
也是合法的。请看这个例子:
这种写法有一个专有名词,叫做:标识符的重声明。实际上,只要对同一个标识符的两次声明各自所在的代码块之间存在包含的关系,就会形成对该标识符的重声明。具体到这里,第一次声明的number
变量所在的是该if
语句的外层代码块,而number := 4
所声明的number
变量所在的是该if
语句的代表代码块。它们之间存在包含关系。因此对number
的重声明就形成了。
这种情况造成的结果就是,if
语句内部对number
的访问和赋值都只会涉及到第二次声明的那个number
变量。这种现象也被叫做标识符的遮蔽。上述代码被执行完毕之后,第二次声明的number
变量的值会是7
,而第一次声明的number
变量的值仍会是0。
switch语句
与串联的if
语句类似,switch
语句提供了一个多分支条件执行的方法。不过在这里用一个专有名词来代表分支——case
。每一个case
可以携带一个表达式或一个类型说明符。前者又可被简称为case
表达式。因此,Go语言的switch
语句又分为表达式switch
语句和类型switch
语句。
先说表达式switch
语句。在此类switch
语句中,每个case
会携带一个表达式。与if
语句中的条件表达式不同,这里的case
表达式的结果类型并不一定是bool
。不过,它们的结果类型需要与switch
表达式的结果类型一致。所谓switch
表达式是指switch
语句中要被判定的那个表达式。switch
语句会依据该表达式的结果与各个case
表达式的结果是否相同来决定执行哪个分支。请看下面的示例:
// 省略若干条语句
可以看到,在上述switch
语句中,name
充当了switch
表达式,而"Go"
和"Rust"
充当了case
表达式。它们的结果类型是一致的,都是string。顺便说一句,可以有只包含一个字面量或标识符的表达式。它们是最简单的表达式,属于基本表达式的一种。
请大家注意switch
语句的写法。switch
表达式必须紧随switch
关键字出现。在后面的花括号中,一个关键字case
、case
表达式、冒号以及后跟的若干条语句组成为一条case
语句。在switch
语句中可以有若干条case
语句。Go语言会依照从上至下的顺序对每一条case
语句中case
表达式进行求值。只要被发现其表达式与switch
表达式的结果相同,该case
语句就会被选中。它包含的那些语句就会被执行。而其余的case
语句则会被忽略。
switch
语句中还可以存在一个特殊的case
——default case
。顾名思义,当没有一个常规的case
被选中的时候,default case
就会被选中。上面示例中就存在一个default case
。它由关键字default
、冒号和后跟的一条语句组成。实际上,default case
不一定被追加在最后。它可以是第一个case
,或者出现在任意顺位上。
另外,与if
语句一样,switch
语句还可以包含初始化子句,且其出现位置和写法也如出一辙。如:
好了,我们已经对switch
语句的一般形式——表达式switch
语句——有所了解了。下面我们来说说类型switch
语句。它与一般形式有两点差别。第一点,紧随case
关键字的不是表达式,而是类型说明符。类型说明符由若干个类型字面量组成,且多个类型字面量之间由英文逗号分隔。第二点,它的switch
表达式是非常特殊的。这种特殊的表达式也起到了类型断言的作用,但其表现形式很特殊,如:v.(type)
,其中v必须代表一个接口类型的值。注意,该类表达式只能出现在类型switch
语句中,且只能充当switch
表达式。一个类型switch
语句的示例如下:
请注意,我们在这里把switch
表达式的结果赋给了一个变量。如此一来,我们就可以在该switch
语句中使用这个结果了。这段代码被执行后,标准输出上会打印出A signed integer: 11. The type is int.
。
最后,我们来说一下fallthrough
。它既是一个关键字,又可以代表一条语句。fallthrough
语句可被包含在表达式switch
语句中的case
语句中。它的作用是使控制权流转到下一个case
。不过要注意,fallthrough
语句仅能作为case
语句中的最后一条语句出现。并且,包含它的case
语句不能是其所属switch
语句的最后一条case
语句。
范例:
for语句
for
语句代表着循环。一条语句通常由关键字for
、初始化子句、条件表达式、后置子句和以花括号包裹的代码块组成。其中,初始化子句、条件表达式和后置子句之间需用分号分隔。示例如下:
我们可以省略掉初始化子句、条件表达式、后置子句中的任何一个或多个,不过起到分隔作用的分号一般需要被保留下来,除非在仅有条件表达式或三者全被省略时分号才可以被一同省略。
我们可以把上述的初始化子句、条件表达式、后置子句合称为for
子句。实际上,for
语句还有另外一种编写方式,那就是用range
子句替换掉for
子句。range
子句包含一个或两个迭代变量(用于与迭代出的值绑定)、特殊标记:=
或=
、关键字range
以及range
表达式。其中,range
表达式的结果值的类型应该是能够被迭代的,包括:字符串类型、数组类型、数组的指针类型、切片类型、字典类型和通道类型。例如:
对于字符串类型的被迭代值来说,for
语句每次会迭代出两个值。第一个值代表第二个值在字符串中的索引,而第二个值则代表该字符串中的某一个字符。迭代是以索引递增的顺序进行的。例如,上面的for
语句被执行后会在标准输出上打印出:
可以看到,这里迭代出的索引值并不是连续的。下面我们简单剖析一下此表象的本质。我们知道,字符串的底层是以字节数组的形式存储的。而在Go语言中,字符串到字节数组的转换是通过对其中的每个字符进行UTF-8编码来完成的。字符串"Go语言"
中的每一个字符与相应的字节数组之间的对应关系如下:
字符 | G | o | 语[0] | 语[1] | 语[2] | 言[0] | 言[1] | 言[2] |
---|---|---|---|---|---|---|---|---|
字节 | 0x47 |
0x6F |
0xE8 |
0xAF |
0xAD |
0xE8 |
0xA8 |
0x80 |
索引 | 0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
注意,一个中文字符在经过UTF-8编码之后会表现为三个字节。所以,我们用语[0]、语[1]和、语[2]分别表示字符'语'
经编码后的第一、二、三个字节。对于字符'言'
,我们如法炮制。
对照这张表格,我们就能够解释上面那条for
语句打印出的内容了,即:每次迭代出的第一个值所代表的是第二个字符值经编码后的第一个字节在该字符串经编码后的字节数组中的索引值。请大家真正理解这句话的含义。
对于数组值、数组的指针值和切片之来说,range
子句每次也会迭代出两个值。其中,第一个值会是第二个值在被迭代值中的索引,而第二个值则是被迭代值中的某一个元素。同样的,迭代是以索引递增的顺序进行的。
对于字典值来说,range
子句每次仍然会迭代出两个值。显然,第一个值是字典中的某一个键,而第二个值则是该键对应的那个值。注意,对字典值上的迭代,Go语言是不保证其顺序的。
携带range
子句的for
语句还可以应用于一个通道值之上。其作用是不断地从该通道值中接收数据,不过每次只会接收一个值。注意,如果通道值中没有数据,那么for
语句的执行会处于阻塞状态。无论怎样,这样的循环会一直进行下去。直至该通道值被关闭,for
语句的执行才会结束。
最后,我们来说一下break
语句和continue
语句。它们都可以被放置在for
语句的代码块中。前者被执行时会使其所属的for
语句的执行立即结束,而后者被执行时会使当次迭代被中止(当次迭代的后续语句会被忽略)而直接进入到下一次迭代。
范例:执行时会在标准输出上打印出1: Golang 2: Java 3: Python 4: C
select语句
select
语句属于条件分支流程控制方法,不过它只能用于通道。它可以包含若干条case
语句,并根据条件选择其中的一个执行。进一步说,select
语句中的case
关键字只能后跟用于通道的发送操作的表达式以及接收操作的表达式或语句。示例如下:
如果该select
语句被执行时通道ch1
和ch2
中都没有任何数据,那么肯定只有default case
会被执行。但是,只要有一个通道在当时有数据就不会轮到default case
执行了。显然,对于包含通道接收操作的case
来讲,其执行条件就是通道中存在数据(或者说通道未空)。如果在当时有数据的通道多于一个,那么Go语言会通过一种伪随机的算法来决定哪一个case
将被执行。
另一方面,对于包含通道发送操作的case
来讲,其执行条件就是通道中至少还能缓冲一个数据(或者说通道未满)。类似的,当有多个case
中的通道未满时,它们会被随机选择。请看下面的示例:
该条select
语句的两个case
中包含的都是针对通道ch3
的发送操作。如果我们把这条语句置于一个循环中,那么就相当于用有限范围的随机整数集合去填满一个通道。
请注意,如果一条select
语句中不存在default case
, 并且在被执行时其中的所有case
都不满足执行条件,那么它的执行将会被阻塞!当前流程的进行也会因此而停滞。直到其中一个case
满足了执行条件,执行才会继续。我们一直在说case
执行条件的满足与否取决于其操作的通道在当时的状态。这里特别强调一点,即:未被初始化的通道会使操作它的case
永远满足不了执行条件。对于针对它的发送操作和接收操作来说都是如此。
最后提一句,break语句也可以被包含在select
语句中的case
语句中。它的作用是立即结束当前的select
语句的执行,不论其所属的case
语句中是否还有未被执行的语句。
范例:输出上打印出 No Data! 1 End.