Go语言的错误处理
首先区分错误和异常,错误是正常的业务逻辑,例如错误码,错误信息等,可以代码中处理好,进行日志记录;而异常则更为致命,例如段错误,访问野指针等,一般会造成程序的崩溃。
Go 语言通过内置的错误接口提供了非常简单的错误处理机制。 其定义如下:
type error interface{
Error() string
}
自定义的类型可以实现这个方法,来得到相应的错误处理函数
以打开文件失败返回错误为例:
import( "fmt" "os" )
func main(){
file,err:=os.Open("test.txt")
if err!=nil{
fmt.Println(err.Error()) //output no such file or directory
}
}
os.Open函数返回的err其实是实现了接口error的os.Patherror的指针,是一个error接口类型,所以可以进行断言:
import( "fmt" "os" )
func main(){
file,err:=os.Open("test.txt")
if val,ok:=err.(*os.Patherror){
fmt.Println(val.Error()) //output no such file or directory
}
}
因此,这提供了一种处理错误的思路,我们可以自定义一个结构体,这个结构体的字段是我们在遇到错误时需要的信息字段,例如errmsg、errcode,然后对这个结构体实现接口error,在编写函数返回值时,返回类型指定为error类型,返回该结构体的指针(如果不是指针,变成值传递,临时变量会销毁),然后进行类型断言,确保是某类错误后,再去获取错误信息。返回的error对象应该是最好是最后一个变量(规范化)。
type s1 struct {
radius float32
}
func (s s1) Error() string {
return "error from s1"
}
func test_error() (int, error) {
return -1, new(s1)
}
func main(){
_, myerr := test_error()
if val, ok := myerr.(*s1); ok {
fmt.Println(val.Error())
}
}//output error from s1
Go语言异常处理
在程序编写时,尽量使用错误而不是异常处理,因为异常往往是程序要崩溃了。
常见的异常为访问野指针导致段错误,或者程序指定要绑定某个端口,但反复重试后仍无法绑定,则抛出异常。
go中抛出异常使用的函数为panic函数,这个函数的作用很像C++中的throw,运行时程序崩溃其实也是调用了panic函数。panic是会不断往外层函数传递的,直到有函数处理了它,或者没有函数处理进而程序退出。
func panic(v interface{})
当调用panic函数后程序终止了,会打印传入给panic的参数,同时还会打印函数调用栈,类同backtrace,返回堆栈信息
func main(){
panic("exit at start")
}
//output
panic: exit at start
goroutine 1 [running]:
main.main()
/root/learn_go/var.go:80 +0x39
exit status 2
延迟函数/延迟语句 关键字:defer
在异常情况下(调用panic函数或程序崩溃时):使用defer关键字,后面跟的语句、函数会在panic函数调用后/程序退出前执行,类似于apue中的atexit函数,执行的顺序和defer语句的执行顺序相反(栈结构)
defer语句虽然是在其所在函数返回后再进行执行,但如果defer语句中的表达式求值在程序运行到该语句时就执行了求值,例如defer函数的参数在此时已经确定了,即使后面更改了传入的参数的值,也没有影响:
func df(a, b int) int {
return a + b
}
func main(){
a := 10
b := 20
defer fmt.Println(df(a, b))
a += b
fmt.Println(a + b)
}//output
50 //main中最后一行打印的结果
30 //defer函数的执行结果
func test_defer(){
defer fmt.Println("defer in test_defer")
panic("panic in test_defer")
}
func main(){
defer fmt.Println("defer in main")
}
//output
defer in test_defer
defer in main
panic: panic in test_defer
goroutine 1 [running]:
main.test_defer()
/root/learn_go/var.go:81 +0x95
main.main()
/root/learn_go/var.go:86 +0xb1
exit status 2
可以看到,每个defer都会把语句压入栈中,等到程序退出前(正常退出或者panic退出)再从栈顶逐一执行,先压入了main的defer,后压入了test_defer函数中的defer语句。
在执行完所有的defer语句后,再执行panic函数。
正常情况下:在某个函数中使用了defer语句,则在该函数return前,会执行所有在该函数中注册的每个defer语句,然后再return。只要函数不return或者异常结束,该函数内声明的defer语句都不会执行。
func test_defer() string {
defer fmt.Println("defer in test_defer")
defer fmt.Println("defer in test_defer 2")
return "hello"
}
func main() {
defer fmt.Println("defer in main")
fmt.Println(test_defer())
}
//output
defer in test_defer 2
defer in test_defer
hello
defer in main
在正常情况下,defer语句的执行顺序是:先return,后再执行defer语句。
func test_defer() int {
i := 0
defer func() {
i++
fmt.Println("first state defer", i)
}()
defer func() {
i++
fmt.Println("second state defer", i)
}()
return i
}
func main(){
fmt.Println(test_defer())
}
//output
second state defer 1
first state defer 2
0
recover函数
程序在panic之后,调用recover可以恢复程序,不让程序崩溃退出,有点像C++的catch异常的感觉。只要处理了异常,就不会使得程序退出。
A call to recover stops the unwinding and returns the argument passed to panic.
recover函数返回值是传给panic函数的参数,返回类型就是interface{},这个类型就是panic函数形参类型。
只能够在延迟函数中调用recover函数来恢复程序,直接调用是不行的:
func test_defer(){
defer func(){
recover()
fmt.Println("recovered")
}()
panic("panic in test_defer")
}
func main(){
test_defer()
fmt.Println("in main")
}
//output
recovered
in main
在与recover搭配的情况下,panic其实是提前中断了其所在的函数(panic代码所在行后所有的语句是不会执行的),其会在所在的函数体内,找defer函数中是否有调用recover,没有的话就返回上一层函数,继续寻找,如果都没找到,则程序结束,否则程序恢复正常运行,不会退出:
func test(){
panic("panic in test")
}
func main(){
defer func(){
recover()
fmt.Println("recovered")
}()
fmt.Println("normal output")
}
//output
recovered
可以看到上述代码中,即使recover从panic中恢复了,但仍然没有输出normal output,这是因为test函数中没有recover,panic传到了外层main函数,main函数提前终止,然后才调用了defer语句,才有了接下来的输出。
除了自己调用panic,运行时也会出现panic,例如未知的情况下访问了野指针,也可以通过recover来恢复,不过这需要在可能出现panic的函数体的第一句就写好延迟函数:
func may_panic(){
defer func(){
recover()
}
/* code */
}
A call to recover stops the unwinding and returns the argument passed to panic. Because the only code that runs while unwinding is inside deferred functions, recover is only useful inside deferred functions.
下面的例子可用于理解为什么defer函数是在return之后执行的
// Error is the type of a parse error; it satisfies the error interface.
type Error string
func (e Error) Error() string {
return string(e)
}
// error is a method of *Regexp that reports parsing errors by
// panicking with an Error.
func (regexp *Regexp) error(err string) {
panic(Error(err))
}
// Compile returns a parsed representation of the regular expression.
func Compile(str string) (regexp *Regexp, err error) {
regexp = new(Regexp)
// doParse will panic if there is a parse error.
defer func() {
if e := recover(); e != nil {
regexp = nil // Clear return value.
err = e.(Error) // Will re-panic if not a parse error.
}
}()
return regexp.doParse(str), nil
}
上面这个例子可能存在一个re-panic现象,也即如果e.(Error)这个类型判断失败,会再一次引起panic,这就是所谓的在处理异常的过程中又发生了异常,这时候就会再往上找recover函数,同时,re-panic的每一次panic信息都会打印。
defer函数在return之后调用的原因:上面这个函数在return的地方调用了函数,而这个函数可能出现panic,此时defer函数才可以发挥作用,执行recover;否则这个panic就没办法在Compile函数内部被捕捉。
此外,defer函数可以修改命名的返回值,可以看到defer函数内部,在panic后,将regexp赋值为nil。
实践例子:
func derfer_test() (s string) {
defer func() {
s = "changed in defer"
}()
return "Not change"
}
func main(){
fmt.Println(defer_test())
}
//output
changed in defer
使用panic和recover的建议,这一套应该在package中处理,不应该暴露给用户,应是package出现了panic后,使用recover返回一个error给用户,避免暴露panic信息给用户。
Useful though this pattern is, it should be used only within a package. Parse turns its internal panic calls into error values; it does not expose panics to its client. That is a good rule to follow.