Go 控制结构
if-else
结构
if 是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号括起来的代码块,否则就忽略该代码块继续执行后续的代码。
1 | if condition { |
如果存在第二个分支,则可以在上面代码的基础上添加 else 关键字以及另一代码块,这个代码块中的代码只有在条件不满足时才会执行。if 和 else 后的两个代码块是相互独立的分支,只可能执行其中一个。
1 | if condition { |
如果存在第三个分支,则可以使用下面这种三个独立分支的形式:
1 | if condition1 { |
else-if 分支的数量是没有限制的,但是为了代码的可读性,还是不要在 if 后面加入太多的 else-if 结构。如果你必须使用这种形式,则把尽可能先满足的条件放在前面。
即使当代码块之间只有一条语句时,大括号也不可被省略 (尽管有些人并不赞成,但这还是符合了软件工程原则的主流做法)。
关键字 if 和 else 之后的左大括号 {
必须和关键字在同一行,如果你使用了 else-if 结构,则前段代码块的右大括号 }
必须和 else-if 关键字在同一行。这两条规则都是被编译器强制规定的。
非法的 Go 代码:
1 | if x{ |
要注意的是,在你使用 gofmt
格式化代码之后,每个分支内的代码都会缩进 4 个或 8 个空格,或者是 1 个 tab,并且右大括号与对应的 if 关键字垂直对齐。
在有些情况下,条件语句两侧的括号是可以被省略的;当条件比较复杂时,则可以使用括号让代码更易读。条件允许是符合条件,需使用 &&、|| 或!,你可以使用括号来提升某个表达式的运算优先级,并提高代码的可读性。
一种可能用到条件语句的场景是测试变量的值,在不同的情况执行不同的语句,不过将在第 5.3 节讲到的 switch 结构会更适合这种情况。
1 | // booleans.go |
输出:
1 | The value is true |
注意事项 这里不需要使用 if bool1 == true
来判断,因为 bool1
本身已经是一个布尔类型的值。
这种做法一般都用在测试 true
或者有利条件时,但你也可以使用取反 !
来判断值的相反结果,如:if !bool1
或者 if !(condition)
。后者的括号大多数情况下是必须的,如这种情况:if !(var1 == var2)
。
当 if 结构内有 break、continue、goto 或者 return 语句时,Go 代码的常见写法是省略 else 部分(另见第 5.2 节)。无论满足哪个条件都会返回 x 或者 y 时,一般使用以下写法:
1 | if condition { |
注意事项 不要同时在 if-else 结构的两个分支里都使用 return 语句,这将导致编译报错 function ends without a return statement
(你可以认为这是一个编译器的 Bug 或者特性)。( 译者注:该问题已经在 Go 1.1 中被修复或者说改进 )
这里举一些有用的例子:
判断一个字符串是否为空:
if str == "" { ... }
if len(str) == 0 {...}
判断运行 Go 程序的操作系统类型,这可以通过常量
runtime.GOOS
来判断 (第 2.2 节)。1
2
3
4
5if runtime.GOOS == "windows" {
. ..
} else { // Unix-like
. ..
}这段代码一般被放在 init () 函数中执行。这儿还有一段示例来演示如何根据操作系统来决定输入结束的提示:
1
2
3
4
5
6
7
8
9var prompt = "Enter a digit, e.g. 3 "+ "or %s to quit."
func init() {
if runtime.GOOS == "windows" {
prompt = fmt.Sprintf(prompt, "Ctrl+Z, Enter")
} else { //Unix-like
prompt = fmt.Sprintf(prompt, "Ctrl+D")
}
}函数
Abs()
用于返回一个整型数字的绝对值:1
2
3
4
5
6func Abs(x int) int {
if x < 0 {
return -x
}
return x
}isGreater
用于比较两个整型数字的大小:1
2
3
4
5
6func isGreater(x, y int) bool {
if x > y {
return true
}
return false
}
在第四种情况中,if 可以包含一个初始化语句(如:给一个变量赋值)。这种写法具有固定的格式(在初始化语句后方必须加上分号):
1 | if initialization; condition { |
例如:
1 | val := 10 |
你也可以这样写:
1 | if val := 10; val > max { |
但要注意的是,使用简短方式 :=
声明的变量的作用域只存在于 if 结构中(在 if 结构的大括号之间,如果使用 if-else 结构则在 else 代码块中变量也会存在)。如果变量在 if 结构之前就已经存在,那么在 if 结构中,该变量原来的值会被隐藏。最简单的解决方案就是不要在初始化语句中声明变量。
下面的代码片段展示了如何通过在初始化语句中获取函数 process()
的返回值,并在条件语句中作为判定条件来决定是否执行 if 结构中的代码:
1 | if value := process(data); value > max { |
测试多返回值函数的错误
Go 语言的函数经常使用两个返回值来表示执行是否成功:返回某个值以及 true 表示成功;返回零值(或 nil)和 false 表示失败(第 4.4 节)。当不使用 true 或 false 的时候,也可以使用一个 error 类型的变量来代替作为第二个返回值:成功执行的话,error 的值为 nil,否则就会包含相应的错误信息(Go 语言中的错误类型为 error: var err error
,我们将会在第 13 章进行更多地讨论)。这样一来,就很明显需要用一个 if 语句来测试执行结果;由于其符号的原因,这样的形式又称之为 comma,ok 模式(pattern)。
在第 4.7 节的程序 string_conversion.go
中,函数 strconv.Atoi
的作用是将一个字符串转换为一个整数。之前我们忽略了相关的错误检查:
1 | anInt, _ = strconv.Atoi(origStr) |
如果 origStr 不能被转换为整数,anInt 的值会变成 0 而 _
无视了错误,程序会继续运行。
这样做是非常不好的:程序应该在最接近的位置检查所有相关的错误,至少需要暗示用户有错误发生并对函数进行返回,甚至中断程序。
我们在第二个版本中对代码进行了改进:
1 | // string_conversion.go |
这是测试 err 变量是否包含一个真正的错误(if err != nil
)的习惯用法。如果确实存在错误,则会打印相应的错误信息然后通过 return 提前结束函数的执行。我们还可以使用携带返回值的 return 形式,例如 return err
。这样一来,函数的调用者就可以检查函数执行过程中是否存在错误了。
习惯用法
1 | value, err := pack1.Function1(param1) |
由于本例的函数调用者属于 main 函数,所以程序会直接停止运行。
如果我们想要在错误发生的同时终止程序的运行,我们可以使用 os
包的 Exit
函数:
习惯用法
1 | if err != nil { |
(此处的退出代码 1 可以使用外部脚本获取到)
有时候,你会发现这种习惯用法被连续重复地使用在某段代码中。
当没有错误发生时,代码继续运行就是唯一要做的事情,所以 if 语句块后面不需要使用 else 分支。
示例 2:我们尝试通过 os.Open
方法打开一个名为 name
的只读文件:
1 | f, err := os.Open(name) |
练习 5.1 尝试改写 string_conversion2.go 中的代码,要求使用 :=
方法来对 err 进行赋值,哪些地方可以被修改?
示例 3:可以将错误的获取放置在 if 语句的初始化部分:
习惯用法
1 | if err := file.Chmod(0664); err != nil { |
示例 4:或者将 ok-pattern 的获取放置在 if 语句的初始化部分,然后进行判断:
习惯用法
1 | if value, ok := readData(); ok { |
注意事项
如果您像下面一样,没有为多返回值的函数准备足够的变量来存放结果:
1 | func mySqrt(f float64) (v float64, ok bool) { |
您会得到一个编译错误:multiple-value mySqrt() in single-value context
。
正确的做法是:
1 | t, ok := mySqrt(25.0) |
注意事项 2
当您将字符串转换为整数时,且确定转换一定能够成功时,可以将 Atoi
函数进行一层忽略错误的封装:
1 | func atoi (s string) (n int) { |
实际上,fmt
包(第 4.4.3 节)最简单的打印函数也有 2 个返回值:
1 | count, err := fmt.Println(x) // number of bytes printed, nil or 0, error |
当打印到控制台时,可以将该函数返回的错误忽略;但当输出到文件流、网络流等具有不确定因素的输出对象时,应该始终检查是否有错误发生(另见练习 6.1b)。
switch
结构
相比较 C 和 Java 等其它语言而言,Go 语言中的 switch 结构使用上更加灵活。它接受任意形式的表达式:
1 | switch var1 { |
变量 var1
可以是任何类型,而 val1
和 val2
则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。前花括号 {
必须和 switch
关键字在同一行。
您可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1, val2, val3
。
每一个 case
分支都是唯一的,从上至下逐一测试,直到匹配为止。( Go 语言使用快速的查找算法来测试 switch
条件与 case
分支的匹配情况,直到算法匹配到某个 case
或者进入 default
条件为止。)
一旦成功地匹配到某个分支,在执行完相应代码后就会退出整个 switch
代码块,也就是说您不需要特别使用 break
语句来表示结束。
因此,程序也不会自动地去执行下一个分支的代码。如果在执行完每个分支的代码后,还希望继续执行后续分支的代码,可以使用 fallthrough
关键字来达到目的。
因此:
1 | switch i { |
并且:
1 | switch i { |
在 case ...:
语句之后,您不需要使用花括号将多行语句括起来,但您可以在分支中进行任意形式的编码。当代码块只有一行时,可以直接放置在 case
语句之后。
您同样可以使用 return
语句来提前结束代码块的执行。当您在 switch 语句块中使用 return
语句,并且您的函数是有返回值的,您还需要在 switch 之后添加相应的 return
语句以确保函数始终会返回。
可选的 default
分支可以出现在任何顺序,但最好将它放在最后。它的作用类似与 if-else
语句中的 else
,表示不符合任何已给出条件时,执行相关语句。
1 | // switch.go |
for
结构
如果想要重复执行某些语句,Go 语言中您只有 for 结构可以使用。不要小看它,这个 for 结构比其它语言中的更为灵活。
注意事项 其它许多语言中也没有发现和 do while 完全对等的 for 结构,可能是因为这种需求并不是那么强烈。
基于计数器的迭代
文件 for.go
中演示了最简单的基于计数器的迭代,基本形式为:
1 | for 初始化语句; 条件语句; 修饰语句 {} |
1 | // for.go |
输出:
1 | This is the 0 iteration |
您还可以在循环中同时使用多个计数器:
1 | for i, j := 0, N; i < j; i, j = i+1, j-1 {} |
这得益于 Go 语言具有的平行赋值的特性(可以查看第 7 章 string_reverse.go 中反转数组的示例)。
您可以将两个 for 循环嵌套起来:
1 | for i:=0; i<5; i++ { |
基于条件判断的迭代
for 结构的第二种形式是没有头部的条件判断迭代(类似其它语言中的 while 循环),基本形式为:for 条件语句 {}
。
您也可以认为这是没有初始化语句和修饰语句的 for 结构,因此 ;;
便是多余的了。
1 | // for.go |
输出:
1 | The variable i is now: 4 |
无限循环
条件语句是可以被省略的,如 i:=0; ; i++
或 for { }
或 for ;; { }
(;;
会在使用 gofmt 时被移除):这些循环的本质就是无限循环。最后一个形式也可以被改写为 for true { }
,但一般情况下都会直接写 for { }
。
想要直接退出循环体,可以使用 break 语句(第 5.5 节)或 return 语句直接返回(第 6.1 节)。
但这两者之间有所区别,break 只是退出当前的循环体,而 return 语句提前对函数进行返回,不会执行后续的代码。
无限循环的经典应用是服务器,用于不断等待和接受新的请求。
1 | for t, err = p.Token(); err == nil; t, err = p.Token() { |
for-range
结构
要注意的是,val
始终为集合中对应索引的值拷贝,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值(译者注:如果 val
为指针,则会产生指针的拷贝,依旧可以修改集合中的原值)。一个字符串是 Unicode 编码的字符(或称之为 rune
)集合,因此您也可以用它迭代字符串:
1 | for pos, char := range str { |
每个 rune 字符和索引在 for-range 循环中是一一对应的。它能够自动根据 UTF-8 规则识别 Unicode 编码的字符。
1 | // range_string.go |
输出:
1 | The length of str is: 27 |
break
与 continue
您可以使用 break
语句重写 for.go
的代码:
1 | // for.go |
下面的示例中包含了嵌套的循环体,break 只会退出最内层的循环:
1 | // for.go |
输出:
1 | 012345 012345 012345 |
关键字 continue 忽略剩余的循环体而直接进入下一次循环的过程,但不是无条件执行下一次循环,执行之前依旧需要满足循环的判断条件。
1 | // for.go |
输出:
1 | 0 1 2 3 4 6 7 8 9 |
另外,关键字 continue 只能被用于 for 循环中。
标签与 goto
for、switch 或 select 语句都可以配合标签(label)形式的标识符使用,即某一行第一个以冒号(:
)结尾的单词(gofmt
会将后续代码自动移至下一行)。
1 | // for.go |
您可以看到当 j==4
和j==5
的时候,没有任何输出:标签的作用对象为外部循环,因此i
会直接变成下一个循环的值,而此时 j 的值就被重设为 0,即它的初始值。如果将 continue
改为 break
,则不会只退出内层循环,而是直接退出外层循环了。另外,还可以使用 goto
语句和标签配合使用来模拟循环。
1 | // goto.go |
特别注意 使用标签和 goto 语句是不被鼓励的:它们会很快导致非常糟糕的程序设计,而且总有更加可读的替代方案来实现相同的需求。
如果您必须使用 goto,应当只使用正序的标签(标签位于 goto 语句之后),但注意标签和 goto 语句之间不能出现定义新变量的语句,否则会导致编译失败。
1 | // goto.go |