Yayako's Blog

「Shooting for the stars when I couldn't make a killing.」

仅作java2go的简单记录,有错误烦请提出

合集整理:Java转Go的学习-合集

demo已上传git:https://gitee.com/yayako/go-learning.git

defer、panic、recover关键字

defer

延迟关键字,会将修饰的代码段延迟执行,官方描述:

A “defer” statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.

执行时机是 defer语句所在函数执行完毕时 ——函数执行return语句,或函数执行到末尾,或相关goroutine发生panic时。并且即使程序发生异常,也会执行。

举个简单的例子

1
2
3
4
5
6
7
8
9
10
11
func main() {
deferTest() // 开始 结束 3 2 1
}

func deferTest() {
fmt.Println("开始")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("结束")
}

也可以延迟匿名自执行函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
deferTest() // 1开始 2结束 1开始 2结束 654321
}

func deferTest() {
fmt.Print("1开始 ")
defer fmt.Println(1)
defer fmt.Print(2)
defer fmt.Print(3)
fmt.Print(" 2结束 ")
// 也可以将延迟代码放到匿名函数中
fmt.Print("1开始 ")
defer func() {
defer fmt.Print(4)
defer fmt.Print(5)
defer fmt.Print(6)
}()
fmt.Print("2结束 ")
}

但需要注意, defer在命名返回值和匿名返回值中的表现是不一样的 ,如下例子中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main() {
// - 匿名返回函数
// 控制台依次输出:1 0
fmt.Println(f1()) // ③ 打印输出返回的0

// - 命名返回值函数
// 控制台依次输出:1 1
fmt.Println(f2())
}

// 匿名返回值函数
func f1() int {
var a int
defer func() { // ② a自增为1,并输出
a++
fmt.Println("defer ", a)
}()
return a // ① 返回0
}

// 命名返回值函数
func f2() (a int) {
defer func() { // ②
a++
fmt.Println("defer ", a)
}()
return a // ①
}

匿名返回值函数的执行结果可以预见(执行顺序①②③),但是为什么命名返回值函数的执行结果会有所不同呢?

首先要知道 return的底层执行: 在go中,return并不是一个原子操作,而是分为 赋值RET指令 两个操作,后者可以理解为是创建一个新的变量ret,并将我们return的值赋值给ret,再将ret返回。

而defer语句执行时机就在赋值之后,RET指令执行之前。

在匿名返回值函数中,相当于按如下顺序执行

  1. 创建了ret变量并将其赋值为0
  2. 执行defer块,a++,但是此时的a与第1步中的ret并无关系,因为int是值类型而非引用类型
  3. 返回ret,此时ret的值仍旧为0

而在命名返回值函数中,则是省去了创建ret变量的过程,因为 返回变量已经被指定并初始化为零值

  1. 执行defer块,a++,此时a=1
  2. 返回a,此时a的值为1

这也是为什么命名返回值函数中无需在return之后带值的原因

另一点要注意的是: defer注册”要延迟执行的函数”时,这个函数所有的参数的值都需要预先被确定

这句话是什么意思呢?不如先查看如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
x := 1
y := 2
defer deferCalc("AA", x, deferCalc("A", x, y))
x = 10
defer deferCalc("BB", x, deferCalc("B", x, y))
y = 20
}

func deferCalc(index string, a, b int) int {
res := a + b
fmt.Println(index, a, b, res)
return res
}

执行结果如下

1
2
3
4
5
A 1 2 3
B 10 2 12

BB 10 12 22
AA 1 3 4

来分析一下执行顺序

当执行到第一个defer语句时,会进行函数AA的注册,所以需要预先确定函数AA的参数,此时 a = x = 1 已经确定,而b的值为 deferCalc("A", x, y) ,故需要先执行函数A,得到 b = deferCalc("A", x, y) = 3 。a、b由此确定下来,a = 1, b = 3,函数AA注册成功。

当执行到第二个defer语句时,同理会进行函数BB的注册,确定此时 a = x = 10b = deferCalc("B", x, y) = 12 , 由此函数BB注册成功。

当函数AA、BB全部注册完毕,并且当前main函数执行完毕,会依次执行注册的defer函数BB、AA,依次输出4、22。

完整时间线整理如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
顺序:注册AA -> 执行A -> 注册BB -> 执行B -> 执行BB -> 执行AA
x := 1
y := 2
注册AA // 注册时需要确定 x,deferCalc("A", x, y)) 的值
x = 1
执行A:deferCalc("A", x, y) => A 1 2 3
deferCalc("A", x, y) = 3
x = 10
注册BB // 注册时需要确定 x,deferCalc("B", x, y)) 的值
x = 10
执行B:deferCalc("B", x, y) => B 10 2 12
deferCalc("B", x, y) = 12
y = 20
执行BB:deferCalc("BB", x, deferCalc("B", x, y)) => BB 10 12 22
执行AA:deferCalc("AA", x, deferCalc("A", x, y)) => AA 1 3 4

defer的作用

还记得java中有try-catch-finally块,不论程序是否执行成功,最终都会执行finally块的语句,而我们主要在这一块写的,就是相关资源的释放,例如锁的释放、连接的释放。

而defer也是主要起这一个主要的作用——在函数执行完毕后,及时地释放资源。

panic-cover

go中目前没有异常处理机制,但是可以通过panic-cover模式来处理错误

  • panic用于抛出异常,recover用于接收异常;
  • panic可以在任何地方引发,而recover只有在defer调用的函数中有效。

一个简单的panic使用例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func panicTest() {
/*
panic: *** panic ***

goroutine 1 [running]:
main.panicTest(...)
golang-learning/src/main/ex7.go:79
main.main()
golang-learning/src/main/ex7.go:74 +0x585
*/
fmt.Println("before panic")
panic("*** panic ***")
fmt.Println("after panic")
}

类比一下java的throw机制,区别在于java还需要在函数定义时抛出异常,并且这块代码编译就过不去。

1
2
3
4
5
public static void main(String[] args) throws Exception {
System.out.println("before throwing");
throw new Exception("*** throw ***");
System.out.println("after throwing"); // java: 无法访问的语句
}

一个简单的recover使用的例子

1
2
3
4
5
6
7
8
9
10
func recoverTest(a, b int) {
defer func() {
// recover接收异常,只能在defer调用函数中有效
err := recover()
if err != nil {
fmt.Println("error:", err)
}
}()
fmt.Println(a / b)
}

我们取 b = 0,查看执行结果

1
error: runtime error: integer divide by zero

异常成功接收,并且没有停止当前程序的运行。

类比Java的try-catch,Java的异常体系更加的庞大复杂,而在go中则无需关心异常的类型,更加简介的同时也受限于此的感觉吧。

1
2
3
4
5
6
7
void test(int a, int b) {
try {
System.out.println(a / b);
} catch (Exception e) {
System.out.println(e);
}
}

结合使用defer、panic、recover

不如模拟一个写入文件操作,且要求改写异常提示,如果文件不存在则提示”打开文件失败“,如果写入文件失败,则提示”写入文件失败“。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func write2Txt(path string) {
file, err := os.OpenFile(path, os.O_WRONLY, 666)

defer func() { // finally
file.Close()
myErr := recover() // catch
if myErr != nil {
fmt.Println("error: ", myErr)
}
}()

if err != nil {
panic("打开文件失败")
}

_, err = file.WriteString("直接写入字符串\r\n")
if err != nil {
panic("写入文件失败")
}
}

评论