Go语言--流程控制

目录

 0 Go语言流程控制特点

1 代码块和作用域

2 条件判断 —— if语句

3 循环(for)

3.1 for循环的初始语句 —— 开始循环时执行的语句

3.2 for循环中的条件表达式 —— 控制是否循环的开关

3.3 for中的结束语句 —— 每次循环结束时执行的语句

4 键值循环(for...range) —— 直接获得对象的索引和数据

4.1 遍历数组、切片 —— 获得索引和元素

4.2 遍历字符串 —— 获得字符

4.3 遍历映射(map) —— 获得map的键值对

4.4 遍历通道

4.5 在遍历中选择希望获得的变量

5 分支选择(switch) —— 拥有多个条件分支的判断

5.1 基本写法

5.2 跨越case的fallthrough —— 兼容C/C++的case设计

5.3 类型switch语句

6 跳转到指定代码标签

6.1 使用goto退出多层循环

6.2 使用goto语句集中处理错误

7 break、continue 语句

7.1 break语句

7.2 continue语句

参考


 0 Go语言流程控制特点

Go语言在流程控制方面的特点如下:

  • 没有do和while循环,只有一个更广义的for循环。
  • switch语句灵活多变,还可以用于类型判断。
  • if语句和switch语句都可以包含一条初始化子语句。
  • break语句和continue语句可以后跟一条标签(label)语句,以标识需要终止或继续的代码块。
  • defer语句可以使我们更加方便地执行异常捕获和资源回收工作。
  • select语句也用于多分支选择,但只与通道配合使用。
  • go语句用于异步启动goroutine并执行指定函数。

1 代码块和作用域

代码块就是由一对花括号包裹的表达式和语句到序列。当然,代码块也可以不包含任何内容,即:空代码块。

除了显式的代码块之外,还有一些隐式的代码块,说明如下:

  • 所有的Go代码形成了一个最大的代码块,即:全域代码块。
  • 每一个包中的代码共同组成了一个代码块,即:代码包代码块。
  • 每一个Go源码文件都是一个代码块,即:源码文件代码块。
  • 每一个if、for、switch 和 select语句都是一个代码块。
  • 每一个在switch 和 select 语句中的case 分支都是一个代码块。

在Go语言中,使用代码块表示词法上的作用域范围,具体规则如下:

  • 一个预定义标志符的作用域是全域代码块。
  • 表示一个常量、变量、类型或函数(不包括方法),且声明在函数之外的标志符的作用域是当前的代码包代码块。
  • 被导入的代码包的名称的作用域是当前的源码文件代码块。
  • 表示方法接收者、方法参数或方法结果的标志符的作用域是当前的方法代码块。
  • 对于表示常量、变量、类型或函数的标志符,如果被声明在函数内部,那么作用域就是包含其声明的那个最内层的代码块。

此外,我们还可以重新声明已经在外层代码块中声明过的标志符。当在内层代码块中使用这个标志符时,它表示的总是在该代码块中与它绑定在一起的那个程序实体。可以说,此时在外层代码块中声明的那个同名标志符被暂时屏蔽了。

示例代码如下:redeclare.go

package main

import (
    "fmt"
)

var v = "1234"

func main(){
    v := []int{1, 2, 3}
    if v != nil {
        var v = 123
        fmt.Printf("%v\n", v)  // 123
    }
}

代码说明:变量v被声明了3次。在不同的代码块作用域下,变量v表示的是不同类型的变量。当判断v是否等于nil时,v代表的是那个切片。而当v的值被打印时,它代表的却是那个整数。

2 条件判断 —— if语句

在Go语言中,可以通过if 关键字进行条件判断,格式如下:

if 表达式1 {
    分支1
} else if 表达式2 {
    分支2
} else {
    分支3
}

Go语言规定,与if 匹配的左括号“{” 必须与if 表达式放在同一行,如果尝试将左括号“{” 放在其他位置上,将会触发编译错误。同理,与else 匹配的左括号“{” 也必须与else 在同一行,else 也必须与上一个if 或else if 的右括号“}” 在同一行。

【##】特殊写法

if 还有一种特殊到写法,可以在if 表达式之前添加一个执行语句,再根据变量值进行条件判断。示例代码如下:

if err := Connect(); err != nil {
    fmt.Println(err)
    return
}

代码说明:err := Connect(),这是一条语句,执行完这条语句后,将结果保存到变量err中,err != nil 才是if 的判断表达式,当err不为nil时,打印错误并返回。这种写法可以将返回值和判断条件放在一行进行处理,而且返回值变量err的作用域被限制在if...else...语句组合中。

<提示> 在编程中,变量在其实现变量的功能后,作用范围越小,所造成的问题可能性越小,每一个变量代表一个状态,有状态的地方,状态就会被修改,函数的局部变量只会影响一个函数的执行,但全局变量可能会影响所有代码的执行状态,因此限制变量的作用范围对代码的稳定性有很大的帮助。

3 循环(for)

Go语言中所有循环类型均可以使用for关键字来完成。

for 循环格式:

for 初始语句; 条件表达式; 结束语句 {
    循环体代码
}

for 循环可以通过break、goto、return、panic 语句强制退出循环。

3.1 for循环的初始语句 —— 开始循环时执行的语句

初始语句是在第一次循环前首先执行的语句,一般使用初始语句执行变量初始化工作,如果变量是在此处声明的,其作用域将被局限在这个for循环体内。

初始语句可以被忽略,但是初始语句之后的分号必须要写。示例代码如下:

//方式1
for step := 5; step > 0; step-- {
    fmt.Println(step)
}

//方式2
step := 5
for ; step > 0 ; step-- {
    fmt.Println(step)
}

代码说明:方式1中的step变量的作用域只在for循环体内;而方式2中的step变量的作用域就比方式1中在for循环的初始语句中声明的step要大。

3.2 for循环中的条件表达式 —— 控制是否循环的开关

每次循环开始前都会计算条件表达式,如果表达式的结果为true,则执行循环体代码,否则结束循环。条件表达式可以被忽略,被忽略的条件表达式默认形成无限循环。

【##】无限循环

var i int

for ; ; i++ {
    if i > 10 {
        break
    }
}

上面的代码还可以写得更简洁,代码如下:

var i int

for {
    if i > 10 {
        break
    }
    i++
}

代码说明:忽略for循环的所有语句,此时for 执行无限循环。将 i++ 从for结束语句位置放置到循环体的末尾是等效的,这样编写的代码更具有可读性,建议使用这种代码样式来描述无限循环。

【##】只有一个循环条件的循环

在上面的代码基础上还可以进一步简化代码,将if 判断表达式整合到for 的条件表达式中,代码如下:

var i int

for i < 10 {
    i++
}

代码说明:上面这段代码其实类似于其他编程语言的while循环语句了,在while后添加一个条件表达式,满足条件表达式时持续循环,否则结束循环。可以看到,在Go语言中,可以使用for 语句来描述类似于其他编程语言中的while循环结构。

3.3 for中的结束语句 —— 每次循环结束时执行的语句

需要注意的是,如果循环被break、goto、return、panic 等语句强制退出时,结束语句不会被执行。

示例:使用for循环打印出九九乘法表。

package main

import "fmt"

func main(){
    for y := 1; y<10; y++ {
        for x := 1; x<=y; x++{
            fmt.Printf("%d*%d=%d ", x, y, x*y)
        }
        fmt.Println()  //换行
    }
}

运行结果:go run multable.go

1*1=1
1*2=2 2*2=4
1*3=3 2*3=6 3*3=9
1*4=4 2*4=8 3*4=12 4*4=16
1*5=5 2*5=10 3*5=15 4*5=20 5*5=25
1*6=6 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36
1*7=7 2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49
1*8=8 2*8=16 3*8=24 4*8=32 5*8=40 6*8=48 7*8=56 8*8=64
1*9=9 2*9=18 3*9=27 4*9=36 5*9=45 6*9=54 7*9=63 8*9=72 9*9=81

4 键值循环(for...range) —— 直接获得对象的索引和数据

Go语言可以使用for...range 遍历数组、切片、字符串、map以及通道(channel)。通过for...range 遍历的返回值有一定的规律:

  • 数组、切片、字符串返回索引和值。
  • 映射(map)返回键和值。
  • 通道只返回通道内的值。

4.1 遍历数组、切片 —— 获得索引和元素

示例:遍历切片。

for i, v := range []int{1, 2, 3, 4} {
    fmt.Printf("index: %d, value: %d\n", i, v)
}

运行结果:go run demo.go

index: 0, value: 1
index: 1, value: 2
index: 2, value: 3
index: 3, value: 4

代码说明:在遍历数组、切片时,i, v分别代表数组/切片的下标及下标对应的值。

4.2 遍历字符串 —— 获得字符

Go语言中,也可以使用for...range 对字符串进行遍历,遍历时,返回的分别是字符的索引下标和字符。

示例:遍历字符串。

var str = "hello 你好"
for i, v := range str {
    fmt.Printf("index: %d, value: 0x%x\n", i, v)
}

运行结果:

index: 0, value: 0x68
index: 1, value: 0x65
index: 2, value: 0x6c
index: 3, value: 0x6c
index: 4, value: 0x6f
index: 5, value: 0x20
index: 6, value: 0x4f60
index: 9, value: 0x597d

代码说明:代码中的变量v,实际类型是rune,本质上是int32类型,以十六进制打印出字符的Unicode编码值。

4.3 遍历映射(map) —— 获得map的键值对

对于map类型来说,for...range遍历map时,返回的分别是map的索引键和索引对应的值,即map的键值对,因为它们总是一对一对的出现的。

示例:遍历map。

m := map[string]int{
    "hello": 100,
    "world": 200,
}

for k, v := range m {
    fmt.Println(k, v)
}
m := map[string]int{
    "hello": 100,
    "world": 200,
}

for k, v := range m {
    fmt.Println(k, v)
}

运行结果:

hello 100
world 200

<注意> 对map遍历时,遍历输出的键值是无序的,如果需要有序地输出键值对,需要进行排序处理。

4.4 遍历通道

for...range 可以遍历通道(channel),但是在遍历通道时,只输出一个值,即通道内对应的数据。

示例:遍历通道。

c := make(chan int)   //创建一个int型通道

//启动一个goroutine
go func(){
    c <- 1
    c <- 2
    c <- 3
    close(c)   //关闭通道
}()

for v := range c {
    fmt.Println(v)
}

运行结果:

1
2
3

代码说明:使用for...range对通道进行c遍历,其实就是不断地从通道中取数据,直到通道被关闭。

4.5 在遍历中选择希望获得的变量

在使用for...range遍历某个对象时,可能有时候不需要同时获得key或者value,这个时候可以采用一些技巧,让代码变得更简洁。

【##】遍历数组/切片时,只获得索引下标或者元素。示例代码如下:

//只获取切片索引,value被设置为匿名变量
for i, _ := range []int{1, 2, 3, 4} {
    fmt.Printf("index: %d\n", i)
}

//只获取切片索引,value部分不设置也是可以的
for i := range []int{1, 2, 3, 4} {
    fmt.Printf("index: %d\n", i)
}

//只获取切片元素,切片索引使用匿名变量
for _, v := range []int{1, 2, 3, 4} {
    fmt.Printf("value: %d\n", v)
}

【##】遍历map时,只获取map的键或者值。

//声明并初始化一个map
m := map[string]int{
    "hello": 100,
    "world": 200,
}

//遍历map,只获取键,值被设置为匿名变量
for k, _ := range m {
    fmt.Println(k)
}

//遍历map,只获取键,值部分也可以不设置
for k := range m {
    fmt.Println(k)
}

//遍历map,只获取值,键部分设置为匿名变量
for _, v := range m {
    fmt.Println(v)
}

【总结】for循环的功能

  • Go语言的for包含初始化语句、条件表达式、结束语句,这三个部分均可缺省。
  • for...range 支持对数组、切片、字符串、map、通道进行遍历操作。
  • 在需要时,可以使用匿名变量对for...range 的变量进行选取。

5 分支选择(switch) —— 拥有多个条件分支的判断

在Go语言中的switch,不仅可以基于常量进行判断,还可以基于表达式进行判断。

<提示> C/C++语言中的switch语句只支持常量判断,不能对字符串、表达式等复杂情况进行处理,这么设计的主要原因是性能问题。C/C++可以根据case的值作为偏移量直接跳转代码,在性能敏感代码处,这样做显然是有好处的。到了Go语言的时代,语言的运行效率并不能决定最终的效率,I/O效率现在是最主要的问题。因此,Go语言中的switch语法设计尽量以使用方便为主。

5.1 基本写法

Go语言的switch中的每一个case与case之间是独立的代码块,无需显式执行break语句,case执行完毕后自动中断,当然显式地在case的末尾加上break语句也是可以的。

示例代码:

switch a := "hello"; a {
case "hello":
    fmt.Println(1)    // 1
case "world":
    fmt.Println(2)
default:
    fmt.Printf(3)
}

《代码说明》上面例子中,每个case均是字符串格式,且使用了default分支。switch语句 和 if语句一样,同样支持一条子语句来初始化局部变量,按从上到下,从左到右顺序匹配case执行。

Go语言规定每个switch语句只能有一个default分支,只有全部的case分支都匹配失败,才会执行default分支语句。因此,建议将default分支放置在switch语句的末尾处。

1、一个case分支包含多个值

当出现多个分支要放在一起的时候,可以写成像下面的代码:

var a = "mum"

switch a {
case "mum", "dad":          //多个匹配条件符合其中一项即可
    fmt.Println("family")

}

2、分支表达式

case后不仅仅只是常量,还可以是表达式。代码如下:

//示例1
var r int = 11

switch {                 //相当于 switch true {...}
case r > 10 && r < 20:
    fmt.Println(r)
}
//输出结果: 11

//示例2
switch x := 5; {        //相当于 switch x := 5; true {...}
case x > 5:
    fmt.Println("a")
case x > 0 && x <= 5:
    fmt.Println("b")    //不能写成 case x>0, x<=5 因为多条件是 or 关系
default:
    fmt.Println("z")
}
//输出结果: b

《注意》这种情况下,switch后面不再跟判断变量,连判断的目标都没有了。

3、相邻的空case不构成多条件匹配。这点和C/C++语言是有区别的。

switch x {
case a:            //单条件,内容为空。隐式 "case a: break"
case b:
    println("b")
}

5.2 跨越case的fallthrough —— 兼容C/C++的case设计

在Go语言中,case是一个独立的代码块,执行完毕后不会像C/C++语言那样紧接着下一个case执行。但是为了兼容一些移植代码,加入了fallthrough 关键字来实现这一功能。

switch x := 5; x {
default:
    fmt.Println(x)
case 5:
    x += 10
    println(x)
    fallthrough      //继续执行下一个case,但不再匹配条件表达式
case 6:
    x += 20
    printfln(x)
    //fallthrough   //如果在此继续fallthrough,不会执行default,完全按源码顺序执行
                    //导致错误:"cannot fallthrough final case in switch" 错误
}               

运行结果:

15

35

<注意> fallthrough 必须放在case代码块末尾,可使用break语句提前结束switch语句。

switch x := 5; x {
default:
    fmt.Println(x)
case 5:
    x += 10
    println(x)

    if x >= 15 {
        break      //终止switch语句,不再执行后续语句
}
    fallthrough    //必须是case代码块的最后一条语句
case 6:
    x += 20
    printfln(x)
}

运行结果:

15

<提示> 对于fallthrough的使用,在新编写的Go语言代码中,不推荐使用,它的出现主要是为了方便兼容移植旧代码。

5.3 类型switch语句

switch语句 也可用于接口类型匹配,写法格式如下

switch 接口变量.(type) {
case 类型1:
    ...
case 类型2:
    ...
...
default:
    ...       //变量不是所有case中列举的类型时的处理分支
}
  • 接口变量:表示需要判断的接口类型的变量。
  • 类型1、类型2......:表示接口变量可能具有的数据类型列表,满足时,会执行指定case分支的代码块。

【##】使用类型分支判断基本类型

示例:下面的例子将一个interface{}类型的参数传给printType()函数,通过switch判断变量v的类型,然后打印对应类型的提示。

func printType(v interface{})() {
    switch v.(type) {
    case int:
        fmt.Println(v, "is int")
    case string:
        fmt.Println(v, "is string")
    case bool:
        fmt.Println(v, "is bool")
    default:
        fmt.Println(v, "unknown type")
    }
}
func main(){
    printType(1024)
    printType("Godlike")
    printType(3.14156)
}

运行结果:

1024 is int
Godlike is string
3.14156 unknown type

<注意> 使用switch语句进行类型分支判断时,每个case表达式中包含的都是类型字面量而不是表达式;同时,fallthrough语句不允许出现在类型switch语句中。

【##】使用类型分支判断接口类型

有多个接口进行类型断言时,可以使用类型分支简化判断过程。

示例:电子支付能够刷脸支付,而现金支付容易被偷。使用类型分支可以方便地判断一种支付方法具备的特性信息。

//定义电子支付结构
type Alipay struct{
}

//为Alipay添加CanUseFaceID()方法,表示电子支付方式支持刷脸
func (a *Alipay) CanUseFaceID() {
}

//定义现金支付结构
type Cash struct{
}

//为Cash添加Stolen()方法,表示现金支付会出现偷窃情况
func (a *Cash) Stolen() {
}

//定义具备刷脸特性接口
type ContainCanUseFaceID interface{
    CanUseFaceID()
}

//定义被偷特性接口
type ContainStolen interface{
    Stolen()
}

//打印支付方式具备的特性
func print(payMethod interface{}) {
    switch payMethod.(type){
        case ContainCanUseFaceID:      //可以刷脸
            fmt.Printf("%T can use faceID\n", payMethod)
        case ContainStolen:            //可能被偷
            fmt.Printf("%T may be stolen\n", payMethod)
    }
}

func main(){
    //使用电子支付
    print(new(Alipay))  //使用new()函数实例化结构体对象
    
    //使用现金支付
    print(new(Cash))
}

运行结果:go run cashAndAlipay.go

*main.Alipay can use faceID
*main.Cash may be stolen

6 跳转到指定代码标签

goto语句通过标签可以进行代码间的无条件跳转。goto语句可以在快速跳出循环、避免重复退出上有一定帮助。Go语言中使用goto语句能简化一些代码的实现过程。

6.1 使用goto退出多层循环

示例:如果需要连续跳出两层循环,使用传统的处理方式如下:

func main(){
    var breakAgain bool
    
    //外层循环
    for x:=0; x<10; x++ {
        //内层循环
        for y:=0; y<10; y++ {
            //满足某个条件时,退出内循环
            if y == 5 {
                //设置退出标志
                breakAgain = true
                break
            }
        }
        //判断标记,退出外循环
        if breakAgain {
            break
        }
    }
    
    fmt.Println("done")
}

修改上面的代码,使用Go语言的goto语句进行优化。

func main(){
    //外层循环
    for x:=0; x<10; x++ {
        //内层循环
        for y:=0; y<10; y++ {
            if y == 5 {
                //跳转到标签
                goto breakHere
            }
        }
    }
    //手动返回,避免进入标签代码部分
    return
    
breakHere
    fmt.Println("done")
}

《代码说明》标签只能被goto使用,但不影响代码执行流程,如果不手动返回,在不满足条件时,标签部分代码仍将会被执行。可以看到,使用goto语句后,无须额外的变量就可以快速推出所有的循环。

<注意事项>

1、关于标签的说明,标签区分大小写,且未使用的标签会引发编译错误。

2、goto语句不能跳转到其他函数,或内层代码块内。

func test() {
test:
    println("test")
    println("test exit")
}

func main() {
    for i:=0; i<3; i++ {
    loop:
        println(i)
    }

    goto test      //错误:label test not defined
    goto loop      //错误:goto loop jumps into block
}

6.2 使用goto语句集中处理错误

多处错误处理如果存在代码重复时,我们可以尝试使用goto语句来对错误进行集中处理。示例代码如下:

err := firstCheckError()
if err != nil {
    goto onExit
}

err = secondCheckError()
if err != nil {
    goto onExit
}

fmt.Println("done")

onExit:
    fmt.Println(err)
    exitProcess()

7 break、continue 语句

 和goto语句定点跳转不同,break、continue语句用于中断代码块的执行。

7.1 break语句

break 用于switch、for、select语句,终止整个语句块的执行。break语句还可以在break后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的for、switch 和 select的代码块上。

7.2 continue语句

continue语句可以结束当前循环,开始下一次的循环迭代过程,仅限在for 循环内使用。在continue语句后也可以添加标签,表示开始标签位置对应的循环。

  • 示例1:
func main() {
    for i:=0; i<10; i++ {
        if i%2 == 0 {
            continue    //立即进入下一轮循环
        }
        
        if i > 5 {
            break       //立即终止整个for循环
        }
        
        fmt.Println(i)
    }
}

运行结果:

1
3
5

示例2:

func main() {
outer:                              //外循环标签
    //外循环
    for x:=0; x<5; x++ {
        //内循环
        for y:=0; y<10; y++ {
            if y > 4 {
                fmt.Println()       //打印换行
                continue outer      //重新从outer标签处开始下一次的外循环
            }
            
            if x > 2 {
                break outer         //退出outer标签的代码块,亦即退出外循环
            }
            
            fmt.Print(x, ":", y, " ")
        }
    }
    
    fmt.Println("main done")
}

运行结果:

0:0 0:1 0:2 0:3 0:4
1:0 1:1 1:2 1:3 1:4
2:0 2:1 2:2 2:3 2:4
main done

参考

《Go语言从入门到进阶实战(视频教学版)》

《Go并发编程实战(第2版)》

《Go语言学习笔记》

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值