Go语言中内置了错误接口提供简单方便的错误处理机制。请注意,异常(Exception)和错误(Error)在概念上来讲是不一样的。Go语言只有错误,没有异常,并提供了针对错误的处理机制。其他语言只有异常处理机制,没有错误处理机制。
错误指的是可能出现问题的地方出现了问题,比如打开一个文件时可能失败,这种情况在人们的意料之中。
异常指的是不应该出现问题的地方出现了问题,比如引用了空指针,这种情况在人们的意料之外。
错误是业务逻辑的一部分,而异常不是 。
如果程序遇到错误不处理,那么可能进一步的产生业务上的错误,比如给用户多扣钱了,或者进一步产生了异常;如果程序遇到异常不处理,那么结果就是进程异常退出。
Go语言中error
是一个接口类型,它的源码如下:
type error interface {
Error() string
}
在函数中,如果检测到了错误,可以使用errors.New()
返回错误信息。Go语言需要你自己详细判断错误发生情况,并返回错误信息。
实例:
package main
import (
"errors"
"fmt"
)
func divide(x, y int) (int, error) {
if y != 0 {
return x / y, nil
} else {
return 0, errors.New("Divisor cannot be zero")
}
}
func printRes(res int, e error) {
if e != nil {
fmt.Println(e)
} else {
fmt.Println(res)
}
}
func main() {
a := 10
b := 2
c := 0
res1, e1 := divide(a, b)
printRes(res1, e1)
res2, e2 := divide(a, c)
printRes(res2, e2)
}
// 5
// Divisor cannot be zero
也可以简单的使用fmt.Errorf()
定义错误信息,定义好的错误信息可以直接打印。
package main
import "fmt"
func main() {
var e error = fmt.Errorf("Error occur")
fmt.Println(e)
}
// Error occur
这里举了个简单的例子,fmt.Errorf()
的f
是格式化的意思,可以传入参数,然后格式化输出,例fmt.Error("%d", a)
。
自定义错误
自定义错误需要给出该错误的结构体定义并实现error
接口。
实例如下:
package main
import "fmt"
type divideZeroError struct {
divident int
divisor int
}
func (ed *divideZeroError) Error() string {
return "divisor zero error"
}
func divide(x, y int) (int, string) {
if y != 0 {
return x / y, ""
} else {
eData := divideZeroError {x, y}
errorMsg := eData.Error()
return 0, errorMsg
}
}
func main() {
a := 10
b := 2
c := 0
if res, errorMsg := divide(a, b); errorMsg == "" {
fmt.Println(res)
}
if _, errorMsg := divide(a, c); errorMsg != "" {
fmt.Println(errorMsg)
}
}
// 5
// divisor zero error
错误捕获
注意,很多博客会称为异常捕获,这个不太严谨的,因为Go语言没有异常这个概念,因此应该叫错误捕获。
类似于try-catch-finally
,Go语言中错误捕获采用的是panic()-defer-recover()
。try-catch-finally
是三个关键字,panic()-defer-recover()
是两个函数加一个关键字,panic
对应的中文翻译为恐慌
、defer
对应的中文翻译为推迟
、recover
对应的中文翻译为恢复
。
Go语言中可以使用panic()
函数抛出一个错误,然后在defer
中通过recover()
函数捕获异常进程后续处理。
panic()
内置函数,用于抛出错误,定义如下:
func panic(v interface {})
defer
预留关键字,延迟指定函数的执行。通常在资源释放,链接关闭、函数结束时调用。多个defer为堆栈结构,后进先出。defer
可用于异常抛出后的处理。
defer
用于添加函数结束时执行的语句,注意时添加不是指定,defer
是动态的。
recover()
内置函数,用于获取异常(类似java中的catch),多次调用时只有第一次能获取值,定义如下:
func recover() interface{}
实例
package main
import "fmt"
func makeError() {
fmt.Println("start to make a error")
panic("error occur")
fmt.Println("end to this error")
}
func catchError1() {
fmt.Println("start to catch error, this is the first catch function")
err := recover()
if err != nil {
fmt.Println("dispose first: " + fmt.Sprintf("%s", err))
}
fmt.Println("end to catch error, this is the first catch function")
}
func catchError2() {
fmt.Println("start to catch error, this is the second catch function")
err := recover()
if err != nil {
fmt.Println("dispose second: " + fmt.Sprintf("%s", err))
}
fmt.Println("end to catch error, this is the second catch function")
}
func invokeError() {
fmt.Println("the method may invoke error start to run")
defer catchError1() // 声明defer才会去捕获异常
defer catchError2()
makeError()
makeError()
fmt.Println("the method may invoke error start to end")
}
func testMethod() {
fmt.Println("test function start")
invokeError()
fmt.Println("test function end")
}
func main() {
testMethod()
}
结果:
test function start
the method may invoke error start to run
start to make a error
start to catch error, this is the second catch function
dispose second: error occur
end to catch error, this is the second catch function
start to catch error, this is the first catch function
end to catch error, this is the first catch function
test function end
请注意,关键字defer后面的函数都会被推迟执行,因此先执行了第一个makeError()
,然后再执行了catchError1()
和catchError2()
,因为defer的堆栈结构,后进先出,因此先执行catchError2()
,即第二个捕获异常的函数中的recover()
成功捕获到了panic()
抛出的异常,并正常处理。catchError2
执行完毕后,catchError1()
开始执行,此时没有捕获到异常,函数invokeError()
执行终止,后续的语句没有被执行。
也就是说defer
之后,函数将结束执行,因此有人把defer
和C++的析构函数类比,称C++的析构函数析构类,Go的defer
析构函数。这种说法是不准确的,析构函数的重要作用是对象生命周期结束前执行一些操作,并最终释放对象占据的存储空间,而defer
只是在函数结束前执行一些操作,并不会释放空间。
错误处理优劣
知乎讨论:Go 语言的错误处理机制是一个优秀的设计吗?
这个有一些争议,设计者的初衷是try-catch-finally
将异常和控制结构混在一起容易使得代码变得混乱。因为开发者很容易滥用异常,以至于小小的错误都会去抛出异常。在Go语言中,函数可以有多个返回值,因此可以返回的时候带上错误。因此Go不使用异常来代替错误,也不使用控制流程,只有遇到真的错误而不是异常的时候,Go才有去处理它。
参考文献
golang捕获异常
go中异常处理