错误处理
Go语言错误处理的设计,虽然不能说完全是个垃圾,但也是相当欠考虑的。Go语言鼓励使用显式的错误检查和处理方式,它并没有一个像Java一样特别完善的异常处理机制,还是使用类似C语言中采用返回值和if
进行判断,这种设计尽管在可读性和可维护性方面没有太大问题,但却让我们的代码丑陋无比。这篇笔记我们介绍Go语言中的错误处理机制。
打印和抛出异常类型
error接口
Go语言提供了error
接口,用于表示异常信息,其定义如下:
type error interface {
Error() string
}
所有实现了Error()
方法的类型都被视为异常类型,常见的错误处理模式是返回一个error
类型的值来表示是否发生了错误。下面例子调用了strconv.Atoi()
,如果转换成功,err
为nil
,否则表示转换出错了,我们可以调用Error()
方法输出其信息。
package main
import (
"fmt"
"strconv"
)
func main() {
i, err := strconv.Atoi("a")
if err != nil {
// 如果出错,打印错误消息,并中断处理流程
fmt.Println("转换出错:", err.Error())
return
}
fmt.Println(i)
}
当然,这里为了简单起见我们的代码中对于异常的处理就是使用fmt.Println()
函数打印,并使用return
中断函数的执行,实际开发中我们可能还需要编写复杂的异常处理和恢复逻辑。此外,使用fmt.Println()
函数打印消息也不是个好的方式,实际开发中我们通常更倾向于使用日志库来输出错误信息,这里就不多介绍了。
抛出异常
我们自己定义的函数或方法内也可以通过返回值的形式指定抛出异常,调用处也可以通过这个返回值接收异常。下面是一个例子。
package main
import (
"errors"
"fmt"
)
type Person struct {
name string
age int
}
func (p *Person) SayHello() (string, error) {
if p.name == "" && p.age == 0 {
return "", errors.New("未初始化!")
} else {
return fmt.Sprintf("Hello, I am %v, %v years old.", p.name, p.age), nil
}
}
func main() {
p := &Person{}
s, err := p.SayHello()
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println(s)
}
}
代码中,errors.New()
方法能将一个字符串消息包装成error
类型,我们调用处的代码可以用err.Error()
取出该消息。
使用panic和recover
虽然Go不鼓励使用panic
和recover
进行一般的错误处理,但在某些情况下例如不可恢复的错误,我们可以使用它们。panic
会主动触发程序“崩溃”,而recover
可以捕获到这个崩溃并进行处理,下面是一个例子。
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
panic("something went wrong")
}
recover
通常结合defer
使用。defer
用于延迟执行某个函数,在defer
后面的函数调用会在panic
发生后被调用,而recover
则用于捕获panic
并恢复程序的执行。
当一个Goroutine发生panic
并且没有recover
来捕获时,该Goroutine将停止执行,并打印出一个栈跟踪信息。这些信息包括panic
的消息以及导致panic
的函数调用堆栈。如果panic
发生在子Goroutine中,且没有被子Goroutine中的recover
捕获,panic
不会直接传播到父Goroutine。父Goroutine会继续执行,除非父Goroutine依赖子Goroutine的结果并访问一个未能正确初始化的共享资源;如果panic
发生在主Goroutine中,并且没有被recover
捕获,整个程序将崩溃,并打印出panic消息和栈跟踪信息。