Golang 错误与异常 总结与最佳实践

区分错误与异常

很赞成参考文章中知乎高赞中“达达”老哥说的,首先要理清错误异常之间的区别。我在阅读之后我总结的区别如下:

区分点错误异常
语言层面errorpanic
不处理的结果可能会导致逻辑业务上的错误,或者进一步产生异常进程异常退出
预见性可以预见不可预见

总的来说,在Go语言中,可以按照是否可以提前指导,很分明的区分了错误(可以提前知道)与异常(不能提前知道)。

不同语言之间的错误与异常

错误与异常,拿来和Python和Java相比的话,总结起来应该是这样的:

语言错误异常
PythonError 解释器不想出现的结果(如语法错误)Exception 运行中的异常情况(-_- 其实我感觉Python混为一谈了)
JavaError Java 与虚拟机有关的错误(如栈溢出、内存不足等等),不能被抓取Exception Java 程序运行中可预料的异常情况,可以被抓取,分为运行时异常受检查的异常
GoError 可以预料的错误,是业务处理中的一部分Panic 不可预料的错误,会导致异常退出

还是很别扭,特别是Java和Go,简直就是颠倒了(\掀桌)。如果结合函数调用的场景,它们的区别如下(总结的,不一定准确):

场景PythonJavaGo
调用其它函数调用失败(如语法错误)错误错误
函数中参数不对(如类型不符)错误错误
调用过程中预料之中的问题(如文件权限不足),但是调用成功异常/错误异常错误
调用过程中预料之外的问题(空指针引用)异常/错误异常异常
堆栈溢出等异常异常异常
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)

Go 语言 panic 和 recover 的原理 | Go 语言设计与实现 (draveness.me)

Golang 学习——error 和创建 error 源码解析 | Go 技术论坛 (learnku.com)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值