程序流程控制又叫做程序执行顺序。编程思想的体现就是程序流程控制的使用体现。所有的编程语言都主要有三大流程控制语句:
- 顺序控制
- 分支/条件控制
- 循环控制语句
且各个控制流程之间可相互嵌套。
事先需要注意以下几点:
- 流程控制语句:Go 编译器对
{
的位置有严格的要求。- 单分支条件判断语句:
if <condition> { //body }
,{
必须跟<condition>
同一行 - 多分支条件判断语句:
if <condition> {} elseif <condition> {} else {}
中{
必须跟<condition>
同一行- 但是
else
后面没有<condition>
可以不再同一行(建议还是写在同一行会好些)
- 单分支条件判断语句:
-
条件判断语句的短路作用:if-else、switch-case ==> 若条件判断需要满足多个条件可以使用 “&&” 或 “ ” ==> 多条件判断有短路作用 - 延迟执行的
defer
是一样关键字
顺序控制语句
顺序控制即:程序至上而下,逐行执行,中间没有任何的判断和跳转,那么该段程序就是顺序控制。顺序控制语句,也是程序默认的执行顺序。其流程图如下:
注意事项
- 顺序控制程序中,定义和使用变量时,采用合法的前向引用。即:使用变量时,总是使用同一层级或上一层级中,前方最靠近的哪一个变量。
package main
function main() {
// 正确的引用
var num1 int = 1
var num2 int = num1 + 1
// 错误的引用
var num4 int = num3 + 1
var num3 int = 3
}
分支控制
分支控制(又称为条件控制)即:依指定变量或表达式的结果,决定后续运行的程序。
单分支控制结构
单分支控制语句的流程图如下:
基本语法如下:
if 条件表达式 {
执行代码块
}
双分支控制结构
双分支控制语句的流程图如下:
基本语法如下:
if 条件表达式 {
执行代码块A
} else { // 注意 Golang 中 } { 是不能换行的
执行代码块B
}
如下代码示例:
// 条件判断语句
func judgeCondition(age uint8) {
/*
* 单分支条件判断语句:if <condition> { //body }
* !!! “{” 必须跟 <condition> 同一行
*/
if age >= 12 &&
age <= 18 {
fmt.Println("青少年")
}
/*
* 多分支判断语句
* !!! { 和 } 花括号必须在 else(else if)必须在同一行
*/
if age > 18 {
fmt.Println("已经成年了")
} else {
fmt.Println("还未成年")
}
if age < 1 {
fmt.Println("婴儿")
} else if age < 7 {
fmt.Println("幼儿")
} else {
fmt.Println("你已经不是个孩子了")
}
/*
* if 高级语法:在 if 里允许先运行一个表达式,取得变量后,再对其进行判断,比如第一个例子里代码也可以写成这样
*/
if myAge := 20; myAge > 18 {
fmt.Println("已经成年了")
}
}
多分支控制语句
多分支控制语句if...elseif...else
的流程图如下:
基本语法如下:
if 条件表达式 {
执行代码块A
} elseif 条件表达式 {
执行代码块B
} elseif 条件表达式 {
执行代码块C
} else {
执行代码块D
}
多分支控制语句switch...case
,先执行表达式expr
,得到值,然后和 case
表达式进行相等比较,如果匹配则执行对应语句块,然后退出。
- Golang 中
case
语句默认是可以省略break
语句,因为编译器会自动添加上 - 若 case 使用了关键词
fallthrough
开启穿透能力时,会直接执行下一个case的内容(不管与下一个case匹配结果如何,执行完都会退出,不会往后续的case进行判断)
其流程图如下:
基本语法如下:
switch 表达式 { // { 必须和 表达式 同一行
case 表达式1,表达式2,...: // 多个条件之间是 或(||) 的关系,用逗号相隔
语句块1
case 表达式3,表达式4,...: // 当 case 后接的是字面量常量时,该常量只能出现一次。如:case "man","man": 编译是会报错的 ==> duplicate case "man" in switch
语句2
... ... // 可多个 case 语句
default:
语句块
}
注意事项
- 即使执行代码块只有一行语句,`{}`也是必须有。如:Java/PHP 中,若
if
语句后只有一行代码,花括号是可以省略的。 - 分支控制流程中,只会有其中一个代码块会被执行
- 官方建议:分支控制语句中条件表达式不需要括号
- 条件表达式不能是赋值语句,其结果必须为布尔类型
switch/case
后是一个表达式(即:常量值、变量、一个有返回值的函数等都可以)case
后各个表达式的值的数据类型必须和switch
表达式的值的数据类型一致case
后可接多个表达式,使用逗号间隔即可- 若
case
后接的是一个常量值(字面量),则要求不能重复(编译不通过) switch...case
语句中,不需要 `break`语句,default
语句不是必须的switch
后也可以不带表达式,类似if..elseif...else
来执行
var score int = 90;
switch {
case score > 90:
fmt.Println("优秀")
case score >= 70:
fmt.Println("良好")
case score >= 60:
fmt.Println("合格")
default:
fmt.Println("不及格")
}
// 优秀
var str string
str = "hello"
switch {
case str == "hello":
fmt.Println("hello")
fallthrough // 穿透,下一个 case 就默认变为了 true
case str == "xxxx":
fmt.Println("xxxx")
break // 手动写上 break 也不会报错,但是没必要。不推荐
case str != "world":
fmt.Println("world")
default:
fmt.Println("worldsssss")
}
// hello
// xxxx
- 【不推荐】分支控制语句的条件表达式中可以定义局部变量,其作用域只局限于当前控制流程的可执行代码块
if age:20; age > 18 {
fmt.Println("成年了!", age); // age 只能在该作用域内使用
}
switch score := 90 {
case score > 90:
fmt.Println("优秀")
case score >= 70:
fmt.Println("良好")
case score >= 60:
fmt.Println("合格")
default:
fmt.Println("不及格")
}
循环结构
让一段代码块循环的执行,直到循环判断条件为 false
时,退出循环。
for
循环语句
语法如下:
for 循环变量初始化; 循环条件; 循环变量迭代 {
// 循环体
// 1. 循环变量初始化,在整个循环生命周期中只执行一次 ==> 该变量作用域只在循环体内
// 2. 当循环条件返回 true 继续循环,返回 false 结束循环
// 3. 每次“循环完”自动执行一次
}
或
// 循环变量初始化
for 循环条件 {
// 循环体
// 循环变量迭代
}
// 无限循环
for {
// 循环体
}
流程图如下:
注意事项:
- 循环条件是返回一个 布尔值 的表达式
- 上述两种
for
循环的循环变量初始化方式不同,其作用域也不同 - 无限循环:
for {}
等价于for ; ; {}
,通常需要配合break
语句 - 对于遍历数组/切片/字符串,可使用:
for key,value := range var {}
,类似 PHP 中的foreach(var as key => value) {}
、 JQuery 中的each()
进行遍历
package main
import "fmt"
function main() {
// 遍历数组
var arr = [3]int{1,2,3}
for index, value := range arr {
fmt.Printf("index = %d; value=%d \n",index, value)
}
// 遍历切片
var numList = []int{1,2}
for index, value := range numList{
fmt.Printf("index = %d; value=%d \n",index, value)
}
// 遍历字符串
var str string = "hello,中国!"
for i:=0; i < len(str); i++ {
fmt.Printf("%c", str[i]) // 中文乱码
}
fmt.Print("\n")
for _,value := range str {
fmt.Printf("%c", value)
}
}
- 对于字符串的遍历,若使用传统的
for
循环是按照字节来遍历,若字符串中含有中文(一个汉字对应的utf8编码是3个字节),则会出现乱码错误!而for...range
是按照字符方式进行遍历,因此有中文也不会乱码。
如果想使用传统方式 for
循环,需要对含有中文的字符串进行遍历,需要先将字符串转换为切片,就不会乱码
var str string = "hello,中国!"
str2 := []rune(str)
for i:=0; i < len(str2); i++ {
fmt.Printf("%c", str2[i])
}
- Go 语言中没有 `while` 和 `do...while` 循环语句,如果需要用到,那么请使用 `for`循环进行实现!【Go设计者的思想:一个问题尽量只有一个解决方法】
嵌套循环注意事项
- 一般不要使用嵌套循环,最多不要超过3层
- 外层循环次数为 m 次,内层循环次数为 n 次,整个循环的时间复杂度为 O(m*n)
练习
- 打印空心金字塔
// 首/底层全打印,其余2个*;行首空格:总层数-当前层数,内行空格:(2*n-1)-当前层*数
/*
*
* *
* *
*******
*/
bufio.NewReader(os.Stdin)
var level int;
fmt.Printf("输入金字塔层数:")
fmt.Scanln(&level)
for i := 1; i <= level; i++ {
// 打印行首空格
for j := 1; j <= (level - i); j++ {
fmt.Printf(" ")
}
// 打印 *
for k := 1; k <= 2*i - 1; k++ {
if k == 1 || k == 2 * i - 1 || i == level{
fmt.Printf("*")
} else {
fmt.Printf(" ")
}
}
fmt.Println()
}
- 打印空心菱形
package main
import (
"fmt"
"bufio"
"os"
)
function main() {
bufio.NewReader(os.Stdin)
var level int
for {
fmt.Printf("请输入一个奇数:")
_, _ = fmt.Scanln(&level)
if level % 2 == 1 {
break
}
}
middle := (level / 2) + 1 // 中位数
for i := 1; i <= level; i++ {
temp := i
if i > middle {
temp = 2 * middle - i
}
// 打印行首空格
for j := 1; j <= middle - temp; j++ {
fmt.Printf(" ")
}
// 打印 *
for k := 1; k <= 2 * temp -1; k++ {
if k == 1 || k == 2 * temp -1{
fmt.Printf("*")
} else {
fmt.Printf(" ")
}
}
fmt.Println()
}
}
跳转控制语句
break
语句
break
语句用于终止某个语句块的执行,用于中断循环控制语句和 多分支控制-switch
语句- 循环结构中
break
语句默认会跳出最近的一层循环体 - 在
golang
的循环结构中,break
语句不能像 PHP 中一样指定跳出的循环体层数,只能通过标签方式进行跳转
label2:
for i := 1; i <= 9; i++ {
for j := 1; j <= i; j++ {
fmt.Printf("%d * %d = %d \t",j, i, i*j)
// break 2 // 错误:不能指定跳出层级
break label2
}
fmt.Println("\n")
}
continue
语句
continue
语句用于循环结构中,表示结束本次循环,继续进行下一次循环- 在
golang
的循环结构中,continue
语句不能像 PHP 中一样指定跳转的循环体层数,只能通过标签方式进行跳转
here:
for i := 0; i < 2; i++ {
for j := 1; j < 4; j++ {
if j == 2 {
// continue 2 // 错误:不能指定跳出层级
continue here
}
fmt.Println("i=",i," j=",j)
}
}
goto
语句(不推荐使用)
goto
语句可以无条件地跳转到程序中指定标签处goto
语句通常与条件语句配合使用,用于实现条件转移,跳出循环体等功能- 在 Go 程序设计中,一般不主张使用
goto
语句,避免造成程序的混乱,使得理解和调试程序产生阻碍 - 在循环控制中使用
goto label
跳出(程序无条件的执行转移位置)和:continue label
及break label
跳出(针对循环体进行跳出)有很大区别
推荐阅读:Go To Statement Considered Harmful:A Retrospective 中文版
here:
// fmt.Println("hhhhh") // 对于 break 和 continue 标签跳出,标签后除了循环体不能跟任何语句
for i := 0; i < 2; i++ {
for j := 1; j < 4; j++ {
if j == 2 {
break here //continue here // 跳出后 i 的值仍然会保留(仍然是在循环体中)
}
fmt.Println("i=",i," j=",j)
}
}
label:
fmt.Println("hhhhh") // 此处可以接非当前循环体语句
for i := 0; i < 2; i++ {
for j := 1; j < 4; j++ {
if j == 2 {
goto label // i 会被重新初始化,已经完全被转移
}
fmt.Println("i=",i," j=",j)
}
}
return
语句
return
语句用于终止跳出当前的方法或函数return
后的语句不再执行,如果return
是在main
函数中,表示终止程序
func 函数名(形参列表) (返回值列表) {
// 语句块
return
}
延迟调用-defer
- defer 是关键词
- 延迟调用:defer functionName()
- defer 所在的当前函数执行完之后再执行 functionName() 函数
- 若有多个 defer ,程序采用栈结构存储。即LIFO:后进先执行
- 类似:析构函数–脚本执行完毕后自动调用
- 延迟调用是当前函数被执行完(包括 return )再进行调用
- 变量快照:defer 只是延时调用函数,但此时传递给函数里的变量(上下文环境)是不会受到后续程序的影响
defer 的作用和 析构函数 的作用类似,常用于释放当前函数(对象)可能在其生命期间获得的资源
如下示例代码:
// 案例1:defer 延迟执行,多个 defer 使用栈存储(LIFO)
func delayOne() {
// 延迟调用
defer delayD()
fmt.Println("A")
defer fmt.Println("C")
fmt.Println("B")
}
func delayD() {
fmt.Println("D")
}
// A B C D
// 案例2:变量快照
var language = "go" // 声明一个全局变量-Go
func delayVar() {
// 变量快照
name := "go"
defer fmt.Println("defer 中的 name:"name) // 打印发生在 name="python" 后,但此时的 name 是前文环境中的快照 "go"
name = "python"
fmt.Println("打印在 name = python 后:" + name)
}
// 打印在 name = python 后: python
// defer 中的 name:go
// 案例3:defer 发生在 return 语句之后
func delay() {
// defer 和 return 的执行顺序: defer 在 return 后调用执行
myLan := myDelay()
fmt.Printf("delay 函数里的 language 变量:%s\n", language)
fmt.Printf("delay 函数里的 return 值:%s\n", myLan)
}
func myDelay() string {
defer func() {
language = "python" // 此处改变了全局变量的值,但被延迟执行了
fmt.Printf("myDelay 函数里 defer 的 language 变量:%s\n", language)
}()
fmt.Printf("myDelay 函数里的 language 变量:%s\n", language)
language = "PHP"
return language
}
// myDelay 函数里的 language 变量:go
// myDelay 函数里 defer 的 language 变量:python ==> return 发生后立即被触发
// delay 函数里的 language 变量:python ==> language 已经在 myDelay 中的 defer 所更改
// delay 函数里的 return 值:PHP