这一期介绍 程序的流程控制,下一期开始引入实战项目:一开始不能太贪心,会先尝试做一个 CLI 工具。
控制结构
程序有三种基本的控制结构,分别是 顺序、分支(选择) 和 循环 。其中顺序结构不需要任何特殊的语句,程序语句按顺序执行即可。需要控制语句的是剩下两种结构。
注意,Go 语言的代码块不能省略大括号({}
,又叫花括号),哪怕只有一行。这是为了保持语言的可伸缩性,使得语言在不同规模和不同的上下文都不会出现歧义。与之相反,条件表达式处于关键字和左大括号 {
中间,而且总是只有一行,所以不需要加括号 ()
。
无论是分支结构还是循环结构,都需要引入条件表达式,来控制执行路径。由于 Go 取消掉了一些操作的值,使得它们从表达式变成了语句,导致相应的操作无法直接加入条件表达式。估计是因为这样,Go 允许在条件表达式前增加一个简单语句,中间用分号 ;
隔开,也就是这样 简单语句;条件表达式
。注意,只能是一句,如果是更复杂的效果,还是应该在进入控制结构之前,通过多个语句先完成操作。
其中简单语句可以是:
- 空白。
- 表达式。
- 通道(
chan
)的发送语句。 - 自增自减。
- 赋值。
- 变量的短声明。
变量短声明很可能是用得最多的语句,因为在这里声明变量可以限制变量的作用域,仅限于控制结构内部,但需要注意变量遮盖(shadow)的问题:
|
|
1. 分支结构
分支结构会指定一个到多个条件,通过测试哪些条件得到满足来决定执行路径。Go 里面可以实现分支结构的语句有:
if 语句
Go 的 if 语句跟其它语言差别不大,直接上例子。
细节差别有:表达式不用括号,代码块不能省略大括号,else if
不像一些语言可以写成 elif
(Go 很省关键字)。
|
|
switch 语句
只有一两个判断条件时, if 语句是很好的选择;但当分支多了起来,大量的 else if
会导致代码冗长又难以维护。这时 switch 语句是更好的选择。
可能是为了省关键字,switch 实际上有两个用法,分别是表达式 switch,和类型 switch。无论哪一种,switch 语句都会从上到下逐个测试 case,执行第一个满足的分支。
表达式 switch:
|
|
类型 switch:
类型 switch 涉及类型断言。
Go 的接口变量允许储存任何满足接口的类型的值,举例说类型 T
满足接口 I
,那么 T
的值 t
就可以赋值给 I
的变量 i
:var i I = t
。
而将接口变量 i
转换为具体的类型 T
时,除了显式的类型转换 t = T(i)
,还可以用 类型断言 t = i.(T)
。两者之间一个明显的差别是,类型转换需要开发者自行确保可以转换,否则就会产生一个运行时 panic;而类型断言有安全形式 t, ok = i.(T)
,如果转换失败,ok
的值为 false
,并不会产生 panic。不过这里不是讨论类型断言,不详细展开。
类型 switch 借用了类型断言的形式,但是括号里的不是某个具体的类型,而是关键字 type
,也就是 i.(type)
;然后 case 后面接的,就不再是值,而是具体的类型。
|
|
与 C 风格 switch 的差别
所以总结下来,switch 语句实际上包含了
- C 风格的 switch-case 经典用法。
- scheme 风格的 switch-cond 用法(但是子句还是用
case
关键字)。 - 类型断言用法。
不熟悉的话,还挺容易混淆的。
除此之外,虽然关键字和结构整体都是沿袭 C 风格的 switch 语句,细节上还是有几个比较重要的差异:
默认 break:
C 风格的 switch,case 之间的默认行为是 fallthrough。换言之,命中的 case 只是一个入口,如果没有遇到 break 语句,接下去的每个 case 都会执行,直到结束。
但实际上,需要 fallthrough 的情况非常少,大多数情况下,我们都只是希望只执行命中的 case,这就导致 case 和 break 总是成对出现,非常啰嗦。
Go 将 switch 的默认行为改为 break,需要 fallthrough 时,再在 case 的结尾显示写一个
fallthrough
。注意类型 switch 不允许 fallthrough。case 不限定为常量值:
C 风格的 case 后面,只能接一个 常量或字面量的值。Go 无此限制。在经典用法中,值可以是常量,也可以是变量或者表达式。
case 后面允许接多个条件:
因为 C 风格 switch 默认 fallthrough,当多个条件共享相同的代码时,只要将多个 case 顺序写在一起,代码放在最后一个 case,就可以共享代码逻辑。
下面看一下例子:
|
|
Go 的默认行为变成了 break,不能这样写了;但是多个条件共享代码反而更容易了,因为 case 后面可以接多个条件:
|
|
例子中的任意一种写法,都可以;但是三种写法不能混用。
select 语句
select 语句可以说是通信版的 switch,也可以说是借鉴了 C 语言 select 函数的含义(虽然读写的对象不同),并且升格成了关键字:
|
|
select 语句里的每个 case 后面都接一个通信操作,要么接收要么发送。
执行时,会在可以执行的 case 中 随机 执行一个(注意是随机,switch 则是从上到下测试第一个满足的),其它 case 忽略。如果没有可以执行的 case,则会执行 default 子句;没有可执行的 case 且没有 default 子句,则会阻塞直到有可以执行的 case 为止。所以如果不想阻塞,就一定要增加 default 子句。
select 语句常常嵌套在循环结构里,实现对通道轮询的效果。详细用法在介绍并发和通道时再展开。
2. 循环结构
在 Go 里面实现循环结构,只有一个 for 语句。看起来这又是省关键字的结果。Go 的不同写法,可以实现 C 风格里的 for、while 和 do-while ,甚至还有 Java 的 for-each 或 Python 的 for-range 效果:
|
|
range 表达式的右边,可以是 数组、数组的指针、切片(slice)、字符串(string
)、映射(map
)或者可接收(可读)的通道(chan
)。换句话说,它们的成员元素可以枚举。
range 表达式本质上是一个语法糖,实际上底层实现还是展开成普通的 for 循环,帮你枚举对象里的成员元素。对于通道以外的类型而言,展开后的三个部分可以近似看作:取第一个元素(初始化);没遍历到最后一个元素(循环条件);取下一个元素(步进)。而对于通道而言,循环条件变成了通道还没关闭。
详细用法,具体在每个类型里介绍。这里只提醒两点:
range 表达式获得的变量都是拷贝,对变量的修改不会影响集合中原来的值。(这个的原理会专门找一期介绍值和引用)如果想修改集合里的值,还是要老老实实地遍历下标(或者 map 的 key),然后直接修改索引值
s[i] = newVal
。多数的 range 表达式可以返回不止一个值。你既可两个值都赋值给变量使用,也可以只要其中一个;丢弃第二个值可以直接忽略
for i := range s {...}
,而丢弃第一个值则必须显式赋值给空白标识符for _, v := range s {...}
。| 类型 | 类型定义 | 第一个值 | 第二个值 |
| ————————– | ——————- | ——————– | ———————————- |
| 数组、数组指针 或 切片a
|[n]E, *[n]E, []E
| 下标i
,类型int
| 下标对应的元素a[i]
,类型E
|
| 字符串s
|string
| 下标i
,类型int
| 下标对应的字节s[i]
,类型byte
|
| 映射m
|map[K]V
| 键值k
,类型K
| 键值对应的元素m[k]
,类型V
|
| 通道c
|chan E, <- chan E
| 元素e
,类型E
| - |
无论是哪一种写法,都可以看作是经典写法的变体(省略了某些部分),所以执行流程都是相同的:
- 执行初始化(init)语句;
- 判断循环条件(condition),满足则进入循环,否则退出(循环条件省略等同于 true);
- 执行循环体;
- 执行步进(post)语句,并回到 2。
3. 控制语句
控制语句主要针对循环结构,除了按照 for 语句的规则执行,还可以加入一些控制语句,改变执行的方向。
分别有 break
、continue
和 goto
三种语句。这三种语句的用法跟 C 家族语言基本一致:
break
跳出当前的循环,并继续执行循环之后的语句。break
也可以用于跳出 switch 代码块,但由于 Go 的 switch 默认会在 case 结尾退出,所以 switch 里break
用得比较少。注意有多层循环嵌套时,
break
只会跳出当前所在的内层循环。如果想跳出在外层循环,需要在跳出的循环前面加标签(label),
break
后面加上标签作为目标,就会跳出对应的循环。continue
中止当前循环的执行,改为执行步进语句,(在同一个循环结构内)开始执行下一次循环。跟
break
类似,有多层循环嵌套时,continue
只会影响当前所在的内层循环。如果想开始外层的下一次循环,同样可以用循环前的标签作为
continue
的目标。goto
必须配合标签 (label) 使用。goto
语句会无条件跳转到(同一个函数内部) 标签 声明的位置继续执行。goto
的使用不限于某种控制结构。实际上,靠 if-else +goto
可以实现任意的控制结构。goto
用好了可以用很少的代码实现复杂的逻辑控制;但是用不好的话,会导致执行流程混乱,造成理解和调试困难。结构化程序设计一般不鼓励使用goto
,多数后来的高级语言也都不提供goto
语句。
标签
标签(label)本质上就是一个 标识符,只是它指向的不是一个常量或变量,而是一个程序的位置(指向声明位置下一行代码);有效作用域总是为同一个函数体,不受嵌套的代码块限制(标签是 Go 里面唯一一种永远都是函数级作用域的标识符)。标签不占用常量和变量的命名,允许标签跟其它标识符重名,但强烈建议不要重名。
标签声明时单独一行,后面接一个冒号 LABEL:
;使用时作为 break
、continue
和 goto
的目标。为了区别于其它标识符,标签一般全大写。
goto
的目标标签可以声明在函数内的任意地方,跳转的限制只有两条:
- 不能跨函数跳转,包括函数内嵌套的函数和闭包。
- 不能(向后)跳过变量声明(可以跳过常量声明)。因为跳过的变量后续引用到会报错。修改的方法是把变量声明提前到跳转区间以外。
相对应地,作为 break
和 continue
目标的标签多一个限制:标签必须声明在外层某个循环前面(相当于指向该循环),用来表明要跳出或者继续的是哪个循环。
|
|
写在后面
写教程可以深入细节,一直到内部的原理。也可以不多解释直接模仿着跑起来,多见几个例子再回头解释。
前者似乎所有人都可以看,新手打基础,老手查漏补缺。但新手可能还没建立起直观的认识,一上来就太多细节,也许就懵了。这个系列既然起了『实战』之名,按理说应该接近后者。直接来几个实例,先跑起来,再介绍里面分别是什么。有 C 家族语言经验的朋友,甚至都不需要解释,从例子里大概就能体会差异,可以用来干些简单的活。
写到第四期,似乎越来越理论和细节了。这并不是我的本意。
这样固然是满足了一部分我自己深入了解 Go 的需要;另一方面,也是希望可以迁就一部分缺少 C 家族语言经验的读者。(是否有没有编程经验的读者呢?因为缺乏来自你们的反馈,读者的面目在我这其实是模糊的。)这就需要在第一个『实战项目』之前先铺垫一些『基础知识』。虽说是基础,凡是涉及的主题,我都希望写透一些,不太愿意把一个主题拆得零碎,先笼统过一遍,下次展开一点,后面再来深入。
但这样下去,与其看我啰嗦,还不如直接看 Go 官方的语言规范来得简洁清晰。虽然我不断强调大家可以当参考资料跳着看;但是从详尽的资料中筛选关键信息不正是我该做的事吗。幸好写到这一期,开发一个最基本的程序所需要的知识勉强是够了,所以下一期赶紧开始进入一个实战程序吧。
开发中肯定还会遇到没介绍过的内容,等到毫无遗漏介绍完再开始是不现实的,这时提问答疑和群内互助就足以解决问题了。开发中遇到的问题,后面正好着重介绍。
这里给出 Go 语言官方的规范文档,大家也可以自行查阅。
官网:https://golang.org/ref/spec
官方提供给国内访问的镜像:https://golang.google.cn/ref/spec
本文为本人原创,采用知识共享 “署名-非商业性使用-相同方式共享” 4.0 (CC BY-NC-SA 4.0)”许可协议进行许可。
本作品可自由复制、传播及基于本作品进行演绎创作。如有以上需要,请留言告知,在文章开头明显位置加上署名(Jayce Chant)、原链接及许可协议信息,并明确指出修改(如有),不得用于商业用途。谢谢合作。
请点击查看协议的中文摘要。