1 序


图片名称 众所周知,任何一门程序设计语言的执行顺序都依赖于流程控制,流程控制语句,用于设定程序执行的次序,建立程序的逻辑结构。可以说,流程控制语句是整个程序的骨架,如果没有任何控制的话,那么程序会按照代码的一行行地顺序执行。

Golang是一门注重简洁、高效、并发编程的编程语言。在编写任何程序时,掌握流程控制是至关重要的,它决定了程序执行的顺序和条件。同时提供了一组清晰而灵活的流程控制结构,使得开发者能够轻松地编写可读性高且高效的代码。

2 Overview

2.1 顺序执行

在Golang中,程序按照代码的顺序逐行执行。这是最基本的流程控制形式,是任何编程语言的基础。通过顺序执行,我们能够在程序中定义一系列的操作,实现所需的功能。

1
2
3
4
5
6
7
8
import "fmt"

func main() {
fmt.Println("Hello, World!")
fmt.Println("Welcome to Golang!")
// Output: Hello, World!
// Output: Welcome to Golang!
}

2.2 控制语句

在Golang中,从根本上讲,流程控制只是为了控制程序语句的执行顺序,一般需要与各种条件配合,因此,在各种流程中,会加入条件判断语句。流程控制语句一般起以下3个作用:

  • 选择,即根据条件跳转到不同的执行序列;
  • 循环,即根据条件反复执行某个序列,当然每一次循环执行的输入输出可能会发生变化;
  • 跳转,即根据条件返回到某执行序列。

Go语言支持如下的几种流程控制语句:

  • 条件语句,对应的关键字为if、else和else if;
  • 选择语句,对应的关键字为switch、case和select(将在介绍channel的时候细说);
  • 循环语句,对应的关键字为for和range;
  • 跳转语句,对应的关键字为goto。
    在具体的应用场景中,为了满足更丰富的控制需求,Go语言还添加了如下关键字:breakcontinuefallthrough,在实际的使用中,需要根据具体的逻辑目标、程序执行的时间和空间限制、代码的可读性、编译器的代码优化设定等多种因素,灵活组合。

接下来简要介绍一下各种流程控制功能的用法以及需要注意的要点。

3 条件控制

条件语句在编程中经常被使用,Golang提供了简单而强大的if、else-if、else结构。这使得我们能够根据不同的条件执行不同的代码块。

3.1 基本语法

if是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行if后由大括号括起来的代码块,否则就忽略该代码块继续执行后续的代码。

1
2
3
if condition {
// do something
}

如果存在第二个分支,则可以在上面代码的基础上添加else关键字以及另一代码块,这个代码块中的代码只有在条件不满足时才会执行。if和else后的两个代码块是相互独立的分支,只可能执行其中一个。

1
2
3
4
5
if condition {
// do something
} else {
// do something
}

如果存在第三个分支,则可以使用下面这种三个独立分支的形式:

1
2
3
4
5
6
7
if condition1 {
// do something
} else if condition2 {
// do something else
} else {
// catch-all or default
}

注:else-if分支的数量是没有限制的,但是为了代码的可读性,还是不要在if后面加入太多的else-if结构。如果你必须使用这种形式,则把尽可能先满足的条件放在前面。

3.2 注意点

1.条件语句不需要使用括号将条件包含起来()。

1
2
3
4
5
6
7
8
9
10
11
import "fmt"

func main() {
var x int = 20
// x > 10无需()括起来
if x > 10 {
fmt.Println("x is greater than 10")
} else {
fmt.Println("x is less than 10")
}
}

2.无论语句体内有几条语句,花括号{}都是必须存在的。

1
2
3
4
5
6
7
8
import "fmt"

func main() {
var x int = 20
if x > 10 // expected '{', found newline syntax
fmt.Println("x is greater than 10")
// 编译错误
}

3.右花括号}必须与if或者else处于同一行,即:elseelse if不能另起一行,否则编译错误。

1
2
3
4
5
6
7
8
9
10
11
12
import "fmt"

func main() {
var x int = 20
if x > 10 {
fmt.Println("x is greater than 10")
}
else { // syntax error: unexpected else, expected }
fmt.Println("x is less than 10")
}
// 编译错误
}

4.在if条件判断语句内可以添加变量初始化语句,使用 ; 间隔,这个变量地作用域只能在该条件逻辑块内,其他地方则不起作用。

1
2
3
4
5
6
7
8
9
10
11
import "fmt"

func main() {
if x := random(50); x > 10 {
fmt.Println("x is greater than 10")
} else {
fmt.Println("x is less than 10")
}
// 超出条件逻辑块作用域,则会编译错误
fmt.Println(x) // undefined: x compiler (UndeclaredName)
}

4 选择语句

如果你的分支条件越来越多,你需要写很多的if-else来实现一些逻辑处理,这个时候代码看上去就很丑很冗长,而且也不易于以后的维护,这个时候switch就能很好的解决这个问题,相比较C和Java等其它语言而言,Go语言中的switch结构使用上更加灵活。它接受任意形式的表达式,他的语法大致如下:

1
2
3
4
5
6
7
8
9
10
11
switch var1 {
case expr1:
// do something1
case expr2:
// do something2
case expr3:
// do something3
default:
// do something4
}

4.1 switch变量

变量var1可以是任何类型,同时也可以是表达式,而expr1和expr2可以是同类型的任意值,也可以是表达式。类型不被局限于常量或整数,但必须是相同的类型,或者最终结果为相同类型的表达式,然后另外前花括号{必须和switch关键字在同一行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import "fmt"

func main() {
x := 10
switch x {
case 20 - x:
fmt.Println("20 - x = ", x)
case 10:
fmt.Println("x = ", x)
default:
fmt.Println("default x value")
}
}
// Output: 20 - x = 10

经过输出你会发现,条件满足第一个case,故输出20 - x = 10,这看起来很正常,但你仔细一看,实际上你会发现当x=10的时候,case 10case 20 - x都满足条件,但是只输出了20 - x = 10,只是为什么?

因为Go语言使用快速的查找算法从上往下依次来测试switch条件与case分支的匹配情况,直到算法匹配到某个case或者进入default条件为止,一旦成功地匹配到某个分支,在执行完相应代码后就会退出整个switch代码块,也就是说您不需要特别使用break语句来表示结束。这就导致同样满足条件的case 10的条件并没有执行到,所以case分支应该是唯一且互斥的,从而避免这样的情况发生。

另外需要注意的是:多个相同条件的case一定不能是常量,否则会发生编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
import "fmt"

func main() {
x := 10
switch x {
case 20 - 10:
fmt.Println("20 - 10 = ", x)
case 10: // duplicate case 10 (constant of type int) in expression switch
fmt.Println("x = ", x)
default:
fmt.Println("default x value")
}
}

该例中,如果case中无论是10还是经过计算表达式20 - 10得到的10都是相同条件的case,这样编译器会直接报重复的case 10,除非像上例中的case 20 - x这样, 在编译期间,编译器无法判断20 - x = 10一定成立,只有在运行期间才会知道,所以编译期间并不会编译错误,但是即便如此,我们也应该避免写出这样的代码。

4.2 case多个条件

另外同一个case可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "fmt"

func main() {
x := 10
switch x {
case 20 - x, x, x * 1:
fmt.Println("x = ", x)
case 15:
fmt.Println("x = 15")
default:
fmt.Println("default x value")
}
// Output: x = 10
}

4.3 switch初始化变量

和if一样,swith后也是可以初始化变量,然后再进行判断,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "fmt"

func main() {
switch x := random(50); {
case 20 - x:
fmt.Println("20 - x = ", x)
case 10:
fmt.Println("x = ", x)
case 15:
fmt.Println("x = 15", x)
default:
fmt.Println("default x value")
}
}

4.4 fallthrough关键字

如果想要执行完第一个case后,继续执行下一个case,我们可以使用fallthrough关键字,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import "fmt"

func main() {
x := 10
switch x {
case 20 - x:
fmt.Println("20 - x = ", x)
fallthrough
case 10:
fmt.Println("x = ", x)
fallthrough
case 15:
fmt.Println("x = 15", x)
default:
fmt.Println("default x value")
}
}
// Output:
// 20 - x = 10
// x = 10
// x = 15

该示例很好演示了fallthrough关键字的作用,但你仔细发现x=10并不满足case 15的条件,但是仍然会执行,这是为什么?

注意: 程序执行到fallthrough的时候,无论下一个case是否满足条件,都会直接执行case后的代码。

4.5 switch表达式

当switch跟一个表达式的时候,表达式返回的类型也要和case的类型保持一致,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import "fmt"

func main() {
age := 20
switch age > 0 {
case age <= 1:
fmt.Println("you are just a baby")
case age > 1 && age <= 18:
fmt.Println("you are still a teenager")
case age > 18:
fmt.Println("you are an adult")
}
// Output: you are an adult
}

5 循环语句

与多数语言不同的是,Go语言中的循环语句只支持for和range关键字,而不支持whiledo-while结构。关键字for的基本使用方法与C和C++中非常接近。

5.1 基于数值的for循环

基于数值的for循环基本语法如下:

1
2
3
for expression1; expression2; expression3 {
//...
}

其中expression1expression2expression3都是表达式,其中expression1expression3是变
量声明或者函数调用返回值之类的,expression2是用来条件判断,expression1在循环开始之前调用,expression3在每轮循环结束之时调用。

使用for循环需要注意两点:

  • 同if判断一样,条件语句不需要使用括号将条件包含起来()
  • 左花括号必须和for处同一行,否则编译错误
  • Go语言的for循环同样支持continue和break来控制循环,但是它提供了一个更高级的break,可以选择中断哪一个循环。

让我们来看一个例子:

1
2
3
4
5
6
7
8
import "fmt"

func main() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
}

5.2 基于条件的for循环

for循环的第二种形式是没有头部的条件判断迭代(类似其它语言中的while循环),基本语法如下:

1
2
3
for condition {
// do something
}

您可以认为这是没有初始化语句和修饰语句的for结构,因此;;便是多余的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import "fmt"

func main() {
var i int = 5
for i >= 0 {
i = i - 1
fmt.Printf("The variable i is now: %d\n", i)
}
}
// Output:
// The variable i is now: 4
// The variable i is now: 3
// The variable i is now: 2
// The variable i is now: 1
// The variable i is now: 0
// The variable i is now: -1

5.3 无限for循环

条件语句是可以被省略的,如i:=0; ; i++for { }for ;; { } 其中;;会在使用gofmt时被移除,这些循环的本质就是无限循环,最后一个形式也可以被改写为for true { },但一般情况下都会直接写for { }
如果for循环的头部没有条件语句,那么就会认为条件永远为true,因此循环体内必须有相关的条件判断以确保会在某个时刻退出循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "fmt"

func main() {
var i int = 15
for {
if i < 10 {
break
}
fmt.Printf("The variable i is now: %d\n", i)
i--
}
}
// Output:
// The variable i is now: 15
// The variable i is now: 14
// The variable i is now: 13
// The variable i is now: 12
// The variable i is now: 11
// The variable i is now: 10

5.4 for-range循环

这是Golang特有的一种的迭代结构,您会发现它在许多情况下都非常有用。它可以迭代任何一个集合(包括数组、切片和map)。语法上很类似其它语言中foreach语句,但您依旧可以获得每次迭代所对应的索引,当迭代map时没有索引idx,取而代之的时key,一般语法格式如下:

1
2
3
4
5
for idx|key, value := range collection
{
// do something
}

要注意的是,value始终为集合中对应索引的值拷贝,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值(译者注:如果val为指针,则会产生指针的拷贝,依旧可以修改集合中的原值),一个字符串是Unicode编码的字符(或称之为rune)集合,因此您也可以用它迭代字符串。

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
import "fmt"

func main() {
// 迭代数组和切片类似,这里以数组为例
array := [...]int{0:1, 2:3, 4}
for idx, value := range array {
fmt.Printf("index = %d, value = %d\n", idx, value)
}
// Output:
// index = 0, value = 1
// index = 1, value = 0
// index = 2, value = 3
// index = 3, value = 4

// 迭代map
mapping := map[string]string{"name": "Ratel","age", "20"}
for key, value := range mapping {
fmt.Printf("key = %s, value = %s\n", key, value)
}
// Output:
// index = name, value = Ratel
// index = age, value = 20

// 迭代字符串
str := "Ratel"
for idx, value := range str {
fmt.Printf("idx = %d, value = %c\n", idx, value)
}
// Output:
// idx = 0, value = R
// idx = 1, value = a
// idx = 2, value = t
// idx = 3, value = e
// idx = 4, value = l
}

6 跳转语句

for、switch或select语句都可以配合标签[label]形式的标识符使用,即某一行第一个以冒号 : 结尾的单词,其中标签标签的名称是大小写敏感的,为了提升可读性,一般建议使用全部大写字母。

6.1 标签定义后必须使用

标签定义了就必须要使用,定义但未使用标签会导致编译错误,例如:

1
2
3
4
5
6
7
8
9
10
import "fmt"

func main() {
i := 0
START: //label START declared and not used compiler (UnusedLabel)
if i < 3 {
fmt.Println(i)
i++
}
}

6.2 goto语句和标签之间不能定义其他变量

当goto后面的标签出现的位置在goto语句之后时,两者之间不能有新的变量定义,否则将导致编译错误,例如:

1
2
3
4
5
6
7
8
9
10
import "fmt"

func main() {
a := 1
goto TARGET // goto TARGET jumps over variable declaration at line 11 compiler
b := 9
TARGET:
b += a
fmt.Printf("a is %v, b is %v", a, b)
}

6.3 goto语句与标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "fmt"

func main() {
i := 0
START:
if i < 3 {
fmt.Println(i)
i++
goto START
}

// Output:
// 0
// 1
// 2
}

6.4 continue语句与标签

continue语句也可以和标签配置使用,我们知道continue语句只能在循环中使用,所以结合continue结合标签也只能在循环中使用,例如:

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
import "fmt"

func main() {
for i := 0; i <= 3; i++ {
LABEL:
for j := 0; j <= 3; j++ {
if j == 2 {
continue LABEL
}
fmt.Printf("i is: %d, and j is: %d\n", i, j)
}
}
}
// Output:
// i is: 0, and j is: 0
// i is: 0, and j is: 1
// i is: 0, and j is: 3
// i is: 1, and j is: 0
// i is: 1, and j is: 1
// i is: 1, and j is: 3
// i is: 2, and j is: 0
// i is: 2, and j is: 1
// i is: 2, and j is: 3
// i is: 3, and j is: 0
// i is: 3, and j is: 1
// i is: 3, and j is: 3

该示例展示了continue LABEL的使用,根据输出结果来看,该示例是否使用标签结果都是一样,原因是continue本身功能就是退出本次循环,从下一次开始,而LABEL标签恰好在下一次的执行位置,所以要不要标签上面的结果都是一样。

接下来我们把标签LABEL移到外层循环呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "fmt"

func main() {
LABEL:
for i := 0; i <= 3; i++ {
for j := 0; j <= 3; j++ {
if j == 2 {
continue LABEL
}
fmt.Printf("i is: %d, and j is: %d\n", i, j)
}
}
}
// Output:
// i is: 0, and j is: 0
// i is: 0, and j is: 1
// i is: 1, and j is: 0
// i is: 1, and j is: 1
// i is: 2, and j is: 0
// i is: 2, and j is: 1
// i is: 3, and j is: 0
// i is: 3, and j is: 1

根据结果我们看到,continue LABEL有两层意思,一是退出当前本次循环,二是跳转到标签处执行代码,当前i=0时,该函数执行到在j==2时退出了本次循环,如果没有标签LABEL或者标签LABEL在内层循环,则会输出i is: 0, and j is 3,现在LABEL在外层,则直接跳转到外层,从i=1开始重新执行,j=3则不会执行。

6.5 break语句与标签

break语句也可以和标签配置使用,我们知道break语句只能在循环中使用,所以结合break结合标签也只能在循环中使用,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "fmt"

func main() {
for i := 0; i <= 3; i++ {
LABEL:
for j := 0; j <= 3; j++ {
if j == 2 {
break LABEL
}
fmt.Printf("i is: %d, and j is: %d\n", i, j)
}
}
}
// Output:
// i is: 0, and j is: 0
// i is: 0, and j is: 1
// i is: 1, and j is: 0
// i is: 1, and j is: 1
// i is: 2, and j is: 0
// i is: 2, and j is: 1
// i is: 3, and j is: 0
// i is: 3, and j is: 1

该示例展示了break LABEL的使用,根据输出结果来看,该示例是否使用标签结果都是一样,原因是break本身功能就是退出本层循环,从外层循环开始,而LABEL标签加在内层循环毫无意义,根本不会执行,所以要不要标签上面的结果都是一样。

接下来我们把标签LABEL移到外层循环呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import "fmt"

func main() {
LABEL:
for i := 0; i <= 3; i++ {
for j := 0; j <= 3; j++ {
if j == 2 {
break LABEL
}
fmt.Printf("i is: %d, and j is: %d\n", i, j)
}
}
}
// Output:
// i is: 0, and j is: 0
// i is: 0, and j is: 1

根据结果我们看到,break LABEL执行完成后,不仅会退出内层循环,同时也会退出外层循环,即标签在哪一层就会退出到哪一层。

6.6 goto注意事项

  • 避免滥用:在绝大多数情况下,可以通过更好的代码结构和控制流语句来代替goto。只在极少数情况下,例如处理错误跳转等,才会考虑使用 goto。

  • 跳转范围:goto只能在同一个函数内部进行跳转,不能跨越函数。

  • 不同分支中的标签:标签不能在不同的分支之间重复使用,例如一个标签不能同时在ifswitch语句中。

  • 避免形成死循环:使用goto时要小心,确保不会形成死循环,否则会导致程序无法正常退出。

特别注意:使用标签和goto语句是不被鼓励的,因为它们会很快导致非常糟糕的程序设计,而且总有更加可读的替代方
案来实现相同的需求。

7 总结

到此相信你已经了解了流程控制,流程控制是编程之路上的重要一步,Golang提供了简单而强大的工具,使得我们能够清晰地表达程序的逻辑,并以高效的方式实现所需的功能。通过深入理解和灵活运用流程控制,您将能够编写出可读性强、可维护性高的Golang代码,下一章节我们将介绍Package包。