Go基础-错误处理与异常

摘要

本文内容转自网络,个人学习记录使用,请勿传播

Go 语言的异常处理语法绝对是独树一帜,一方面它鼓励你使用 C 语言的形式将错误通过返回值来进行传递,另一方面它还提供了高级语言一般都有的异常抛出和捕获的形式,但是又不鼓励你使用这个形式。后面我们统一将返回值形式的称为「错误」,将抛出捕获形式的称为「异常」。

错误接口

Go 语言规定凡是实现了错误接口的对象都是错误对象,这个错误接口只定义了一个方法。

1
2
3
type error interface {
Error() string
}

注意这个接口的名称,它是小写的,是内置的全局接口。通常一个名字如果是小写字母开头,那么它在包外就是不可见的,不过 error 是内置的特殊名称,它是全局可见的。

编写一个错误对象很简单,写一个结构体,然后挂在 Error() 方法就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

type SomeError struct {
Reason string
}

func (s SomeError) Error() string {
return s.Reason
}

func main() {
var err error = SomeError{"something happened"}
fmt.Println(err)
}

---------------
something happened

Go 语言内置了一个通用错误类型,在 errors 包里。这个包还提供了一个 New() 函数让我们方便地创建一个通用错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package errors

func New(text string) error {
return &errorString{text}
}

type errorString struct {
s string
}

func (e *errorString) Error() string {
return e.s
}

// 这个结构体 errorString 是首字母小写的,意味着我们无法直接使用这个类型的名字来构造错误对象,而必须使用 New() 函数

var err = errors.New("something happened")

// 如果你的错误字符串需要定制一些参数,可使用 fmt 包提供了 Errorf 函数

var thing = "something"
var err = fmt.Errorf("%s happened", thing)

错误处理

Go 语言里面不会抛异常,而是以返回值的形式来通知上层逻辑来处理错误

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
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import "os"
import "fmt"

func main() {
// 打开文件
var f, err = os.Open("main.go")
if err != nil {
// 文件不存在、权限等原因
fmt.Println("open file failed reason:" + err.Error())
return
}
// 推迟到函数尾部调用,确保文件会关闭
defer f.Close()
// 存储文件内容
var content = []byte{}
// 临时的缓冲,按块读取,一次最多读取 100 字节
var buf = make([]byte, 100)
for {
// 读文件,将读到的内容填充到缓冲
n, err := f.Read(buf)
if n > 0 {
// 将读到的内容聚合起来
content = append(content, buf[:n]...)
}
if err != nil {
// 遇到流结束或者其它错误
break
}
}
// 输出文件内容
fmt.Println(string(content))
}

-------
package main

import "os"
import "fmt"
.....
  • os.Open()f.Read() 函数返回了两个值,Go 语言不但允许函数返回两个值,三个值四个值都是可以的
  • 只不过 Go 语言普遍没有使用多返回值的习惯,仅仅是在需要返回错误的时候才会需要两个返回值。
  • 除了错误之外,还有一个地方需要两个返回值,那就是字典,通过第二个返回值来告知读取的结果是零值还是根本就不存在。
1
var score, ok := scores["apple"]
  • 读文件操作f.Read(),它会将文件的内容往切片里填充,填充的量不会超过切片的长度(注意不是容量)。如果将缓冲改成下面这种形式,就会死循环!
1
var buf = make([]byte, 0, 100)

Redis 中错误处理

1
go get github.com/go-redis/redis
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import "fmt"
import "strconv"
import "github.com/go-redis/redis"

func main() {
// 定义客户端对象,内部包含一个连接池
var client = redis.NewClient(&redis.Options {
Addr: "localhost:6379",
})

// 定义三个重要的整数变量值,默认都是零
var val1, val2, val3 int

// 获取第一个值
valstr1, err := client.Get("value1").Result()
if err == nil {
val1, err = strconv.Atoi(valstr1)
if err != nil {
fmt.Println("value1 not a valid integer")
return
}
} else if err != redis.Nil {
fmt.Println("redis access error reason:" + err.Error())
return
}

// 获取第二个值
valstr2, err := client.Get("value2").Result()
if err == nil {
val2, err = strconv.Atoi(valstr2)
if err != nil {
fmt.Println("value1 not a valid integer")
return
}
} else if err != redis.Nil {
fmt.Println("redis access error reason:" + err.Error())
return
}

// 保存第三个值
val3 = val1 * val2
ok, err := client.Set("value3",val3, 0).Result()
if err != nil {
fmt.Println("set value error reason:" + err.Error())
return
}
fmt.Println(ok)
}

------
OK

因为 Go 语言中不轻易使用异常语句,所以对于任何可能出错的地方都需要判断返回值的错误信息。上面代码中除了访问 Redis 需要判断之外,字符串转整数也需要判断。

另外还有一个需要特别注意的是因为字符串的零值是空串而不是 nil,你不好从字符串内容本身判断出 Redis 是否存在这个 key 还是对应 key 的 value 为空串,需要通过返回值的错误信息来判断。代码中的 redis.Nil 就是客户端专门为 key 不存在这种情况而定义的错误对象。

defer关键字

defer关键字的作用是当外围函数返回之后才执行被推迟的函数。在文件输入输出操作中经常可以见到defer关键字,因为它使您不必记住何时关闭已打开的文件:defer关键字调用文件关闭函数关闭已打开的文件时,可以紧靠着文件打开函数之后。

defer函数在外围函数返回之后,以后进先出(LIFO)的原则执行。简单点说,在一个外围函数中有3个defer函数:f1()最先出现,然后f2(),最后f3(),当外围函数执行返回之后,f3()最先被执行,接着是f2(),最后是f1()

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
29
30
31
32
33
34
35
36
37
package main

import (
"fmt"
)

func d1() {
for i := 3; i > 0; i-- {
defer fmt.Print(i, " ")
}
}

func d2() {
for i := 3; i > 0; i-- {
defer func() {
fmt.Print(i, " ")
}()
}
fmt.Println()
}

func d3() {
for i := 3; i > 0; i-- {
defer func(n int) {
fmt.Print(n, " ")

}(i)
}
}

func main() {
d1()
d2()
fmt.Println()
d3()
fmt.Println()
}

多个 defer 语句

有时候我们需要在一个函数里使用多次 defer 语句。比如拷贝文件,需要同时打开源文件和目标文件,那就需要调用两次 defer f.Close()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"
import "os"

func main() {
fsrc, err := os.Open("source.txt")
if err != nil {
fmt.Println("open source file failed")
return
}
defer fsrc.Close()
fdes, err := os.Open("target.txt")
if err != nil {
fmt.Println("open target file failed")
return
}
defer fdes.Close()
fmt.Println("do something here")
}

需要注意的是 defer 语句的执行顺序和代码编写的顺序是反过来的,也就是说最先 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
29
30
31
32
package main

import "fmt"
import "os"

func main() {
fsrc, err := os.Open("source.txt")
if err != nil {
fmt.Println("open source file failed")
return
}
defer func() {
fmt.Println("close source file")
fsrc.Close()
}()

fdes, err := os.Open("target.txt")
if err != nil {
fmt.Println("open target file failed")
return
}
defer func() {
fmt.Println("close target file")
fdes.Close()
}()
fmt.Println("do something here")
}

--------
do something here
close target file
close source file

异常与捕捉

Go 语言提供了 panicrecover 全局函数让我们可以抛出异常、捕获异常。它类似于其它高级语言里常见的 throw try catch 语句,但是又很不一样,比如 panic 函数可以抛出来任意对象。

严格来说,panic()是一个内置的Go函数,它终止Go程序的当前流程并开始panicking! 另一方面,recover()函数也是一个内置的Go函数,允许你收回那些使用了panic()函数的goroutine的控制权。

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
29
30
31
32
33
34
35
36
37
package main

import "fmt"

var negErr = fmt.Errorf("non positive number")

func main() {
fmt.Println(fact(10))
fmt.Println(fact(5))
fmt.Println(fact(-5))
fmt.Println(fact(15))
}

// 让阶乘函数返回错误太不雅观了
// 使用 panic 会合适一些
func fact(a int) int{
if a <= 0 {
panic(negErr)
}
var r = 1
for i :=1;i<=a;i++ {
r *= i
}
return r
}

-------
3628800
120
panic: non positive number

goroutine 1 [running]:
main.fact(0xfffffffffffffffb, 0x1)
/Users/qianwp/go/src/github.com/pyloque/practice/main.go:18 +0x77
main.main()
/Users/qianwp/go/src/github.com/pyloque/practice/main.go:10 +0xf4
exit status 2

上面的代码抛出了 negErr,直接导致了程序崩溃,程序最后打印了异常堆栈信息。下面我们使用 recover 函数来保护它,recover 函数需要结合 defer 语句一起使用,这样可以确保 recover() 逻辑在程序异常的时候也可以得到调用。

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
29
30
31
32
33
package main

import "fmt"

var negErr = fmt.Errorf("non positive number")

func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("error catched", err)
}
}()
fmt.Println(fact(10))
fmt.Println(fact(5))
fmt.Println(fact(-5))
fmt.Println(fact(15))
}

func fact(a int) int{
if a <= 0 {
panic(negErr)
}
var r = 1
for i :=1;i<=a;i++ {
r *= i
}
return r
}

-------
3628800
120
error catched non positive number

输出结果中的异常堆栈信息没有了,说明捕获成功了,不过即使程序不再崩溃,异常点后面的逻辑也不会再继续执行了。

panic 抛出的对象未必是错误对象,而 recover() 返回的对象正是 panic 抛出来的对象,所以它也不一定是错误对象。

1
2
func panic(v interface{})
func recover() interface{}

我们经常还需要对 recover() 返回的结果进行判断,以挑选出我们愿意处理的异常对象类型,对于那些不愿意处理的,可以选择再次抛出来,让上层来处理。

1
2
3
4
5
6
7
8
9
defer func() {
if err := recover(); err != nil {
if err == negErr {
fmt.Println("error catched", err)
} else {
panic(err) // rethrow
}
}
}()

Go 语言官方表态不要轻易使用 panic recover,除非你真的无法预料中间可能会发生的错误,或者它能非常显著地简化你的代码。简单一点说除非逼不得已,否则不要使用它。

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
29
package main

import (
"fmt"
)

func a() {
fmt.Println("Inside a()")
defer func() {
if c := recover(); c != nil {
fmt.Println("Recover inside a()!")
}
}()
fmt.Println("About to call b()")
b()
fmt.Println("b() exited!")
fmt.Println("Exiting a()")
}

func b() {
fmt.Println("Inside b()")
panic("Panic in b()!")
fmt.Println("Exiting b()")
}

func main() {
a()
fmt.Println("main() ended!")
}