Go基础篇-流程控制
1 序
众所周知,任何一门程序设计语言的执行顺序都依赖于流程控制,流程控制语句,用于设定程序执行的次序,建立程序的逻辑结构。可以说,流程控制语句是整个程序的骨架,如果没有任何控制的话,那么程序会按照代码的一行行地顺序执行。
Golang是一门注重简洁、高效、并发编程的编程语言。在编写任何程序时,掌握流程控制是至关重要的,它决定了程序执行的顺序和条件。同时提供了一组清晰而灵活的流程控制结构,使得开发者能够轻松地编写可读性高且高效的代码。
2 Overview
2.1 顺序执行
在Golang中,程序按照代码的顺序逐行执行。这是最基本的流程控制形式,是任何编程语言的基础。通过顺序执行,我们能够在程序中定义一系列的操作,实现所需的功能。
1 | import "fmt" |
2.2 控制语句
在Golang中,从根本上讲,流程控制只是为了控制程序语句的执行顺序,一般需要与各种条件配合,因此,在各种流程中,会加入条件判断语句。流程控制语句一般起以下3个作用:
- 选择,即根据条件跳转到不同的执行序列;
- 循环,即根据条件反复执行某个序列,当然每一次循环执行的输入输出可能会发生变化;
- 跳转,即根据条件返回到某执行序列。
Go语言支持如下的几种流程控制语句:
- 条件语句,对应的关键字为if、else和else if;
- 选择语句,对应的关键字为switch、case和select(将在介绍channel的时候细说);
- 循环语句,对应的关键字为for和range;
- 跳转语句,对应的关键字为goto。
在具体的应用场景中,为了满足更丰富的控制需求,Go语言还添加了如下关键字:break
、continue
和fallthrough
,在实际的使用中,需要根据具体的逻辑目标、程序执行的时间和空间限制、代码的可读性、编译器的代码优化设定等多种因素,灵活组合。
接下来简要介绍一下各种流程控制功能的用法以及需要注意的要点。
3 条件控制
条件语句在编程中经常被使用,Golang提供了简单而强大的if、else-if、else
结构。这使得我们能够根据不同的条件执行不同的代码块。
3.1 基本语法
if是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行if后由大括号括起来的代码块,否则就忽略该代码块继续执行后续的代码。
1 | if condition { |
如果存在第二个分支,则可以在上面代码的基础上添加else关键字以及另一代码块,这个代码块中的代码只有在条件不满足时才会执行。if和else后的两个代码块是相互独立的分支,只可能执行其中一个。
1 | if condition { |
如果存在第三个分支,则可以使用下面这种三个独立分支的形式:
1 | if condition1 { |
注:
else-if
分支的数量是没有限制的,但是为了代码的可读性,还是不要在if后面加入太多的else-if结构。如果你必须使用这种形式,则把尽可能先满足的条件放在前面。
3.2 注意点
1.条件语句不需要使用括号将条件包含起来()。
1 | import "fmt" |
2.无论语句体内有几条语句,花括号{}都是必须存在的。
1 | import "fmt" |
3.右花括号}必须与if或者else处于同一行,即:else
和else if
不能另起一行,否则编译错误。
1 | import "fmt" |
4.在if条件判断语句内可以添加变量初始化语句,使用 ; 间隔,这个变量地作用域只能在该条件逻辑块内,其他地方则不起作用。
1 | import "fmt" |
4 选择语句
如果你的分支条件越来越多,你需要写很多的if-else
来实现一些逻辑处理,这个时候代码看上去就很丑很冗长,而且也不易于以后的维护,这个时候switch
就能很好的解决这个问题,相比较C和Java等其它语言而言,Go语言中的switch结构使用上更加灵活。它接受任意形式的表达式,他的语法大致如下:
1 | switch var1 { |
4.1 switch变量
变量var1可以是任何类型,同时也可以是表达式,而expr1和expr2可以是同类型的任意值,也可以是表达式。类型不被局限于常量或整数,但必须是相同的类型,或者最终结果为相同类型的表达式,然后另外前花括号{必须和switch关键字在同一行。
1 | import "fmt" |
经过输出你会发现,条件满足第一个case,故输出20 - x = 10
,这看起来很正常,但你仔细一看,实际上你会发现当x=10的时候,case 10
和case 20 - x
都满足条件,但是只输出了20 - x = 10
,只是为什么?
因为Go语言使用快速的查找算法从上往下依次来测试switch条件与case分支的匹配情况,直到算法匹配到某个case或者进入default条件为止,一旦成功地匹配到某个分支,在执行完相应代码后就会退出整个switch代码块,也就是说您不需要特别使用break语句来表示结束。这就导致同样满足条件的
case 10
的条件并没有执行到,所以case分支应该是唯一且互斥的,从而避免这样的情况发生。
另外需要注意的是:多个相同条件的case一定不能是常量,否则会发生编译错误:
1 | import "fmt" |
该例中,如果case中无论是10还是经过计算表达式20 - 10得到的10都是相同条件的case,这样编译器会直接报重复的case 10
,除非像上例中的case 20 - x
这样, 在编译期间,编译器无法判断20 - x = 10
一定成立,只有在运行期间才会知道,所以编译期间并不会编译错误,但是即便如此,我们也应该避免写出这样的代码。
4.2 case多个条件
另外同一个case可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:
1 | import "fmt" |
4.3 switch初始化变量
和if一样,swith后也是可以初始化变量,然后再进行判断,例如:
1 | import "fmt" |
4.4 fallthrough关键字
如果想要执行完第一个case后,继续执行下一个case,我们可以使用fallthrough
关键字,例如:
1 | import "fmt" |
该示例很好演示了fallthrough
关键字的作用,但你仔细发现x=10并不满足case 15
的条件,但是仍然会执行,这是为什么?
注意: 程序执行到fallthrough的时候,无论下一个case是否满足条件,都会直接执行case后的代码。
4.5 switch表达式
当switch跟一个表达式的时候,表达式返回的类型也要和case的类型保持一致,例如:
1 | import "fmt" |
5 循环语句
与多数语言不同的是,Go语言中的循环语句只支持for和range关键字,而不支持while
和do-while
结构。关键字for的基本使用方法与C和C++中非常接近。
5.1 基于数值的for循环
基于数值的for循环基本语法如下:
1 | for expression1; expression2; expression3 { |
其中expression1
、 expression2
和expression3
都是表达式,其中expression1
和expression3
是变
量声明或者函数调用返回值之类的,expression2
是用来条件判断,expression1
在循环开始之前调用,expression3
在每轮循环结束之时调用。
使用for循环需要注意两点:
- 同if判断一样,条件语句不需要使用括号将条件包含起来()
- 左花括号必须和for处同一行,否则编译错误
- Go语言的for循环同样支持continue和break来控制循环,但是它提供了一个更高级的break,可以选择中断哪一个循环。
让我们来看一个例子:
1 | import "fmt" |
5.2 基于条件的for循环
for循环的第二种形式是没有头部的条件判断迭代(类似其它语言中的while
循环),基本语法如下:
1 | for condition { |
您可以认为这是没有初始化语句和修饰语句的for结构,因此;;
便是多余的。
1 | import "fmt" |
5.3 无限for循环
条件语句是可以被省略的,如i:=0; ; i++
或for { }
或for ;; { }
其中;;
会在使用gofmt
时被移除,这些循环的本质就是无限循环,最后一个形式也可以被改写为for true { }
,但一般情况下都会直接写for { }
。
如果for循环的头部没有条件语句,那么就会认为条件永远为true,因此循环体内必须有相关的条件判断以确保会在某个时刻退出循环。
1 | import "fmt" |
5.4 for-range循环
这是Golang特有的一种的迭代结构,您会发现它在许多情况下都非常有用。它可以迭代任何一个集合(包括数组、切片和map)。语法上很类似其它语言中foreach
语句,但您依旧可以获得每次迭代所对应的索引,当迭代map时没有索引idx,取而代之的时key,一般语法格式如下:
1 | for idx|key, value := range collection |
要注意的是,value
始终为集合中对应索引的值拷贝,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值(译者注:如果val为指针,则会产生指针的拷贝,依旧可以修改集合中的原值),一个字符串是Unicode
编码的字符(或称之为rune
)集合,因此您也可以用它迭代字符串。
1 | import "fmt" |
6 跳转语句
for、switch或select语句都可以配合标签[label]形式的标识符使用,即某一行第一个以冒号 : 结尾的单词,其中标签标签的名称是大小写敏感的,为了提升可读性,一般建议使用全部大写字母。
6.1 标签定义后必须使用
标签定义了就必须要使用,定义但未使用标签会导致编译错误,例如:
1 | import "fmt" |
6.2 goto语句和标签之间不能定义其他变量
当goto后面的标签出现的位置在goto语句之后时,两者之间不能有新的变量定义,否则将导致编译错误,例如:
1 | import "fmt" |
6.3 goto语句与标签
1 | import "fmt" |
6.4 continue语句与标签
continue语句也可以和标签配置使用,我们知道continue语句只能在循环中使用,所以结合continue结合标签也只能在循环中使用,例如:
1 | import "fmt" |
该示例展示了continue LABEL
的使用,根据输出结果来看,该示例是否使用标签结果都是一样,原因是continue本身功能就是退出本次循环,从下一次开始,而LABEL标签恰好在下一次的执行位置,所以要不要标签上面的结果都是一样。
接下来我们把标签LABEL移到外层循环呢?
1 | import "fmt" |
根据结果我们看到,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 | import "fmt" |
该示例展示了break LABEL
的使用,根据输出结果来看,该示例是否使用标签结果都是一样,原因是break本身功能就是退出本层循环,从外层循环开始,而LABEL标签加在内层循环毫无意义,根本不会执行,所以要不要标签上面的结果都是一样。
接下来我们把标签LABEL移到外层循环呢?
1 | import "fmt" |
根据结果我们看到,break LABEL
执行完成后,不仅会退出内层循环,同时也会退出外层循环,即标签在哪一层就会退出到哪一层。
6.6 goto注意事项
避免滥用:在绝大多数情况下,可以通过更好的代码结构和控制流语句来代替goto。只在极少数情况下,例如处理错误跳转等,才会考虑使用 goto。
跳转范围:
goto
只能在同一个函数内部进行跳转,不能跨越函数。不同分支中的标签:标签不能在不同的分支之间重复使用,例如一个标签不能同时在
if
和switch
语句中。避免形成死循环:使用
goto
时要小心,确保不会形成死循环,否则会导致程序无法正常退出。
特别注意:使用标签和
goto
语句是不被鼓励的,因为它们会很快导致非常糟糕的程序设计,而且总有更加可读的替代方
案来实现相同的需求。
7 总结
到此相信你已经了解了流程控制,流程控制是编程之路上的重要一步,Golang提供了简单而强大的工具,使得我们能够清晰地表达程序的逻辑,并以高效的方式实现所需的功能。通过深入理解和灵活运用流程控制,您将能够编写出可读性强、可维护性高的Golang代码,下一章节我们将介绍Package
包。