区分错误与异常
很赞成参考文章中知乎高赞中“达达”老哥说的,首先要理清错误与异常之间的区别。我在阅读之后我总结的区别如下:
区分点 | 错误 | 异常 |
---|---|---|
语言层面 | error | panic |
不处理的结果 | 可能会导致逻辑业务上的错误,或者进一步产生异常 | 进程异常退出 |
预见性 | 可以预见 | 不可预见 |
总的来说,在Go语言中,可以按照是否可以提前指导,很分明的区分了错误(可以提前知道)与异常(不能提前知道)。
不同语言之间的错误与异常
错误与异常,拿来和Python和Java相比的话,总结起来应该是这样的:
语言 | 错误 | 异常 |
---|---|---|
Python | Error 解释器不想出现的结果(如语法错误) | Exception 运行中的异常情况(-_- 其实我感觉Python混为一谈了) |
Java | Error Java 与虚拟机有关的错误(如栈溢出、内存不足等等),不能被抓取 | Exception Java 程序运行中可预料的异常情况,可以被抓取,分为运行时异常和受检查的异常 |
Go | Error 可以预料的错误,是业务处理中的一部分 | Panic 不可预料的错误,会导致异常退出 |
还是很别扭,特别是Java和Go,简直就是颠倒了(\掀桌)。如果结合函数调用的场景,它们的区别如下(总结的,不一定准确):
场景 | Python | Java | Go |
---|---|---|---|
调用其它函数调用失败(如语法错误) | 错误 | 错误 | |
函数中参数不对(如类型不符) | 错误 | 错误 | |
调用过程中预料之中的问题(如文件权限不足),但是调用成功 | 异常/错误 | 异常 | 错误 |
调用过程中预料之外的问题(空指针引用) | 异常/错误 | 异常 | 异常 |
堆栈溢出等 | 异常 | 异常 | 异常 |
Golang中的异常处理
在Python和Java中最常见的处理异常的方法就是try catch
机制,而在Go语言中,则是panic recover defer
机制,这三个都是关键字,各自的作用为:
-
defer
: 会在当前函数返回前执行传入的函数, 常用于在函数调用结束后完成一些收尾工作(如数据库回滚,关闭文件等等) -
panic
: 能够改变程序的控制流,调用之后立即停止当前函数的剩余代码,让程序进入“恐慌”,会递归执行调用方的defer
-
recover
:能够终止程序的“恐慌”,可以中止panic
造成的程序崩溃。只能在defer
中发挥作用
使用的例子
package main
import "fmt"
func foo(){
defer func() {
fmt.Println("------> defer\tbegin-----")
if err :=recover(); err != nil{
fmt.Println("[*] recover error: ", err)
}
fmt.Println("[=] (recover done)")
fmt.Println("------< defer\tend-----")
}()
fmt.Println("===> foo Begin")
panic("!!{Bug boom}!!")
fmt.Println("<=== foo End")
}
func main() {
fmt.Println("============[main begin]================")
foo()
fmt.Println("============[main end]================")
}
运行结果如下:
Golang中的错误处理
error的定义
从官网src/builtin/builtin.go - The Go Programming Language (golang.org)的源码里来看,
// 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
}
error
其实就是一个接口,里面只包含一个函数,函数的返回值为string。
error的创建
1. errors.New ()
函数
在errors
包中,提供了一个New
函数,给该函数一个字符串参数,可以得到一个error对象:
源码:
func New(text string) error {
return &errorString{text}
}
使用方法:
err := errors.New("一个新错误")
fmt.Printf("err 错误类型:%T,错误为:%v\n", err, err)
运行结果:
fmt.Errorf ()
函数
这个函数会对字符串格式化,增加上下文信息,可以更精确的描述错误。
// 类型1:字符串中未包含错误
err1 := fmt.Errorf("Error1:E1") //返回一个错误
fmt.Printf("err1 %T \t %v\n", err1, err1)
// 类型2:字符串中包含了%w
err2 := fmt.Errorf("Error2:%w", err1)
fmt.Printf("err2 %T \t\t %v\n", err2, err2)
运行结果:
可以看到其最大的差别在于对格式符%w
有无的:
- 无
%w
时,底层也是调用errors.New()
创建错误。因此错误类型就是*errors.errorString
- 有
%w
时,底层实例化wrapError
结构体指针。 因此错误类型是*fmt.wrapError
,可以理解为包裹错误类型。
error的处理
在error中最常见的处理方式就是通过多值进行返回,明了的暴露出来,要么处理,要么略过。最常见的处理方式如下:
err := bar()
if err != nil {
fmt.Printf("got err, %+v\n", err)
}
错误处理的最佳实践
错误处理的弊端
if err!=nil
的弊端在于,有太多的错误需要处理,一个程序中错误处理甚至占据了很大一部分,这简直让人爆炸。
同时由于错误的信息很少,我们需要知道出错的更多信息,在什么文件的,哪一行代码,用来更好的定位。
最佳实践:github.com/pkg/errors
有几个关键函数:
Wrap
函数:包装底层错误,增加上下文信息,附加调用栈WithMessage
函数:增加上下文,不附加调用栈WithStack
函数:仅返回堆栈信息Cause
函数:判断底层错误
package main
import (
"fmt"
"github.com/pkg/errors"
)
/* 全局变量 自定义错误*/
var ERROR=errors.New("<Error>")
//错误源头,包含堆栈信息
func foo() error {
return errors.Wrap(ERROR, "====={Foo Message}=====")
}
//间接调用错误函数
func bar() error {
return errors.WithMessage(foo(), "====={Bar Message}=====")
}
func main() {
err := bar()
//使用Cause 进行判断
if errors.Cause(err) == ERROR {
fmt.Printf("Simple Error Info: %v\n", err) //信息保持一行
fmt.Printf("\nRich Error Info: %+v\n", err) //信息包含调用栈
return
}
}
结果
参考文章:
Go 语言的错误处理机制是一个优秀的设计吗? - 知乎 (zhihu.com)
Golang 错误处理最佳实践. 官方团队和开发者社区都在尝试改进Go的错误处理,… | by Che Dan | Medium
Go 编程模式:错误处理 | 酷 壳 - CoolShell
pkg/errors: Simple error handling primitives (github.com)
Go语言(golang)的错误(error)处理的推荐方案 | 飞雪无情的博客 (flysnow.org)
Golang错误和异常处理的正确姿势 - 简书 (jianshu.com)
探讨 Go 错误机制|OhYee博客 (oyohyee.com)