hello.md 26 KB

Go语言简介

Go是一门编译型的和静态的编译语言。

  • 内置并发编程支持: a. 使用协程(goroutine)作为基本的计算单位。轻松地创建协程 b. 使用通道(channel)来实现协程间的同步和通信
  • 内置映射(map)和切片(slice)类型
  • 支持多态---?
  • 使用接口(interface)来实现装盒(value boxing)和反射(reflection)
  • 支持指针
  • 支持函数闭包------?
  • 支持方法
  • 支持延迟函数调用
  • 支持类型内嵌------指方法的定义?
  • 支持类型推断-----?
  • 内存安全----?
  • 自动垃圾回收-----?
  • 良好的代码跨平台性----二进制文件?
  • 自定义泛型-----(指自定义库?)

最简单的Go程序

package main

func main() {

}

packagefunc 是两个关键字。两个 main 是两个标识符。

此程序第一行指定了当前源代码文件所处的包的包名(此处为main)。 第三行和第四行声明了一个名为 main 的函数。此函数为程序的入口函数。

运行一个Go程序

Go官方工具链工具要求所有的Go源码文件以.go后缀结尾

运行单个go源码文件 go run test.go

如果一个程序的main包中有若干Go源代码文件,在相应目录下可以用下面的命令运行此程序。 go run .

Go 子命令

  • go run 不推荐在正式的大型目中使用。对于正式的项目,最好用go build 或者go install 构建可执行文件来运行Go程序
  • go mod init -------------? go mod tidy 扫描项目,添加依赖至 go.mod 或者从中删除不再被使用的依赖 go get 添加、升级、降级或者删除单个依赖。
  • 以 _ 和 . 开头的源代码文件会被Go官方工具链工具忽略掉
  • go run 、 go build 和 go install 只会输出代码语法错误 go vet 可以输出警告
  • go fmt 格式化代码
  • go test 运行单元和基准测试用例
  • go doc 在终端中查看Go代码库包的文档

程序源码基本元素介绍

在大多数高级编程语言中,数据通常被抽象为各种类型(type)和值(value)。 一个类型可以看作是值的模板。一个值可以看作是某个类型的实例。 大多数编程语 言支持自定义类型和若干预声明类型(即内置类型)。

函数说明

  • 入参是int
  • 返回结果也是int,两个

    func StatRandomNumbers(numRands int) (int, int) {
    	// 声明了两个变量(类型都为int,初始值都为0)
    	var a, b int
    	// 一个for循环代码块
    	for i := 0; i < numRands; i++ {
    		// 一个if-else条件控制代码块
    		if rand.Intn(MaxRand) < MaxRand/2 {
    			a = a + 1
    		} else {
    			b++ // 等价于:b = b + 1
    		}
    	}
    	return a, b // 此函数返回两个结果
    }
    
    x, y := StatRandomNumbers(num)
    

函数左括号不能位于行首(单独换行)

关键字和标识符

关键字

  • const、func、import、package、type和var用来声明各种代码元素。
  • chan、interface、map和struct用做 一些组合类型的字面表示中。
  • break、case、continue、default、 else、fallthrough、for、 goto、if、range、 return、select和switch用在流程控制语句中。 详见基本流程控制语法(第12章)。
  • defer和go也可以看作是流程控制关键字, 但它们有一些特殊的作用。详见 协程和延迟函数调用(第13章)。

标识符

一个标识符是一个以Unicode字母或者_开头并且完全由Unicode字母和Unicode数字 组成的单词。

  • 导出标识符,需要首字母大写,不能以 _ 开头
  • 东方字符都被视为非导出字符

基本类型及其字面量表示

这边有问题的话,具体还是看书吧,不好说

基本内置类型

内置类型也称为预声明类型

Go支持如下内置基本类型:

  • 一种内置布尔类型:bool。
  • 11种内置整数类型:int8、uint8、int16、uint16、int32、uint32、 int64、uint64、int、uint和uintptr。
  • 两种内置浮点数类型:float32和float64。
  • 两种内置复数类型:complex64和complex128。
  • 一种内置字符串类型:string。

除了bool和string类型,其它的15种内置基本类型都称为数值类型(整型、浮点 数型和复数型)。

Go中有两种内置类型别名(type alias):

  • byte是uint8的内置别名。 我们可以将byte和uint8看作是同一个类型。
  • rune是int32的内置别名。 我们可以将rune和int32看作是同一个类型。

自定义类型

type status bool // status和bool是两个不同的类型
type boolean = bool // boolean和bool表示同一个类型

零值

每种类型都有一个零值。一个类型的零值可以看作是此类型的默认值。

  • bool 的零值是 false
  • 数组类型的零值都是 0 (不同类型的 0 在内存中占用空间可能不同)。
  • 字符串类型的零值是一个空字符串

基本类型的字面量表示形式

布尔值的字面量形式

我们可以将false和true这两个预声 明的具名常量当作布尔类型的字面量形式。 但是,我们应该知道,从严格意义上 说,它们不属于字面量。

整数类型值的字面量形式

整数类型值有四种字面量形式:十进制形式(decimal)、八进制形式(octal)、 十六进制形式(hex)和二进制形式(binary)。

package main

func main(){
    println(15 == 017) //true
    println(15 == 0xF) //true
}

浮点数类型值的字面量形式

1.23
01.23 // == 1.23
.23
1.
// 一个e或者E随后的数值是指数值(底数为10)。
// 指数值必须为一个可以带符号的十进制整数字面量。
1.23e2 // == 123.0
123E2 // == 12300.0
123.E+2 // == 12300.0
1e-1 // == 0.1
.1e0 // == 0.1
0010e-2 // == 0.1
0e+5 // == 0.0

虚部字面量形式

数值字面表示中使用下划线分段来增加可读性

// 合法的使用下划线的例子
6_9 // == 69
0_33_77_22 // == 0337722
0x_Bad_Face // == 0xBadFace
0X_1F_FFP-16 // == 0X1FFFP-16
0b1011_0111 + 0xA_B.Fp2i
// 非法的使用下划线的例子
_69 // 下划线不能出现在首尾
69_ // 下划线不能出现在首尾
6__9 // 下划线不能相连
0_xBadFace // x不是一个合法的八进制数字
1_.5 // .不是一个合法的十进制数字
1._5 // .不是一个合法的十进制数字

rune值的字面量形式

1| package main
2|
3| func main() {
4| println('a' == 97)
5| println('a' == '\141')
6| println('a' == '\x61')
7| println('a' == '\u0061')
8| println('a' == '\U00000061')
9| println(0x61 == '\x61')
10| println('\u4f17' == '众')
11| }

字符串的字面量形式

常量和变量

类型不确定值和类型确定值

在Go中,有些值的类型是不确定的。换句话说,有些值的类型有很多可能性。 这些 值称为类型不确定值。对于大多数类型不确定值来说,它们各自都有一个默认类 型, 除了预声明的nil。nil是没有默认类型的。

默认类型,就是你声明一个变量,不说明它的类型,就被当成默认类型

  • 字符串字面量的默认类型是预声明的string类型
  • 一个布尔字面量的默认类型是预声明的bool类型。
  • 一个整数型字面量的默认类型是预声明的int类型。
  • 一个rune字面量的默认类型是预声明的rune(亦即int32)类型。
  • 一个浮点数字面量的默认类型是预声明的float64类型。
  • 如果一个字面量含有虚部字面量,则此字面量的默认类型是预声明的 complex128类型。

类型不确定厂里的显示类型转换

一些合法的转换


// 结果为complex128类型的1.0+0.0i。虚部被舍入了。
complex128(1 + -1e-1000i)
// 结果为float32类型的0.5。这里也舍入了。
float32(0.49999999)
// 只要目标类型不是整数类型,舍入都是允许的。
float32(17000000000000000)
float32(123)
uint(1.0)
int8(-123)
int16(6+0i)
complex128(789)
string(65) // "A"
string('A') // "A"
string('\u68ee') // "森"
string(-1) // "\uFFFD"
string(0xFFFD) // "\uFFFD"
string(0x2FFFFFFFF) // "\uFFFD"

一些非法的转换

int(1.23) // 1.23不能被表示为int类型值。
uint8(-1) // -1不能被表示为uint8类型值。
float64(1+2i) // 1+2i不能被表示为float64类型值。
// -1e+1000不能被表示为float64类型值。不允许溢出。
float64(-1e1000)
// 0x10000000000000000做为int值将溢出。
int(0x10000000000000000)
// 字面量65.0的默认类型是float64(不是一个整数类型)。
string(65.0)
// 66+0i的默认类型是complex128(不是一个整数类型)。
string(66+0i)

常量声明(具名)

1| package main
2|
3| // 声明了两个单独的具名常量。(是的,
4| // 非ASCII字符可以用做标识符。)
5| const π = 3.1416
6| const Pi = π // 等价于:const Pi = 3.1416
7|
8| // 声明了一组具名常量。
9| const (
10| No = !Yes
11| Yes = true
12| MaxDegrees = 360
13| Unit = "弧度"
14| )
15|
16| func main() {
17| // 声明了三个局部具名常量。
18| const DoublePi, HalfPi, Unit2 = π * 2, π * 0.5, "度"
19| }

常量声明中的等号=表示“绑定”而非“赋值”。每个常量描述将一个或多个字面 量绑定到各自对应的具名常量上。 或者说,每个具名常量其实代表着一个字面常 量。

常量可以直接声明在包中,也可以声明在函数体中。 声明在函数体中的常量 称为局部常量(local constant),直接声明在包中的常量称为包级常量 (package-level constant)。

包级常量也常常被称为全局常量。

包级常量声明中的常量描述的顺序并不重要。比如在上面的例子中,常量描述No和Yes的顺序可以掉换一下。

类型确定具名常量

我们可以在声明一些常量的时候指定这些常量的确切类型。这样声明的常量称为类型确定具名常量。

1| const X float32 = 3.14
2|
3| const (
4| A, B int64 = -3, 5
5| Y float32 = 2.718
6| )
//我们也可以使用显式类型转换来声明类型确定常量。 下面的例子和上面的例子是完全等价的
1| const X = float32(3.14)
2|
3| const (
4| A, B = int64(-3), int64(5)
5| Y = float32(2.718)
6| )

常量声明中的自动补全

在一个包含多个常量描述的常量声明中,除了第一个常量描述,其它后续的常量描 述都可以只有标识符部分。 Go编译器将通过照抄前面最紧挨的一个完整的常量描述 来自动补全不完整的常量描述。

1| const (
2| X float32 = 3.14
3| Y // 这里必须只有一个标识符
4| Z // 这里必须只有一个标识符
5|
6| A, B = "Go", "language"
7| C, _
8| // 上一行中的空标识符是必需的(如果
9| // 上一行是一个不完整的常量描述)。
10| )

//自动补全

1| const (
2| X float32 = 3.14
3| Y float32 = 3.14
4| Z float32 = 3.14
5|
6| A, B = "Go", "language"
7| C, _ = "Go", "language"
8| )

在常量声明中使用iota

iota是Go预声明(内置)的一个特殊的具名常量。其被预声明为0,但是它的值在编译阶段并非恒定。当此预声明的iota出现在一个常量声明中的时候,它 的值在第n个常量描述中的值为n(从0开始)。 所以iota只对含有多个常量描述 的常量声明有意义。

例如

package main
2|
3| func main() {
4| const (
5| k = 3 // 在此处,iota == 0
6|
7| m float32 = iota + .5 // m float32 = 1 + .5
8| n // n float32 = 2 + .5
9|
10| p = 9 // 在此处,iota == 3
11| q = iota * 2 // q = 4 * 2
12| _ // _ = 5 * 2
13| r // r = 6 * 2
14| s, t = iota, iota // s, t = 7, 7
15| u, v // u, v = 8, 8
16| _, w // _, w = 9, 9
17| )
18|
19| const x = iota // x = 0 (iota == 0)
20| const (
21| y = iota // y = 0 (iota == 0)
22| z // z = 1
23| )
24|
25| println(m) // +1.500000e+000
26| println(n) // +2.500000e+000
27| println(q, r) // 8 12
28| println(s, t, u, v, w) // 7 7 8 8 9
29| println(x, y, z) // 0 0 1
30| }

实际编程中,我们应该用更有意义的方式。比如:

1| const (
2| Failed = iota - 1 // == -1
3| Unknown // == 0
4| Succeeded // == 1
5| )
6|
7| const (
8| Readable = 1 << iota // == 1
9| Writable // == 2
10| Executable // == 4
11| )

变量声明和赋值操作语句

所有的变量值都是类型确定值。当声明一个变量的时候,我们必须在代码中给编译 器提供足够的信息来让编译器推断出此变量的确切类型。

Go语言有两种变量声明形式。一种称为标准形式,另一种称为短声明形式。 短声明形式只能用来声明局部变量。

和常量声明一样,多个变量可以用一对小括号组团在一起被声明。

1| var (
2| lang, bornYear, compiled = "Go", 2007, true
3| announceAt, releaseAt int = 2009, 2012
4| createdBy, website string
5| )

每个局部声明的变量至少要被有效使用一次

局部变量被声明后至少要被有效使用一次,否则编译器将报错。包级变量无此限制。

可以参考下图

_, _ = r, s // 将r和s做为源值使用一次。

值的可寻址性

所有变量都是可以寻址的,所有常量都是不可被寻址的。

变量和常量的作用域

1| package main
2|
3| const y = 70
4| var x int = 123 // 包级变量
5|
6| func main() {
7| // 此x变量遮挡了包级变量x。
8| var x = true
9|
10| // 一个内嵌代码块。
11| {
12| x, y := x, y-10 // 这里,左边的x和y均为新声明
13| // 的变量。右边的x为外层声明的
14| // bool变量。右边的y为包级变量。
15|
16| // 在此内层代码块中,从此开始,
17| // 刚声明的x和y将遮挡外层声明x和y。
18|
19| x, z := !x, y/10 // z是一个新声明的变量。
20| // x和y是上一句中声明的变量。
21| println(x, y, z) // false 60 6
22| }
23| println(x) // true
24| println(y) // 70 (包级变量y从未修改)
25| /*
26| println(z) // error: z未定义。
27| // z的作用域仅限于上面的最内层代码块。
28| */
29| }

运算符

op=运算符

对于一个二元算数运算符op,语句x = x op y可以被简写为x op= y。 在这个 简写的语句中,x只会被估值一次。

就是 += 的描述

字符串衔接运算符

被+的两个操作数,必须为同一类型的字符串值

+=运算符也适用

函数的声明和调用

函数声明

标准的函数声明

1| func SquaresOfSumAndDiff(a int64, b int64) (s int64, d
int64) {
2| x, y := a + b, a - b
3| s = x * x
4| d = y * y
5| return // <=> return s, d
6| }

Golang 函数不支持入参默认值;参数是默认值是其类型的零值。

连续输入的参数类型共用

1| func SquaresOfSumAndDiff(a, b int64) (s, d int64) {
2| return (a+b) * (a+b), (a-b) * (a-b)
3| // 上面这行等价于下面这行:
4| // s = (a+b) * (a+b); d = (a-b) * (a-b); return
5| }

函数调用

一个声明的函数可以通过名称和一个实参列表来调用 实参列表就是括号括起来的 实参列表中每个单值实参对应着(或说被传递给了)一共形参 函数传参属于赋值操作。 实参类型与形参的声明类型不必严格一样。会存在隐式类型转换

函数的声明可以在调用前,也可以调用后

函数调用的退出阶段

在Go中,当一个函数调用返回后(比如执行了一个return语句或者函数中的最后一 条语句执行完毕), 此调用可能并未立即退出。一个函数调用从返回开始到最终退 出的阶段称为此函数调用的退出阶段(exiting phase)。

匿名函数

func() {
    println("hello")
}()

Go支持匿名函数

一个匿名函数在定义后可以被立即调用,如上示例

也可以被赋值给某个函数类型的值,从 而我们不必在定义完此匿名函数后立即调用它,而是可以在以后合适的时候再调用它。

内置函数

代码包和包引入

  • main函数必须位于名为main的package包中
  • 对于一个fmt.Println函数调用,任何两个相邻的实参的输出之间将被插入 一个空格字符,并且在最后将输出一个空行字符

    1| package main
    2|
    3| import "fmt"
    4| import "math/rand"
    5|
    6| func main() {
    7| fmt.Printf("下一个伪随机数总是%v。\n", rand.Uint32())
    8| }
    

这个例子多引入了一个math/rand标准库包。 此包是math标准库包中的一个子 包。 此包提供了一些函数来产生伪随机数序列。

  • math/rand 标准库包的引入名是rand。 rand.Uint32() 函数调用将返回一共uint32类型的随机数

代码包目录、代码包引入路径和代码包依赖关系

todo:这个,咱之后再系统地结合gopath 和 goroot来看下吧

init 函数

在程序运行时刻,在进入main入口函数之前,每个init函数在此包加载的时候将 被(串行)执行并且只执行一遍。

init函数可以有多个,串行执行

程序代码要素初始化顺序

引用包中的init函数—>引用包的加载—>当前代码包的init函数—>当前代码包加载

完整的引入声明语句形式

import fmt "fmt" // <=> import "fmt"
import rand "math/rand" // <=> import "math/rand"
import time "time" // <=> import "time"

用于区分两个包名一样的代码包

句点引入

一个完整引入声明语句形式的引入名importname可以是一个句点(.)。 这样的引 入称为句点引入。使用被句点引入的包中的导出代码要素时,限定标识符的前缀必 须省略。

1| package main
2|
3| import (
4| . "fmt"
5| . "time"
6| )
7|
8| func main() {
9| Println("Current time:", Now())
10| }	

每个非匿名引入必须至少使用一次

编译器会提示你的

表达式、语句和简单语句

不重要

基本流程控制语法

基本流程控制代码块

  1. if-else 条件分支代码块
  2. for 循环代码块
  3. switch-case 多条件分支代码块

特殊流程控制代码块

  1. 容器类型相关的 for-range 循环代码块
  2. 接口类型相关的 type-switch 多条件分支代码块
  3. 通道类型相关的 select-case 多分支代码块

if-else

一个if-else条件分支控制代码块的完整形式如下:

1| if InitSimpleStatement; Condition {
2| // do something
3| } else {
4| // do something
5| }

12| if n := rand.Int(); n%2 == 0 {
13| fmt.Println(n, "是一个偶数。")
14| } else {
15| fmt.Println(n, "是一个奇数。")
16| }

InitSimpleStatement 是初始化部分,可忽略,如果有初始化,变量域只包括当前if-else代码块中

for

for循环代码块的完整形式如下:

1| for InitSimpleStatement; Condition; PostSimpleStatement {
2| // do something
3| }

1| for i := 0; i < 10; i++ {
2| fmt.Println(i)
3| }

for 循环 如果条件表达式部分被省略,则编译器视其为true。

1| for i := 0; ; i++ { // 等价于:for i := 0; true; i++ {
2| if i >= 10 {
3| break
4| }
5| fmt.Println(i)
6| }
7|
8| // 下面这几个循环是等价的。
9| for ; true; {
10| }
11| for true {
12| }
13| for ; ; {
14| }
15| for {
16| }

循环体内声明的同名变量,是新变量。

1| for i := 0; i < 3; i++ {
2| fmt.Print(i)
3| i := i // 这里声明的变量i遮挡了上面声明的i。
4| // 右边的i为上面声明的循环变量i。
5| i = 10 // 新声明的i被更改了。
6| _ = i
7| }
//输出:012

协程、延迟函数调用、恐慌和恢复

协程

image-20220824175500201

并行计算属于特殊的并发计算。

Go 不支持创建系统线程,所以协程是一个Go程序内部唯一的并发实现方式。

我们只需 在一个函数调用之前使用一个go关键字,即可让此函数调用运行在一个新的协程之 中。 当此函数调用退出后,这个新的协程也随之结束了。我们可以称此函数调用为 一个协程调用(或者为此协程的启动调用)。 一个协程调用的所有返回值(如果存 在的话)必须被全部舍弃

并发同步

并发计算可能会导致数据竞争

数据竞争,在一个计算向一段内存写数据时,另一个计算在读此段内存数据,结果导 致读出的数据的完整性得不到保证;在一个计算向一段内存写数据的时候,另一个计算也向此段内存写数据,结果导致被写入的数据的完整性得不到保证。

调用不同的计算,控制他们访问资源的时间段,避免数据竞争发生,称为并发同步(或者数据同步)

当一个程序的主协程退出后,此程序也就退出了,即使还有一些其它协程在运行。

如何 确保主协程在这20条问候语都打印完毕之后才退出呢?

我们必须使用某种并发同步 技术来达成这一目标。 Go支持几种并发同步技术(第36章)。 其中, 通道(第21章)是最独特和最常用 的。 但是,为了简单起见,这里我们将使用sync标准库包中的WaitGroup来同步 上面这个程序中的主协程和两个新创建的协程。

WaitGroup类型有三个方法:Add、Done、Wait

  • ADD 用来注册新的需要完成的任务数
  • Done 用来通知某个任务已经完成了
  • 一个Wait方法调用将阻塞(等待)到所有任务都已经完成之后才继续执行其后的语句

    package main
    
    import (
    	"log"
    	"math/rand"
    	"sync"
    	"time"
    )
    
    var wg sync.WaitGroup
    
    func SayGreetings(greeting string, times int) {
    	for i := 0; i < times; i++ {
    		log.Println(greeting)
    		d := time.Second * time.Duration(rand.Intn(5)) / 2
    		time.Sleep(d)
    	}
    	wg.Done() // 通知当前任务已经完成。
    }
    
    func main() {
    	rand.Seed(time.Now().UnixNano())
    	log.SetFlags(0)
    	wg.Add(2) // 注册两个新任务。
    	go SayGreetings("hi!", 10)
    	go SayGreetings("hello!", 10)
    	wg.Wait() // 阻塞在这里,直到所有任务都已完成。
    //wg.Wait() 阻塞的是主协程,等两个新协程完成各自任务后,主协程切回运行状态。
    }
    
    

image-20220825171232554

注意,time.Sleep 或者等待系统调用返回的协程被认为是运行状态,而不是阻塞状态。

协程被创建后,会自动进入运行状态,协程只能从运行状态而不能从阻塞状态退出。 如果一个运行中的程序当前所有的协程都出 于阻塞状态,则这些协程将永远阻塞下去,程序将被视为死锁了。 当一个程序死锁 后,官方标准编译器的处理是让这个程序崩溃。

协程的调度

并非所有处于运行状况的协程都在执行。同一时刻,只能最多有和逻辑CPU一样多的协程在同时执行。runtime.NumCPU 查询当前程序可用的逻辑CPU数目

重申一下,睡眠和等待系统调用返回子状态被认为是运行状 态,而不是阻塞状态。

延迟函数调用

一个函数跟在一个defer 关键字后面,成为一个延迟函数调用。 和协程调用类似,被延迟的函数调用的所有返回值(如果存在)必须全部被舍弃。

当一个延迟调用语句被执行时,其中的延迟函数调用不会立即被执行,而是被推入 由当前协程维护的一个延迟调用队列。 当一个函数调用返回(此时可能尚未完全退 出)并进入它的退出阶段(第9章)后,所有在执行此函数调用的过程中已经被推入 延迟调用队列的调用将被按照它们被推入的顺序逆序被弹出队列并执行。 当所有这 些延迟调用执行完毕后,此函数调用也就完全退出了。

被延迟调用的函数会被推入一个延迟调用队列中,先进后调用;一个函数调用返回并进入它的推出阶段后,会有序按照调用队列顺序进行执行

一个延迟调用可以修改包含此延迟调用的最内层函数的返回值

func Triple(n int) (r int) {
	defer func() {
		fmt.Println(r)
		r += n // 修改返回值
		fmt.Println(r)
	}()
	return n + n // <=> r = n + n; return
	// return 10 ——> r=10 ——> 进入defer r=15 ——> return 15
	//这个函数先执行完return 再执行defer内部,很神奇。
}

协程和延迟调用的实参的估值时刻

延迟调用的实参,是在此延迟调用被推入延迟调用队列是被估值(确定的)。

匿名函数体内表达式是在此函数被执行时候才被估值的。

12| func(a, b int) {
13| println("a*a + b*b =", a*a + b*b) // a*a + b*b = 25
14| }(x, y) // 立即调用并传递两个实参。 把x,y传递给a,b

恐慌panic和恢复recover

Go不支持异常抛出和捕获,而是推荐使用返回值显式返回错误。 不过,Go支持一套 和异常抛出/捕获类似的机制。此机制称为恐慌/恢复(panic/recover)机制。

可用调用内置函数panic来产生一个恐慌以使当前协程进入恐慌状态。 进入恐慌状况是另一种使当前函数调用开始返回的途径。 一旦一个函数调用产生一 个恐慌,此函数调用将立即进入它的退出阶段。 如果一个协程在恐慌状态下退出,整个程序会崩溃。 通过在一个延迟函数调用之中调用内置函数recover,当前协程中的一个恐慌可以被消除,从而使当前协程进入正常状况。

恐慌后,当前函数进入退出状态,只会执行defer队列中的代码