程序运行过程中不可避免的发生各种错误,要想让自己的程序保持较高的健壮性,那么异常,错误处理是需要考虑周全的,每个编程语言提供了一套自己的异常错误处理机制,在Go中,你知道了吗?接下来我们一起看看Go的异常错误机制。
func sum (x,y int) (int,int,int){ z := x+y return z,x,y }
// The error built-in interface type is the conventional interface for // representing an error condition, with the nil value representing no error. //翻译下来就是: //错误的内置接口类型是 error,没有错误用 nil 表示 type error interface { Error() string }
func New(text string) error { return &errorString{text} } // errorString is a trivial implementation of error. 翻译 // 把error转换成String是错误的简单实现 type errorString struct { s string } func (e *errorString) Error() string { return e.s }
我们可以看到errorString结构体实现了 Error()string 接口,通过New()方法返回一个errorString指针类型的对象。
看到这里不知道大家想到没,Go对错误的处理就是显示的通过方法返回值告诉你需要对错误进行判断和处理。也就是错误对你是可见的,这也需要开发人员在方法中尽可能的考虑到各种发生的错误,并返回给方法调用者。
我们知道程序在运行时会发生各种各样的运行时错误,比如数组下标越界异常,除数为0的异常等等,而这些异常如果不被处理会导致go程序的崩溃,那么如何捕获这些运行时异常转化为错误返回给上层调用链,就让我一起看看这几个关键字:panic, defer recover,此处我们不讨论原理。
我们把运行时发生异常称为发生了一个恐慌,我们也可以手动抛出一个恐慌,如下
func TestPanic(){ panic("发生恐慌了") } //截取一部分结果,我们看到程序终止了,打印了堆栈信息 anic: 发生恐慌了 [recovered] panic: 发生恐慌了 goroutine 19 [running]: testing.tRunner.func1(0xc0000b6100) D:/sdk/go12/src/testing/testing.go:830 +0x399 panic(0x521da0, 0x57bb10) D:/sdk/go12/src/runtime/panic.go:522 +0x1c3 gome_tools/basic.TestPanic(...) D:/gome_space/gome_tools/basic/array_slice.go:101
恐慌发生了怎么处理呢,这时需要defer和recover一起协作,defer什么意思呢,是表示这个方法最后执行的一段代码,无论这个方法发生错误,异常等,defer里面的里代码一定会被执行,而我们可以在defer中通过recover关键字恢复我们的恐慌,将之处理,转化为一个错误并打印,如下代码:
func TestDeferAndRecover(){ defer func(){ if err:=recover(); err != nil{ fmt.Println("异常信息为:",err) } }() panic("发生恐慌了") } //结果 异常信息为: 发生恐慌了
func division(x,y int) (int,error){ //如果除数为0,则返回一个错误信息给上游 if y == 0{ return 0,errors.New("y is not zero") } z := x / y return z ,nil } result, err := division(1,0) if err != nil { //处理错误逻辑 } //处理正常逻辑
如上,division函数里面判断y等于0时,给调用者返回一个错误信息,调用者通过两个变量来接受division的返回值,判断 err是否为空做出不同的错误处理逻辑
还是上面的 division(x,y)(z,error)
函数,假设我们入参传(4,2)进去,这时我们是清楚的知道不可能发生错误,我们可以按如下处理,通过下划线 _ 忽略这个返回值。
//通过_忽略第二个返回值 result, _ := division(1,0) //打印结果 fmt.Println(result)
还是 division(x,y)(z,error)
函数,假设小明忘记了或者没想到要判断除数为0的情况,写出来的代码如下:
func division(x,y int) (int,error){ z := x / y return z ,nil }
小红在调用上面的方法时写成了 result,_ := division(1,0)
,很明显division方法是会发生错误的,错误信息如下,integer divide by zero ,被除数为0,我们知道程序出错了,并且整个程序终止了
tips: Go语言中,一旦某一个协程发生了panic而没有被捕获,那么导致整个go程序都会终止,确实有点坑,但确实如此(了解java的人都知道,在java中一个线程发生发生了异常,只要其主线程不曾终止,那么整个程序还是运行的) ,但go不是这样的,文章最后我会写一个例子,大家可以看看。
通过上面的tips,我们知道,我们不能让我们的方法发生panic,在不确保方法不会发生panic时一定要捕获,谨记。
panic: runtime error: integer divide by zero [recovered] panic: runtime error: integer divide by zero
func division(x,y int) (result int,err error){ defer func(){ if e := recover(); e != nil{ err = e.(error) } }() result = x / y return result ,nil }
这段代码什么意思呢?当我们 division(1,0)
时,一定会报除0异常,division函数声明了返回值result(int型),err(error型),当 x / y
发生异常时,在defer函数中,我们通过recover()函数来捕获发生的异常,如果不为空,将这个异常赋值给返回结果的变量 err,我们再来调用这个函数 division(1,0)
看看输出什么,如下,是不是将堆栈信息转化为了一段字符串描述。
0 runtime error: integer divide by zero
我们知道go中关于错误定义了一个接口,如果想要自定义自己的错误类型,我们只需要实现这个接口就可以了,还是这个函数,我们为其定义一个除数为0的错误
type DivideByZero struct{ //错误信息 e string //入参信息(除数和被除数) param string } //实现接口中的Error()string方法,组装错误信息为字符串 func (e *DivideByZero) Error() string { buffer := bytes.Buffer{} buffer.WriteString("错误信息:") buffer.WriteString(divideByZero.e) buffer.WriteString(",入参信息:") buffer.WriteString(divideByZero.param) return buffer.String() } func division(x,y int) (int,error){ //如果除数为0,则返回一个错误信息给上游 if y == 0{ //这个时候我们返回如下错误 return 0, &DivideByZero{ e:"除数不能0", param:strings.Join([]string{strconv.Itoa(x),strconv.Itoa(y)},","), } } z := x / y return z ,nil } //最终结果 0 错误信息:除数不能为0,入参信息:1,0
上文提到,go中一旦某一个协程发生了panic而没被recover,那么整个go程序会终止,而Java中,某一线程发生了异常,即便没被catche,那么只是这个线程终止了,Java程序是不会终止的,只有主线程完成Java程序才会结束,看下面两段代码
public static void main(String []args){ new Thread(new Runnable() { @Override public void run() { throw new RuntimeException("抛出异常了"); } }).start(); try { Thread.sleep(10 * 1000); }catch (InterruptedException e) { } }
func main(){ go func() { panic("发生恐慌了") }() time.Sleep(10 * time.Second) }
上面两端代码含义都是一样的,启动后各开一个线程和协程,在线程和协程内分别主动抛出异常,但结果不一样,java的主线程会休眠10秒钟后结束,而go主协程会立即结束。