Skip to content

基本流程控制

if语句

Go语言的流程控制主要包括条件分支、循环和并发。在本章,我们先来谈谈基本的流程控制语句。具体到本小节,我们将提及if语句。

if语句一般会由关键字if、条件表达式和由花括号包裹的代码块组成。所谓代码块,即是包含了若干表达式和语句的序列。在Go语言中,代码块必须由花括号包裹。另外,这里的条件表达式是指其结果类型是bool的表达式。一条最简单的if语句可以是:

1
2
3
if 100 > number { 
    number += 3
}

这里的标识符number可以代表一个int类型的值。这条if语句的意思是:如果number的值小于100,那么就把其值增加3。我还可以在此之上添加else分支,就像这样:

1
2
3
4
5
if 100 > number {
    number += 3
} else {
    number -= 2
}

else分支的含义是,提供在条件不成立(具体到这里是number的值不小于100)的情况下需要执行的操作。除此之外,if语句还支持串联。请看下面的例子:

1
2
3
4
5
6
7
if 100 > number {
    number += 3
} else if 100 < number {
    number -= 2
} else {
    fmt.Println("OK!")
}

可以看到,上述代码很像是把多条if语句串接在一起了一样。这样的if语句用于对多个条件的综合判断。上述语句的意思是,若number的值小于100则将其加3,若number的值大于100则将其减2,若number的值等于100则打印OK!。

注意,我们至此还未对number变量进行声明。上面的示例也因此不能通过编译。我们当然可以用单独的语句来声明该变量并为它赋值。但是我们也可以把这样的变量赋值直接加入到if子句中。示例如下:

1
2
3
4
5
6
7
if number := 4; 100 > number {
    number += 3
} else if 100 < number {
    number -= 2
} else {
    fmt.Println("OK!")
}

这里的number := 4被叫做if语句的初始化子句。它应被放置在if关键字和条件表达式之间,并与前者由空格分隔、与后者由英文分号;分隔。注意,我们在这里使用了短变量声明语句,即:在声明变量number的同时为它赋值。这意味着这里的number被视为一个新的变量。它的作用域仅在这条i语句所代表的代码块中。也可以说,变量number对于该if语句之外的代码来说是不可见的。我们若要在该if语句以外使用number变量就会造成编译错误。

另外还要注意,即使我们已经在这条if语句所代表的代码块之外声明了number变量,这里的语句number := 4也是合法的。请看这个例子:

1
2
3
4
5
6
7
8
var number int
if number := 4; 100 > number {
    number += 3
} else if 100 < number {
    number -= 2
} else {
    fmt.Println("OK!")
}

这种写法有一个专有名词,叫做:标识符的重声明。实际上,只要对同一个标识符的两次声明各自所在的代码块之间存在包含的关系,就会形成对该标识符的重声明。具体到这里,第一次声明的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表达式的结果是否相同来决定执行哪个分支。请看下面的示例:

var name string

// 省略若干条语句

1
2
3
4
5
6
7
8
switch name {
    case "Golang":
        fmt.Println("A programming language from Google.")
    case "Rust":
        fmt.Println("A programming language from Mozilla.")
    default:
        fmt.Println("Unknown!")
}

可以看到,在上述switch语句中,name充当了switch表达式,而"Go""Rust"充当了case表达式。它们的结果类型是一致的,都是string。顺便说一句,可以有只包含一个字面量或标识符的表达式。它们是最简单的表达式,属于基本表达式的一种。

请大家注意switch语句的写法。switch表达式必须紧随switch关键字出现。在后面的花括号中,一个关键字casecase表达式、冒号以及后跟的若干条语句组成为一条case语句。在switch语句中可以有若干条case语句。Go语言会依照从上至下的顺序对每一条case语句中case表达式进行求值。只要被发现其表达式与switch表达式的结果相同,该case语句就会被选中。它包含的那些语句就会被执行。而其余的case语句则会被忽略。

switch语句中还可以存在一个特殊的case——default case。顾名思义,当没有一个常规的case被选中的时候,default case就会被选中。上面示例中就存在一个default case。它由关键字default、冒号和后跟的一条语句组成。实际上,default case不一定被追加在最后。它可以是第一个case,或者出现在任意顺位上。

另外,与if语句一样,switch语句还可以包含初始化子句,且其出现位置和写法也如出一辙。如:

1
2
3
4
5
6
7
8
9
names := []string{"Golang", "Java", "Rust", "C"}
switch name := names[0]; name {
    case "Golang":
        fmt.Println("A programming language from Google.")
    case "Rust":
        fmt.Println("A programming language from Mozilla.")
    default:
        fmt.Println("Unknown!")
}

好了,我们已经对switch语句的一般形式——表达式switch语句——有所了解了。下面我们来说说类型switch语句。它与一般形式有两点差别。第一点,紧随case关键字的不是表达式,而是类型说明符。类型说明符由若干个类型字面量组成,且多个类型字面量之间由英文逗号分隔。第二点,它的switch表达式是非常特殊的。这种特殊的表达式也起到了类型断言的作用,但其表现形式很特殊,如:v.(type),其中v必须代表一个接口类型的值。注意,该类表达式只能出现在类型switch语句中,且只能充当switch表达式。一个类型switch语句的示例如下:

1
2
3
4
5
6
7
8
9
v := 11
switch i := interface{}(v).(type) {
    case int, int8, int16, int32, int64:
        fmt.Printf("A signed integer: %d. The type is %T. \n", i, i)
    case uint, uint8, uint16, uint32, uint64:
        fmt.Printf("A unsigned integer: %d. The type is %T. \n", i, i)
    default:
        fmt.Println("Unknown!")
}

请注意,我们在这里把switch表达式的结果赋给了一个变量。如此一来,我们就可以在该switch语句中使用这个结果了。这段代码被执行后,标准输出上会打印出A signed integer: 11. The type is int.

最后,我们来说一下fallthrough。它既是一个关键字,又可以代表一条语句。fallthrough语句可被包含在表达式switch语句中的case语句中。它的作用是使控制权流转到下一个case。不过要注意,fallthrough语句仅能作为case语句中的最后一条语句出现。并且,包含它的case语句不能是其所属switch语句的最后一条case语句。

范例:

package main

import (
    "fmt"
    "math/rand"
)

func main() {
    ia := []interface{}{byte(6), 'a', uint(10), int32(-4)}
    switch v := ia[rand.Intn(4) %2 ]; interface{}(v).(type) {
    case int32 :
        fmt.Printf("Case A.")
    case byte :
        fmt.Printf("Case B.")
    default:
        fmt.Println("Unknown!")
    }
}

for语句

for语句代表着循环。一条语句通常由关键字for、初始化子句、条件表达式、后置子句和以花括号包裹的代码块组成。其中,初始化子句、条件表达式和后置子句之间需用分号分隔。示例如下:

1
2
3
for i := 0; i < 10; i++ {
    fmt.Print(i, " ")
}

我们可以省略掉初始化子句、条件表达式、后置子句中的任何一个或多个,不过起到分隔作用的分号一般需要被保留下来,除非在仅有条件表达式或三者全被省略时分号才可以被一同省略。

我们可以把上述的初始化子句、条件表达式、后置子句合称为for子句。实际上,for语句还有另外一种编写方式,那就是用range子句替换掉for子句。range子句包含一个或两个迭代变量(用于与迭代出的值绑定)、特殊标记:==、关键字range以及range表达式。其中,range表达式的结果值的类型应该是能够被迭代的,包括:字符串类型、数组类型、数组的指针类型、切片类型、字典类型和通道类型。例如:

1
2
3
for i, v := range "Go语言" {
    fmt.Printf("%d: %c\n", i, v)
}

对于字符串类型的被迭代值来说,for语句每次会迭代出两个值。第一个值代表第二个值在字符串中的索引,而第二个值则代表该字符串中的某一个字符。迭代是以索引递增的顺序进行的。例如,上面的for语句被执行后会在标准输出上打印出:

1
2
3
4
0: G
1: o
2: 
5: 

可以看到,这里迭代出的索引值并不是连续的。下面我们简单剖析一下此表象的本质。我们知道,字符串的底层是以字节数组的形式存储的。而在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

package main

import (
    "fmt"
)

func main() {
    map1 := map[int]string{1: "Golang", 2: "Java", 3: "Python", 4: "C"}
    for i, v := range map1 {
        fmt.Printf("%d: %c\n", i, v)
    } 
}

select语句

select语句属于条件分支流程控制方法,不过它只能用于通道。它可以包含若干条case语句,并根据条件选择其中的一个执行。进一步说,select语句中的case关键字只能后跟用于通道的发送操作的表达式以及接收操作的表达式或语句。示例如下:

ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
// 省略若干条语句
select {
    case e1 := <-ch1:
        fmt.Printf("1th case is selected. e1=%v.\n", e1)
    case e2 := <-ch2:
        fmt.Printf("2th case is selected. e2=%v.\n", e2)
    default:
        fmt.Println("No data!")
} 

如果该select语句被执行时通道ch1ch2中都没有任何数据,那么肯定只有default case会被执行。但是,只要有一个通道在当时有数据就不会轮到default case执行了。显然,对于包含通道接收操作的case来讲,其执行条件就是通道中存在数据(或者说通道未空)。如果在当时有数据的通道多于一个,那么Go语言会通过一种伪随机的算法来决定哪一个case将被执行。

另一方面,对于包含通道发送操作的case来讲,其执行条件就是通道中至少还能缓冲一个数据(或者说通道未满)。类似的,当有多个case中的通道未满时,它们会被随机选择。请看下面的示例:

ch3 := make(chan int, 100)
// 省略若干条语句
select {
    case ch3 <- 1:
        fmt.Printf("Sent %d\n", 1)
    case ch3 <- 2:
        fmt.Printf("Sent %d\n", 2)
    default:
        fmt.Println("Full channel!")
}

该条select语句的两个case中包含的都是针对通道ch3的发送操作。如果我们把这条语句置于一个循环中,那么就相当于用有限范围的随机整数集合去填满一个通道。

请注意,如果一条select语句中不存在default case, 并且在被执行时其中的所有case都不满足执行条件,那么它的执行将会被阻塞!当前流程的进行也会因此而停滞。直到其中一个case满足了执行条件,执行才会继续。我们一直在说case执行条件的满足与否取决于其操作的通道在当时的状态。这里特别强调一点,即:未被初始化的通道会使操作它的case永远满足不了执行条件。对于针对它的发送操作和接收操作来说都是如此。

最后提一句,break语句也可以被包含在select语句中的case语句中。它的作用是立即结束当前的select语句的执行,不论其所属的case语句中是否还有未被执行的语句。

范例:输出上打印出 No Data! 1 End.

package main

import "fmt"

func main() {
    ch4 := make(chan int, 1)
    for i := 0; i < 4; i++ {
        select {
        case e, ok := <-ch4:
            if !ok {
                fmt.Println("End.")
                return
            }
            fmt.Println(e)
            close(ch4)
        default:
            fmt.Println("No Data!")
            ch4 <- 1
        }
    }
}