Go语言基础05-Go流程控制

从策划到设计制作,每一步都追求做到细腻,制作可持续发展的企业网站。为客户提供网站制作、做网站、网站策划、网页设计、域名注册、网络空间、网络营销、VI设计、 网站改版、漏洞修补等服务。为客户提供更好的一站式互联网解决方案,以客户的口碑塑造优易品牌,携手广大客户,共同发展进步。

1 概述

流程控制是顺序编程中必不可少的一部分,它是整个编程基础的重要一环。在顺序编程的流程控制部分,Go语言和其他主流语言有一些差别,主要体现在Go语言没有do-while语句,因此for语句拥有更广泛的含义与用途。另一方面switch语句也有一些扩展,例如支持类型判断和初始化子语句等。

除了这些常见的流程控制语法的关键字,Go语言还有三个特殊的关键字,分别是:

  • defer:用于捕获异常和资源回收等工作;
  • select:用于多分支选择(配合通道使用);
  • go:用于异步启动goroutine并执行特定函数。

2 条件语句

2.1 if判断

例子:

package main

import "fmt"

func main() {
    a := 1
    if a < 20 {
        fmt.Printf("a小于20\n")
    }
    fmt.Printf("a的值是:%d\n", a)
}

2.2 if-else判断

例子:

package main

import "fmt"

func main() {
    a := 100
    if a < 20 {
        fmt.Printf("a小于20\n")
    } else {
        fmt.Printf("a大于20\n")
    }
    fmt.Printf("a的值是:%d\n", a)
}

例子:

package main

import "fmt"

func main() {
    a := 100
    if a < 20 {
        fmt.Printf("a小于20\n")
        if a > 10 {
            fmt.Printf("a大于10\n")
        } else {
            fmt.Printf("a小于10\n")
        }
    } else {
        fmt.Printf("a大于20\n")
    }
    fmt.Printf("a的值是:%d\n", a)
} 
}

说明:
上面的代码嵌套了一层if-else语句,这在编程规范中可以说大忌,特别是在逻辑复杂的情况下,嵌套if语句将非常影响性能,因此就有了else-if语句。

2.3 else-if判断

else-if语句是在前面if-else语句之上再度扩展的,为了解决多重判断的问题。例如:

package main

import "fmt"

func main() {
    a := 11
    if a > 20 {
        fmt.Printf("a大于20\n")
    } else if a < 10 {
        fmt.Printf("a小于10\n")
    } else {
        fmt.Printf("a大于10\n")
        fmt.Printf("a小于20\n")
    }
    fmt.Printf("a的值是:%d\n", a)
}

else-if语句可以连续使用多个else if关键字,例如判断一个数字是否大于10小于20且不等于11:

package main

import "fmt"

func main() {
    a := 13
    if a > 20 {
        fmt.Printf("a大于20\n")
    } else if a < 10 {
        fmt.Printf("a小于10\n")
    } else if a == 11 {
        fmt.Printf("a等于11\n")
    } else {
        fmt.Printf("a大于10\n")
        fmt.Printf("a小于20\n")
        fmt.Printf("a不等于11\n")
    }
    fmt.Printf("a的值是:%d\n", a)
}

2.4 初始化子语句

if语句可以有一个子语句,用于初始化局部变量,例如:

package main

import "fmt"

func main() {
    if a := 10; a < 20 {
        fmt.Printf("a小于20\n")
    } else {
        fmt.Printf("a的值是:%d\n", a)
    }
}

注意1:
子语句只能有一个表达式,比如下面的例子是无法编译的。

func main() {
    if b := 10; a := 10; a < 20 {  //编译出错,初始化子语句只能有一个表达式
        fmt.Printf("a小于20\n")
    } else {
        fmt.Printf("a的值是:%d\n", a)
    }
}

注意2:
a的值是在if代码块中定义的,所以不能在代码块之外调用,例如下面代码也是无法编译的:

func main() {
    if a := 10; a < 20 {
        fmt.Printf("a小于20\n")
    } 
    //编译出错,a是在if代码块中定义的,所以不能再函数中调用
    fmt.Printf("a的值是:%d\n", a)
}

3 选择语句

在上面使用if条件语句时,一般用于二元判断,对于多元判断使用条件语句就会显得烦琐,所以就有了选择语句。例如判断一个数字属于哪个区间就可以用选择语句轻松实现,避免条件语句烦琐的嵌套过程。

3.1 switch语句

在Go语言中,switch表示选择语句的关键字,switch语句会根据初始化表达式得出一个值,然后根据case语句的条件,执行相应的代码块,最终返回特定内容。每个case被称为一种情况,只有当初始化语句的值符合case的条件语句时,case才会被执行。

如果没有遇到符合的case,则可以使用默认的case(default case),如果己经遇到了
符合的case,那么后面的case都不会被执行。

与其他编程语言不同的是,在Go语言编程中,switch有两种类型。

  • 表达式switch:在表达式switch中,case包含与switch表达式的值进行比较的表达式。
  • 类型switch:在类型switch中,case包含与特殊注释的switch表达式的类型进行比较的类型。
3.1.1 表达式switch

例子:

package main

import "fmt"

func main() {
    grade := "B"
    marks := 90

    switch marks {
    case 90:
        grade = "A"
    case 80:
        grade = "B"
    case 60, 70:
        grade = "C"
    default:
        grade = "D"
    }
    fmt.Printf("你的成绩为%s\n", grade)
}

这种方式虽然简单,但一般不这样写,因为不容易扩展这个选择语句,例如把分数改为100或者91时,也会返回“你的成绩为D”,这明显不符合实际情况。这是因为选择语句中没有考虑值的范围的问题,现在假设90分到100分为A, 80分到89分为B, 60分到79分为C ,低于60分则为D 。这种情况下上面的单值判断己经不能满足我们此时的需求。

因此就需要完整的switch 表达式写法了:

package main

import "fmt"

func main() {
    grade := "E"
    marks := 90

    switch {
    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.Printf("你的成绩优秀!\n")
    case grade == "B":
        fmt.Printf("表现良好!\n")
    case grade == "C", grade == "D": //case表达式可以有多个
        fmt.Printf("再接再厉!\n")
    default:
        fmt.Printf("成绩不合格!\n")
    }
    fmt.Printf("你的成绩为%s\n", grade)
}

上面例子中有两个switch相互关联,第一个switch语句判断成绩所属区间,并得出grade的值, 然后第二个switch根据grade的值返回相应的话语。

注意,每一个case都可以拥有多个表达式,它的含义与fallthrough一样,例如下面两种写法是一样的意思:

 /*  多表达式写法   */
    case grade == "C", grade == "D": //case表达式可以有多个
        fmt.Printf("再接再厉!\n")
    /*  fallthrough写法    */
    case grade == "C":
        fallthrough
    case grade == "D":
        fmt.Printf("再接再厉!\n")

fallthrough关键词可以把当前case控制权交给下一个case语句判断。

3.1.2 类型switch

类型switch语句针对变量类型判断执行哪个case代码块,下面是一个简单的例子:

package main

import "fmt"

var x interface{} //空接口

func main() {
    x = 1
    switch i := x.(type) { //这里表达式只有一句初始化子语句
    case nil:
        fmt.Printf("这里是nil,x的类型是%T", i)
    case int:
        fmt.Printf("这里是int,x的类型是%T", i)
    case float64:
        fmt.Printf("这里是float64,x的类型是%T", i)
    case bool:
        fmt.Printf("这里是bool,x的类型是%T", i)
    case string:
        fmt.Printf("这里是string,x的类型是%T", i)
    default:
        fmt.Printf("未知类型")
    }
}

类型switch的初始化子语句中需要判断的变量必须是具有接口类型的变量(如果是固定类型的变量就没有判断的意义了)。在语法上类型switch与表达式switch没有太大区别。

3.2 switch初始化语句

switch语句同样拥有初始化子语句,和if一样均是写在关键字后面,只能有一句语句,例如:

 /* 单值判断写法    */
    switch marks := 90; marks {
    case 90:
        grade = "A"
    case 80:
        grade = "B"
    case 70:
        grade = "C"
    case 60:
        grade = "D"
    default:
        grade = "E"
    }

    /* 范围表达式写法   */
    switch marks := 90; { //这里的分号不能省略
    case marks >= 90:
        grade = "A"
    case marks >= 80:
        grade = "B"
    case marks >= 70:
        grade = "C"
    case marks >= 60:
        grade = "D"
    default:
        grade = "E"
    }

3.3 select语句

在Go语言中,除了switch语句,还有一种选择语句一-select,这种选择语句用于配合通道(channel)的读写操作,用于多个channel的并发读写操作。

switch是按顺序从上到下依次执行的,而select是随机选择一个case来判断,直到匹配其中的一个case,举个例子:

package main

import "fmt"

func main() {
    a := make(chan int, 1024)
    b := make(chan int, 1024)

    for i := 0; i < 10; i++ {
        fmt.Printf("第%d次", i)
        a <- 1
        b <- 1

        select {
        case <-a:
            fmt.Println("from a")
        case <-b:
            fmt.Println("from b")
        }
    }
}

上述代码的意思是,同时在ab中选择,哪个有内容就从哪个读,由于channel的读写操作是阻塞操作,使用select语句可以避免单个channel的阻塞。此外select同样可以使用default代码块,用于避免所有channel同时阻塞。

4 循环语句

循环语句是编程中常使用的流程控制语句之一,在Go语言中,循环语句的关键字是for ,没有while关键字。for语句可以根据指定的条件重复执行其内部的代码块,这个判断条件一般是由for关键字后面的子语句给出的。

例如一个简单的for循环例子:

package main

import "fmt"

func main() {
    for a := 0; a < 5; a++ {
        fmt.Printf("a的值是:%d\n", a)
    }
}

上面for关键字后面有三个子语句,初始化变量a0,并判断当a小于5时执行下面代码块的内容,每次判断a的值都加l,直到不符合初始化语句的判断条件,进而退出循环。

4.1 for的子语句

for语句后面的三个子语句我们称为:

  • 初始化子语句
  • 条件子语句
  • 后置子语句

这三者不能颠倒顺序,其中条件子语句是必需的,条件子语句会返回一个布尔型,true 则执行代码块,false则跳出循环。

例如以下示例代码中就省略了初始化子语句和后置子语句:

package main

import "fmt"

func main() {
    a := 0
    b := 5
    for a < b {
        a++
        fmt.Printf("a的值是:%d\n", a)
    }
}

在上面的例子中,for关键字后面只有一个a< b的判断语句,这是典型的条件判断语
句,它实际上是三个子语句的简写:

for ; a < b ;
//Go语言编译器会自动判断三个子语句中是否存在条件子语句

//例如写成这样就会报错
for ; ; a < b

后置子语句的意思就是先进行条件子语句判断,for代码块执行之后再对条件变量操作的语句进行判断,上面例子中的a++就是一个后置子语句。

4.2 range子语句

每一个for语句都可以使用一个特殊的range子语句,其作用类似于迭代器,用于轮询数组或者切片值中的每一个元素,也可以用于轮询字符串的每一个字符,以及字典值中的每个键值对,甚至还可以持续读取一个通道类型值中的元素。

例子:

package main

import "fmt"

func main() {
    str := "abcz"

    for i, char := range str {
        fmt.Printf("字符串第%d个字符的值为%d\n", i, char)
    }
    for _, char := range str { //忽略第一个值(忽略index)
        println(char)
    }

    for i := range str { //忽略第二个值
        fmt.Println(i)
    }

    for range str { //忽略全部返回值,只执行下面代码块
        println("执行成功")
    }
}

range关键宇右边是range表达式,表达式一般写在for语句前面,以便提高代码易读性。像上面的例子可以写成:

for i := range "abcz"
//把原来的str := "abcz"表达式写到range关键字后面

这样不仅降低了可读性,也不容易管理后续的循环代码块。

range关键宇左边表示的是一对索引-值对,根据不同的表达式返回不同的结果,详见下表。

右边表达式返回的类型第一个值第二个值
stringindexstr[index],返回类型为rune
array/sliceindexstr[index]
mapkeym[key]
channelelement

从上表可以看出,除了轮询字符串,还支持其他类型,例如数组,切片,字典甚至通道等等。

例子:

package main

import "fmt"

func main() {
    m := map[string]int{"a": 1, "b": 2}
    for k, v := range m {
        println(k, v)
    }

    numbers := []int{1, 2, 3, 4}
    for i, x := range numbers {
        fmt.Printf("第%d次,x的值为%d。\n", i, x)
    }
}

返回第一个值为索引值(键值),有时候并不是我们所需要的,因此可以使用“_”空标识符表示忽略第一个返回值。对于空字典或切片、空数组、空字符串等情况,for语句会直接结束,不会循环。

但如果需要指定for执行循环的次数,例如需要获取数组(或者其他支持的类型)里面的值,而数组中有一个值是空字符串,则可以指定数组(或其他支持类型)长度,强制让for循环执行相应次数,例如将上面的例子稍微改一下:

package main

import "fmt"

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循环返回的信息可以看到第5x的值为0,代码块的确执行了5次。

5 延迟语句

defer用于延迟调用指定函数,defer关键字只能出现在函数内部。
例子:

package main

import "fmt"

func main() {
    defer fmt.Println("World")
    fmt.Println("Hello")
}

上面例子会首先打印Hello,然后再打印World,因为第一句使用了defer关键字, defer语句会在函数最后执行,被延迟的操作是defer后面的内容。

defer后面的表达式必须是外部函数的调用,上面的例子就是针对fmt.Println 函数的延迟调用。defer有两大特点:

  • 只有当defer语句全部执行,defer所在函数才算真正结束执行。
  • 当函数中有defer语句时,需要等待所有defer语句执行完毕,才会执行return语句。

因为defer的延迟特点,可以把defer语句用于回收资源、清理收尾等工作。使用defer语句之后,不用纠结回收代码放在哪里,反正都是最后执行。

这里需要注意defer的执行时机,例如下面的例子:

package main

import "fmt"

var i = 0

func print() {
    fmt.Println(i)
}

func main() {
    for ; i < 5; i++ {
        defer print()
    }
}

上面例子返回的是55,这是因为每个defer都是在函数轮询之后,最后才执行,此时i的值当然就是5了。如果要正确反向打印数字则应该这样写:

package main

import "fmt"

var i = 0

func print(i int) {
    fmt.Println(i)
}

func main() {
    for ; i < 5; i++ {
        defer print(i)
    }
}

上面例子引入了函数参数,虽然还没介绍函数的概念,但是不妨碍理解这里面的defer关键知识。这里之所以是一个反序的数字列表,是因为defer其实是一个栈,遵循先入后出,或者理解为后进先出。

i等于0 时,defer语句第一次被压栈,此时defer后面的函数返回0; i不断自增,一直到i等于4时,defer语句第5次入栈,defer后的函数返回4;此时i的自增不再满足for条件,于是跳出循环,在结束之前,Go语言会根据defer后进先出原则逐条打印栈内的数值,于是就出现现在看到的结果了。

简单来说就是当一个函数内部有多个defer语句时,最后面的defer语句最先执行(当然是指在所有defer语句中) 。

6 标签

在Go语言中,有一个特殊的概念就是标签,可以给forswitchselect等流程控制代码块打上一个标签,配合标签标识符可以方便跳转到某一个地方继续执行,有助于提高编程效率。标签格式如下:

L1:
    for i := 0; i <= 5; i++ {
        //代码块
    }

//下面写法也可以,不过Go语言编译器会自动格式化为上面的格式
L2:switch i {
    //代码块
}

标签的名称是区分大小写的,为了提高代码易读性,建议标签名称使用大写字母和数字。标签可以标记任何语句, 并不限定于流程控制语句,未使用的标签会引发错误。

6.1 break

break语句意思是打断,这个语句表示打断当前流程控制。
例子:

package main

import "fmt"

func main() {
    for i := 0; i < 10; i++ {
        fmt.Printf("i的值是:%d\n", i)
        if i > 4 {
            break
        }
    }
}

上面例子中break的用法与主流编程语言并无差别,当i >4时,break语句打断了后续的循环操作,跳出循环。

但是对于嵌套流程控制语句,例如下面这种情况:

package main

import "fmt"

func main() {
    for {
        x := 1
        switch {
        case x > 0:
            fmt.Println("A")
            break
        case x == 1:
            fmt.Println("B")
        default:
            fmt.Println("C")
        }
    }
}

上面的例子会一直返回A,无限循环。上面代码出现的break语句只是跳出了switch流程,并没有跳出for循环,所以这个程序会一直执行下去。

为了跳出指定的流程控制代码块,就需要标签出场了:

package main

import "fmt"

func main() {
LOOP1:
    for {
        x := 1
        switch {
        case x > 0:
            fmt.Println("A")
            break LOOP1
        case x == 1:
            fmt.Println("B")
        default:
            fmt.Println("C")
        }
    }
}

上面代码中的break LOOPl表示跳出到标签为LOOPl的代码块之外。

6.2 continue

break相反,continue用于跳转到指定代码块位置继续执行任务,continue 仅能用于for循环。例如与上面几乎一样的代码,只是改动了一个关键宇:

package main

import "fmt"

func main() {
LOOP1:
    for i := 0; i <= 5; i++ {
        switch {
        case i > 0:
            fmt.Println("A")
            continue LOOP1
        case i == 1:
            fmt.Println("B")
        default:
            fmt.Println("C")
        }
        fmt.Printf("i is:%d\n", i)
    }
}

上面代码中因为使用了continue语句跳转到了外围for循环中,与break不同的是,continue表示跳转后继续执行操作。

6.3 goto

Go语言中的goto语句可以无条件跳转到相同函数中的带标签语句。标签、goto等关键字都并非Go语言独创,Go语言可以说是一门大量参考了其他语言优点的编程语言,在流程控制上做了一些扩增(如类型switch),同时也减少了一些关键宇(如while) 。

package main

func main() {
    var i int
    for {
        println(i)
        i++
        if i > 2 {
            goto BREAK
        }
    }
BREAK:
    println("break")
}

上面代码中,goto语句指向了BREAK标签,所以当循环中遇到i>2时,直接跳转打印breakgoto只能在同一个函数中跳转。

下面的是我的公众号二维码,欢迎关注。文章转载请注明出处www.leexide.com
Go语言基础05-Go流程控制


文章标题:Go语言基础05-Go流程控制
当前URL:http://hbruida.cn/article/ighssh.html