流程控制是Go语言中必不可少的一部分,也是整个编程基础的重要一环。Go语言的流程控制语句和其他编程语言的流程控制语句有些不同,主要体现在Go语言没有do-while语句。Go语言常用的流程控制包括if语句、switch语句、for语句及goto语句等,switch语句和goto语句主要是为了简化代码、降低代码重复率,属于扩展类的流程控制语句。
1、条件判断
在Go语言中,if语句主要用于条件判断。if语句还有两个分支结构:if-else语句和else-if语句
a. if 单分支
在Go语言中,关键字if是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行if后由大括号“{}”括起来的代码块,否则就忽略该代码块继续执行后续的代码。
if 条件表达式 { 代码块
}
使用if语句判断一个变量的大小:
func main() {/*定义局部变量*/var a int = 20if a < 30 {fmt.Printf("a 小于 30\n")}fmt.Printf("a 的值为:%d\n", a)
}
b. if-else 双分支
if语句后可以使用可选的else语句,else语句中的表达式在条件表达式为false时执行。if和else后的两个代码块是相互独立的分支,只能执行其中一个。
if 条件表达式 { 代码块1
} else { 代码块2
}
func main() {/*定义局部变量*/var a int = 50if a < 30 {fmt.Printf("a 小于 30\n")} else {fmt.Printf("a 不小于 30\n")}fmt.Printf("a 的值为:%d\n", a)
}
c. if-else-if 多分支
if 条件表达式 { 代码块1
} else if 条件表达式 { 代码块2
} else {代码块3
}
func main() {/*定义局部变量*/var a int = 15if a < 10 {fmt.Printf("a 小于 10\n")} else if a > 20 {fmt.Printf("a 大于 20\n")} else {fmt.Printf("a 大于 10\n")fmt.Printf("a 小于 20\n")}fmt.Printf("a 的值为:%d\n", a)
}
d. if 语句的注意事项
(1)if后面的条件判断子句不需要使用小括号括起来,例如,if a > 30。
(2)Go语言规定,与if匹配的“{”必须与if和表达式放在同一行,如果尝试将“{”放在其他位置,将会出发编译错误。与else匹配的“{”也必须与else在同一行,else也必须与上一个if或else if的右边的大括号在一行。
(3)if后面可以带一个简单的初始化语句,并以分号进行分隔,该简单语句声明的变量的作用域是整个if语句块,包括后面的else if和else分支。
(4)Go语言没有条件运算符(a>b?a:b),符合Go语言的设计理念,只提供一种方法做事情。
(5)if分支语句如果遇到return,则直接返回
2、选择结构
switch语句和select语句在Go语言中主要用于条件的选择。相比C语言,Go语言中的switch语句在结构上更加灵活,语法设计尽量以使用方便为主。
a. switch 语句
在Go语言中,switch表示选择语句的关键字,switch语句会根据初始化表达式得出一个值,然后根据case语句的条件,执行相应的代码块,最终返回特定内容。每个case被称为一种情况,只有当初始化语句的值符合case的条件语句时,case才会被执行。
如果没有遇到符合的case,则可以使用默认的case (default case),如果已经遇到了符合的case,那么后面的case都不会被执行。
Go语言改进了switch的语法设计,case与case之间是独立的代码块,不需要通过break语句跳出当前case代码块以避免执行到下一行
switch (表达式) {case常数1:代码块1;case常数2:代码块2;……default:代码块n+1;
}
func main() {/*定义局部变量*/a := "hello"switch a {case "hello":fmt.Println("hello")case "world":fmt.Println("world")default:fmt.Println("hello world!!!")}
}
与其他编程语言不同的是,在Go语言编程中,switch有两种类型。
(1)表达式switch:在表达式switch中,case包含与switch表达式的值进行比较的表达式。
(2)类型switch:在类型switch中,case包含与特殊注释的switch表达式的类型进行比较的类型。
表达式switch:
func main() {grade := "E"marks := 95switch {case marks >= 90:grade = "A"case marks >= 80:grade = "B"case marks >= 70:grade = "C"case marks >= 60:grade = "D"default:grade = "E"}switch {case grade == "A":fmt.Println("成绩优秀!")case grade == "B":fmt.Println("表现良好!")case grade == "C", grade == "D":fmt.Println("再接再厉!")default:fmt.Println("成绩不合格!")}fmt.Println("你的成绩为:", grade)
}
类型switch:
类型switch语句针对变量的类型判断该执行哪个case代码块
func main() {x = 1switch i := x.(type) {case nil:fmt.Println("这里是nil,x的类型是%T", i)case int:fmt.Println("这里是int,x的类型是%T", i)case float64:fmt.Println("这里是float64,x的类型是%T", i)case bool:fmt.Println("这里是bool,x的类型是%T", i)case string:fmt.Println("这里是string,x的类型是%T", i)default:fmt.Println("未知类型!")}
}
switch的特点如下:
- (1)switch和if语句一样,switch后面可以带一个可选的简单的初始化语句。
- (2)switch后面的表达式也是可选的,如果没有表达式,则case子句是一个布尔表达式,而不是一个值,此时就相当于多重if-else语句。
- (3)switch条件表达式的值不像C语言那样必须限制为整数,可以是任意支持相等比较运算的类型变量。
- (4)通过fallthough语句来强制执行下一个case子句(不再判断下一个case子句的条件是否满足)。
- (5)switch支持default语句,当所有的case分支都不符合时,执行default语句,并且default语句可以放到任意位置,并不影响switch的判断逻辑。
- (6)switch和.(type)结合可以进行类型的查询。
b. select 语句
在Go语言中,除了switch语句外,还有一种选择结构——select。select语句可以用于配合通道(channel)的读/写操作,用于多个channel的并发读/写操作。
select语句类似于switch语句,switch语句是按照顺序从上到下依次执行,而select是随机选择一个case执行。如果没有case可运行,它将阻塞,直到有case可运行。
select {case: 代码块1; case: 代码块2; default : /*可选*/ 代码块n;
}
- 每个case都必须是一个通信。
- 所有channel表达式都会被求值。
- 所有被发送的表达式都会被求值。
- 如果任意某个通信可以进行,它就执行,其他被忽略。
- 如果有多个case都可以运行,select会随机公平地选出一个执行,其他不会执行。
- 如果没有可执行的case,但是有default子句,则执行该语句。
- 如果没有可执行的case,并且也没有default子句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。
func main() {a := make(chan int, 1024)b := make(chan int, 1024)for i := 0; i < 10; i++ {fmt.Printf("第%d次,", i)a <- 1b <- 1select {case <-a:fmt.Println("from a")case <-b:fmt.Println("from b")default:fmt.Println("from c")}}
}
在以上代码中,同时在a和b中进行选择,哪个有内容就从哪个读,由于channel的读/写操作是阻塞操作,使用select语句可以避免单个channel的阻塞。此外,select同样可以使用default代码块,避免所有channel同时阻塞。
3、循环结构
在Go语言中,循环语句的关键字是for,没有while关键字。for语句可以根据指定的条件重复执行其内部的代码块,这个判断条件一般是由for关键字后面的子语句给出的。
a. for 语句
for循环是一个循环控制结构,可以执行指定次数的循环。循环体不停地进行循环,直到循环终止条件返回false时自动退出循环,执行for的“}”之后的语句。
for 循环控制变量初始化; 循环终止条件; 循环控制变量增量 { 循环体
}
初始语句是在第一次循环前执行的语句,一般使用初始语句执行变量初始化,如果变量在此处被声明,其作用域将被局限在这个for的范围内。
初始语句可以被忽略,但是初始语句之后的分号必须写,例如:
step := 2
for ; step > 0; step-- { fmt.Println(step)
}
注意事项:
- 左花括号“{”必须与for处于同一行。
- Go语言中的for循环与C语言一样,都允许在循环条件中定义和初始化变量,唯一的区别是,Go语言不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量。
- Go语言的for循环同样支持continue和break来控制循环,但是它提供了一个更高级的break,可以选择中断具体的哪一个循环。
b. range 语句
每一个for语句都可以使用一个特殊的range子语句,其作用类似于迭代器,用于轮询数组或者切片值中的每一个元素,也可以用于轮询字符串的每一个字符,以及字典值中的每个键值对,甚至还可以持续读取一个通道类型值中的元素。
range关键字的右边是range表达式,表达式一般写在for语句的前面,以便提高代码的易读性。
range关键字的左边表示的是一对索引-值对,根据不同的表达式,返回不同的结果,range右边表达式返回的类型如下图:
range右边表达式返回的类型,除了轮询字符串外,还包括数组、切片、字典及通道等。
func main() {numbers := [5]int{1, 2, 3, 4}for i, x := range numbers {fmt.Printf("第%d次,x的值为%d\n", i, x)}
}
在以上代码中,定义了numbers的长度为5,但numbers中只有4个值,因此最后一个为空值,从for循环返回的信息可以看到第5次x的值为0,代码块的确执行了5次。
4、defer 延迟
Go语言除了传统的流程控制语句外,还有一些特殊的控制语句,defer就是其中之一。defer主要用于延迟调用指定的函数,defer关键字只能出现在函数的内部:
func main() {defer fmt.Printf("world!")fmt.Printf("Hello ")
}
在以上代码中会首先打印hello,然后打印world,因为第一句使用了defer关键字,defer语句会在函数最后执行,defer后面的表达式必须是外部函数的调用,上面的例子就是针对fmt.Println函数的延迟调用。
defer有如下两大特点:
(1)只有当defer语句全部执行,defer所在函数才算真正结束执行。
(2)当函数中有defer语句时,需要等待所有defer语句执行完毕,才会执行return语句。
因为defer的延迟特点,可以把defer语句用于回收资源、清理收尾等工作。使用defer语句之后,不用纠结回收代码放在哪里,反正都是最后执行。
需要注意defer的执行时机:
var i = 0func print() {fmt.Println(i)
}func main() {for ; i < 5; i++ {defer print()}
}
- 在以上代码中,返回了5个5,这是因为每个defer都是在函数轮询之后才执行,此时i的值为5。
var i = 0func print(i int) {fmt.Println(i)
}func main() {for ; i < 5; i++ {defer print(i)}
}
- 当i等于0时,defer语句第一次被压栈,此时defer后面的函数返回0;i不断自增,一直到i等于4时,defer语句第5次入栈,defer后的函数返回4。
- Go语言会根据defer后进先出原则逐条打印栈内的数值,因此就看到了现在的结果。
5、标签
在Go语言中,有一个特殊的概念就是标签,可以给for、switch或select等流程控制代码块打上一个标签,配合标签标识符可以方便跳转到某一个地方继续执行,有助于提高编程效率。
标签的名称区分大小写,为了提高代码的易读性,建议标签名称使用大写字母和数字。标签可以标记任何语句,并不限定于流程控制语句,未使用的标签会引发错误。
a. break 语句
Go语言中的break语句主要用于以下两方面:
- 用于循环语句中跳出循环,并开始执行循环之后的语句。
- break可以使switch语句执行一条case后跳出语句。
在多重循环中,可以用标号label标出想跳出的指定循环:
func main() {OuterLoop:for i := 0; i < 2; i++ {for j := 0; j < 5; j++ {switch j {case 2:fmt.Println(i, j)break OuterLoopcase 3:fmt.Println(i, j)break OuterLoop}}}fmt.Println("调出循环")
}
- 退出OuterLoop对应的循环之外,也就是跳转到最后。
b. continue 语句
Go语言中的continue语句可以结束当前循环,开始下一次的循环迭代过程,仅限在for循环内使用,在continue语句后添加标签时,表示开始标签对应的循环:
func main() {
OuterLoop:for i := 0; i < 2; i++ {for j := 0; j < 5; j++ {switch j {case 2:fmt.Println(i, j)continue OuterLoopcase 3:fmt.Println(i, j)continue OuterLoop}}}fmt.Println("调出循环")
}
- 第8行将结束当前循环,开启下一次外层循环,而不是第4行的循环。与break不同的是,continue表示跳转后继续执行操作。
c. goto 语句
Go语言中的goto语句通过标签进行代码间的无条件跳转,同时goto语句在快速跳出循环、避免重复退出上也有一定的作用,使用goto语句能简化一些代码的实现过程。
goto语句通常与条件语句配合使用,可用来实现条件转移、构成循环、跳出循环体等功能。但是,在结构化程序设计中一般不主张使用goto语句,以免造成程序流程的混乱,使理解和调试程序都产生困难。
goto语句有以下几个特点:
- goto语句只能在函数内跳转。
- goto语句不能跳过内部变量声明语句,这些变量在goto语句的标签语句处又是可见的。
- goto语句只能跳到同级作用域或者上层作用域内,不能跳到内部作用域内。
使用goto退出多层循环:
func main() {for x := 0; x < 10; x++ {for y := 0; y < 10; y++ {if y == 2 {// 跳转到标签goto breakHere}}}// 手动返回,避免执行进入标签return// 标签breakHere:fmt.Println("done")
}
- 第11行,标签只能被goto使用,但不影响代码执行流程,此处如果不手动返回,在不满足条件时,也会执行第14行代码。