Go 的错误异常处理,一直都是一个非常好玩的话题。
如果你习惯了 try catch 这样的语法后,会觉得处理错误真简单,然后你再来接触 Go 的错误异常,你会发现他好复杂啊,怎么到处都是 error,到处都需要处理 error。
所以如果你去一些论坛,或许喷得最多的就是这个点了。
一、Go 的约定
首先咱们需要知道 Go 语言里面有个约定,就是一个方法的返回参数,我们通常习惯的把错误当最后一个参数返回。
这虽然官方在这点上没有做硬性规定,但是大家也都习惯这么做,所以大家在写代码时就尽量不要去违反了哈,咱就是放第一个,咱就是玩,估计会被骂死的。
至于为啥 Go 要这样去设计处理异常,咱们这种干饭人事就不去分析了,官方怎么设计咱们就怎么遵守就好了。
二、简单错误创建
Go 的标准库里面为我们提供了两种使用字符串快速创建错误的方式。
1、 errors
我们可以使用 errors 包的 New 方法,传入一个字符串快速地创建。
var e error
e = errors.New("我是错误")
2、fmt
可能大多数同学都习惯用 fmt 去输出一些内容,同样他还能为我们创建错误。
var e error
e = fmt.Errorf("%s", "我还是错误")
相比 errors 包,fmt 还支持格式化字符串输出。
所以从这个角度可以看到,其实错误对 Go 语言来说,其实就是一段字符串。
三、哨兵错误
接下来我们分享 Go 中最常用的设计 error 的方式,那就是哨兵模式。
怎么去理解呢?
就像童话故事里一座城堡,在城堡的一些关卡,总会安排各种各样的哨兵,他们不同哨兵负责的事不同。
所以我们通常会在一个包里面设置一些标志性的错误,方便调用者对错误做更好的处理。
拿我们常用的 GORM 这个库吧,我们在查询某条数据的时候,如果没找到这条数据,不知道你是怎么判断的。
其实官方为我们提供了错误哨兵,在源码 github.com/jinzhu/gorm/errors.go
中:
var (
// ErrRecordNotFound returns a "record not found error". Occurs only when attempting to query the database with a struct; querying with a slice won't return this error
ErrRecordNotFound = errors.New("record not found")
// ErrInvalidSQL occurs when you attempt a query with invalid SQL
ErrInvalidSQL = errors.New("invalid SQL")
// ErrInvalidTransaction occurs when you are trying to `Commit` or `Rollback`
ErrInvalidTransaction = errors.New("no valid transaction")
// ErrCantStartTransaction can't start transaction when you are trying to start one with `Begin`
ErrCantStartTransaction = errors.New("can't start transaction")
// ErrUnaddressable unaddressable value
ErrUnaddressable = errors.New("using unaddressable value")
)
所以我们就可以直接通过返回的 error 来判断是不是没找到数据,下面我写一份假代码:
g,_ := gorm.Open()
e = g.Find().Error
if e == gorm.ErrRecordNotFound {
fmt.Println("没找到")
}
其实这样用 == 比较是有坑的,后面我们会讲到。
所以如果我们在写我们的模块的时候,也可以这样去设计我们的错误。
虽然这种设计模式网上也有很多人说不好,因为他建立起了两个包之间的依赖,说人话就是,如果我们要比较错误,就必须导入错误所在的包。
反正任何设计都会有人说好有人说坏,大家理智看到就好了。
四、对错误进行编程
我们需要时刻记住,Go 语言中错误其实就是一串字符串。
所以我们尽量避免去比较 error.Error() 输出的值,因为他正常情况下不是给我们人看的,而是给程序看的,同时方便我们调试。
所以,Go 里面的错误其实我们可以进行一系列的编程。
Go 语言中的错误定义是一个借口,只要是声明了 Error() string
这个方法,就意味着他就可以判定他是一个错误。
这是 Go 中的错误定义源码:
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
如果官方的错误,并不能满足你的需求,咱们也可以自定义。
1、创建错误
我们先来使用常量去创建自定义错误吧:
type MyError string
func (this MyError) Error() string {
return string(this)
}
这样我们就创建好我们的自定义错误了,使用下:
func main() {
var e error
e = MyError("hello")
fmt.Println(e)
}
当然我们可以把 string 换成 struct ,同时加入很多我们自定义的属性:
type MyError struct {
Code int
Msg string
}
func NewMyError(code int, msg string) *MyError {
return &MyError{Code: code, Msg: msg}
}
func (this MyError) Error() string {
return fmt.Sprintf(“%d-%s”,this.Code, this.Msg)
}
// FindUser 模拟下我们的业务方法
func FindUser() error {
return NewMyError(404, “找不到内容”)
}
func main() {
var e error
e = FindUser()
fmt.Println(e)
}
2、错误的API
最后我们来说说 Go 语言中错误的 API,到目前为止,我们面对错误除了输出外,就是使用 == 对错误进行哨兵比较,但是这样未必准确。
所以官方在错误的基础上,又扩展了几个 API。
1、Is
我们面对错误,尽量不要使用这样的方式去比较:
// 尽量少用
if e.Error() == "404-找不到内容" {
}
尽量少用,最好不用。
也少用这样的方式:
var ErrorNotFind = NewMyError(404, "找不到内容")
// FindUser 模拟下我们的业务方法
func FindUser() error {
return ErrorNotFind
}
func main() {
var e error
e = FindUser()
log.Println(e)
// 尽量少用
if e == ErrorNotFind {
}
}
目前我们的错误结构体还是非常简单的,如果我们的结构体里面的属性再多几个,很可能就会出现牛头对马嘴情况。
所以官方为我们提供了 Is 方法的 API,他默认使用 == 将特定的错误与错误链中的错误进行比较,如果不一样,就会去调用错误实现的 Is 方法进行比较。
func (this *MyError) Is(target error) bool {
log.Println("到这里来了....")
if inputE, ok := target.(*MyError); ok {
if inputE.Code == this.Code && inputE.Msg == this.Msg {
return true
}
}
return false
}
func main() {
var e error
e = FindUser()
log.Println(e)
if errors.Is(e, NewMyError(404, “ddd”)) {
log.Println(“是 ErrorNotFind”)
}else {
log.Println(“不是 ErrorNotFind”)
}
}
首先我们先去实现下 Is 这个方法,随后我们使用 errors.Is
进行比较,你会看到控制台输出了:
$ go run main.go
2022/08/13 17:20:48 404-找不到内容
2022/08/13 17:20:48 到这里来了....
2022/08/13 17:20:48 不是 ErrorNotFind
2、Unwrap
这是一个不大常用的 API ,标准库里面 fmt.Errorf
就是一个非常典型的使用案例。
场景是什么呢?
我们通常在错误异常的时候,会有给错误加上一些上下文的需求,那在哪里加呢?
就是错误的 Unwrap
方法里面:
func (this *MyError) Unwrap() error {
this.Msg = "hello" + this.Msg
return this
}
func main() {
var e error
e = FindUser()
log.Println(“最原始的错误:”, e)
wE := errors.Unwrap(e)
log.Println(“加了上下文的错误:”, wE)
}
然后看下我们的输出结果:
$ go run main.go
2022/08/13 17:30:06 最原始的错误: 404-找不到内容
2022/08/13 17:30:06 加了上下文的错误: 404-hello找不到内容
你会发现,errors.Unwrap
后的错误调用了我们自定义错误的 Unwrap 方法,在我们的 msg 前面加了 hello。
对错误进行编程最常用的两个 API 就是这两个了,还有一些不大常用的比如 As,大家感兴趣的可以自行去翻阅下资料。
总结
Go 的错误处理和其他语言不太一样,如果遵守错误处理的规范,不对错误进行隐藏,写出来的代码一般都是比较健壮的。
于是就难免会出现一个包里面,特别多的错误处理代码,这就是时间和空间的博弈,就看 Go 语言的领路人如何取舍了。
其次每个人对错误的理解和处理思路方式都不太一样。
欢迎留下你对错误处理的思路和看法,就比如:
我们到底是该多使用哨兵错误,还是该少用呢?
很期待你的留言!