乐趣区

关于程序员:没有-trycatch该如何处理-Go-错误异常

错误处理是软件开发中不可回避的问题,「Go 中次要通过 error 和 panic 别离示意谬误和异样」,并提供了较为简洁的谬误异样解决机制。本文咱们就来介绍 Go 中的一些错误处理机制,再聊聊 Go 语言中错误处理的槽点和冀望。

Errorsare values
错误处理是每个开发人员都须要面对的问题,在我过来接触的编程语言中,大多是通过「try-catch 的形式」对可能呈现谬误的代码块进行包装:「程序运行 try 中代码,如果 try 中的代码运行出错,程序将会立刻跳转到 catch 中执行异样解决逻辑」。

与其余的编程语言不同,Go 中提倡“Errorsare values!”的解决思维,它将 error 作为一个返回值,来迫使调用者对 error 进行解决或者疏忽。于是,在代码中咱们将会编写大量的 if 判断语句对 error 进行判断,如下所示:

result, err := dothing(“work”)
if err != nil {
log.Fatal(err)
}
// do other thing
当我的项目的代码快速增长起来时,咱们会发现代码中到处都是相似 err != nil 的判断片段。尽管这会使代码变得很繁缛,然而这种设计和约定也会激励开发人员明确检查和确定谬误产生的地位。

在 Go 中,error 接口定义如下:

type error interface {
Error() string
}
最罕用的 error 实现是 Go 规范库 errors 包中内置的 errorString,它是一个仅蕴含错误信息的 error 实现,能够通过 errors.New 和 fmt.Errorf 函数创立。内置的 error 接口使得开发人员能够为谬误增加任何所需的信息,error 能够是实现 Error() 办法的任何类型,比方咱们能够为谬误增加错误码和调用栈信息,如下所示:

type Error struct {
Msg string
Code int32
St []uintptr // 调用栈
}
// 获取调用栈信息
func callers() []uintptr {
var pcs [32]uintptr
n := runtime.Callers(3, pcs[:])
st := pcs[0:n]
return st
}
func New(code int32, msg string) error {
return &Error{
Code: int32(code),
Msg: msg,
St: callers(),
}
}
func (e *Error) Error() string {
if e == nil {
return “OK”
}
return fmt.Sprintf(“code:%d, msg:%s”, e.Code, e.Msg)
}
通过「断言」的形式能够将 error 转化为特定的类型从而进行特异化解决,如下所示:

if e, ok := err.(*Error); ok {
// 取出堆栈信息进行解决
st := e.st
// ….
}else {
// 其余错误处理
}
在 Go 1.13 版本之后,errors 包中增加了 errors.Is 和 errors.As 函数:errors.Is 办法用来比拟两个 error 是否相等,而 errors.As 函数用来判断 error 是否为特定类型。

因为 error 是一个值,因而咱们能够对其进行编程,简化 Go 错误处理的反复代码。在一些管道和循环的代码中,只有其中一次解决呈现谬误,就应该退出本次管道或者循环。寻常的做法是在每次迭代都查看谬误,但为了让管道和循环的操作显得更加天然,咱们能够将 error 封装到独立的办法或者变量中返回,以防止错误处理覆盖管制流程,如 gorm 中的 DB 设计所示:

err := DB.Where(queryString, queryValue…).
Table(“table_name”).
Updates(map[string]interface{}{…}).Error
if err != nil{
// 错误处理逻辑
}
这里 error 是从 gorm.DB 的 Error 成员变量中获取的。在数据库申请执行完结之后,程序才从 DB 中获取执行谬误,这样的写法使得错误处理不会中断执行流程。但须要留神的是,无论如何简化 error 的设计,程序都要检查和处理错误,谬误是无奈防止的。

defer、panic 和 recover
谬误个别是一些开发人员“预料之内”的谬误,比方获取数据库连贯失败等,这些都是在 Go 中通过 error 表白并可控。但当程序出现异常,如数组拜访越界这类“意料之外”的谬误时,它可能导致程序运行解体,此时就须要开发人员捕捉异样并恢复程序的失常运行流程。

接下来咱们就介绍 defer、panic 和 recover 如何组合复原运行时执行异样的 Go 程序。

「defer 是 Go 中提供的一种提早执行机制」,每次执行 defer,都会将对应的函数压入栈中。在函数返回或者 panic 异样完结时,Go 会顺次从栈中取出提早函数执行。

在编程的时候,常常须要关上一些资源,比方数据库连贯、文件等,在资源应用实现之后须要开释,不然有可能会造成资源透露。这个时候,咱们能够通过 defer 语句在函数执行完之后,主动开释资源,防止在每个函数返回之前手动开释资源,缩小冗余代码。

defer 有三个比拟重要的特点。「第「「一个」」是** 依照调用 defer 的逆序执行」,即后调用的在函数退出时先执行,「后进先出」。如下例子所示:

func main() {
defer fmt.Println(“I register at first, but execute at last”)
defer fmt.Println(“I register at middle, execute at middle”)
defer fmt.Println(“I register at last, execute at first”)
fmt.Println(“test begin”)
}
预期的后果为:

test begin
I register at last, execute at first
I register at middle, execute at middle
I register at first, but execute at last
「第二」个特点是「defer 被定义时,参数变量会被立刻解析,传递参数的值拷贝」。在函数内应用的变量其实是对外部变量的一个拷贝,在函数体内,对变量更改也不会影响内部变量,如下所示:

func main() {
i := 10
defer fmt.Printf(“defer i is %d\n”, i)
i = 20
fmt.Printf(“current i is %d\n”, i)
}
预期后果为:

current i is 20
defer i is 10
然而当 defer 以闭包的形式援用内部变量时,则会在提早函数真正执行的时候,依据整个上下文确定以后的值,如下示例代码:

func main() {
for i := 0; i < 5; i++ {
defer func() {
fmt.Println(i)
}()
}
}
预期的输入后果为:

5
5
5
5
5
上述例子为了演示简略,在 for 循环中应用了 defer,但 ** 在日常开发中,「我倡议你还是不要在循环中应用 defer」。因为相较于间接调用,defer 的执行存在着额定的开销,例如 defer 会对其后须要的参数进行内存拷贝,还会对 defer 构造进行压栈出栈操作。因而,在循环中应用 defer 可能会带来较大的性能开销。

「defer 的第三个特点是能够读取并批改函数的命名返回值」,如上面的例子所示:

func main() {
fmt.Println(test())
}
func test() (i int) {
defer func() { i++}()
return 1
}
预期的返回后果为:

2
这是因为对于命名返回值,defer 和 return 的执行程序如下:

将 1 赋给 i;
执行 i++;
返回 i 作为函数返回值。
defer 的外部实现为一个「提早调用链表」,如下图所示:

图片图片
defer 提早调用链表示意图

其中,g 代表 goroutine 的数据结构。每个 goroutine 中都有一个 _defer 链表,当代码中遇到 defer 关键字时,Go 都会将 defer 相干的函数和参数封装到 _defer 构造体中,而后将其注册到以后 goroutine 的 _defer 链表的表头。在以后函数执行结束之后,Go 会从 goroutine 的 _defer 链表头部取出来注册的 defer 执行并返回。

_defer 构造体中存储 defer 执行相干的信息,定义如下所示:

type _defer struct {
siz int32 // 参数与后果内存大小
started bool
heap bool // 是否在堆上调配
openDefer bool // 是否通过凋谢编码优化
sp uintptr // 栈指针
pc uintptr // 调用方的程序计数器
fn *funcval // defer 传入的函数
_panic *_panic
link *_defer // 下一个 _defer
}
「panic 是一个内置函数,用于抛出程序执行的异样」。它会终止其后将要执行的代码,并顺次逆序执行 panic 所在函数可能存在的 defer 函数列表;而后返回该函数的调用方,如果函数的调用方中也有 defer 函数列表,也将被逆序执行,执行完结后再返回到上一层调用方,直到返回以后 goroutine 中的所有函数为止,最初报告异样,程序解体退出。异样能够间接通过 panic 函数调用抛出,也可能是因为运行时谬误而引发,比方拜访了空指针等。

而「recover 内置函数可用于捕捉 panic,从新恢复程序失常执行流程,然而 recover 函数只有在 defer 外部应用才无效」。如上面例子所示:

func main() {
err := panicAndReturnErr()
if err != nil{
fmt.Printf(“err is %+v\n”, err)
}
fmt.Println(“returned normally from panicAndReturnErr”)
}
func panicAndReturnErr() (err error){
defer func() {

    // 从 panic 中复原

if e := recover(); e != nil {

        // 打印栈信息

buf := make([]byte, 1024)
buf = buf[:runtime.Stack(buf, false)]
err = fmt.Errorf(“[PANIC]%v\n%s\n”, e, buf)
}
}()
fmt.Println(“panic begin”)
panic(“panic this game”)
fmt.Println(“panic over”)
return nil
}
预期的执行后果为:

panic begin
err is [PANIC]panic this game
goroutine 1 [running]:
main.panicAndReturnErr.func1(0xc000062f08)
/Users/apple/Desktop/micro-go-course/section37/defer_example.go:21 +0xa1
panic(0x10ad640, 0x10eb360)
/usr/local/go/src/runtime/panic.go:969 +0x166
main.panicAndReturnErr(0x0, 0x0)
/Users/apple/Desktop/micro-go-course/section37/defer_example.go:26 +0xc2
main.main()
/Users/apple/Desktop/micro-go-course/section37/defer_example.go:10 +0x26
returned normally from panicAndReturnErr
从这个执行后果能够看出,panicAndReturnErr 函数在 panic 之后将会执行 defer 定义的提早函数,恢复程序的失常执行逻辑。在上述例子中,咱们在 defer 函数中应用 recover 函数帮忙程序从 panic 中恢复过来,并获取异样堆栈信息组成 error 返回调用方。panicAndReturnErr 从 panic 中复原后将间接返回,不会执行函数中 panic 后的其余代码。

在日常开发中,对于可能呈现执行异样的函数,如数组越界、操作空指针等,在函数中定义一个应用 recover 函数的 defer 提早函数,无利进步程序执行的健壮性,防止程序运行时异样解体。

Go 错误处理的一些吐槽
Go 语言的错误处理在社区始终有争议,Go 错误处理形式也始终是很多人诟病的中央,有些人吐槽说一半的代码都是 if err != nil {/ 打印 && 错误处理 /},重大影响失常的解决逻辑。

Go 应用的是对显式谬误后果的显式谬误查看,而其余异样解决型语言(诸如 C ++,C#,Java 等)应用的是对隐式后果进行隐式查看。对于异样解决型语言的解决形式,因咱们全然看不到隐式查看,所以难以验证程序是否正确复原到查看失败时的状态。当咱们辨别谬误和异样,依据规定设计函数,就会大大提高可读性和可维护性。

咱们往往冀望的是在代码中缩小大量谬误查看代码,使谬误查看更轻量,使错误处理更便捷。所以期待在前面的 Go 版本可能缩小反复地异样解决,使得谬误查看及错误处理放弃显式的形式,并兼容现有代码。

小结
本文咱们次要介绍了 Go 中常见的错误处理机制 Go 提倡将谬误作为返回值返回给调用方,由调用方决定如何解决或者疏忽谬误。通过 defer 和 recover 内置函数,咱们能够轻易地将运行时异样的 Go 程序复原到失常执行流程。从程序开发的角度来说,建设 ’ 速错 ’ 理念,程序终止了,你就会第一工夫晓得谬误。因而,咱们在晚期开发阶段,最简略的同时也可能是最好的办法是调用 panic 函数来中断程序的执行以强制产生谬误,使得该谬误不会被疏忽,因此可能尽快修复,保障应用程序的稳定性。

退出移动版