Go的异常处理机制

396 阅读4分钟

在实际的项目中,对于异常的最佳实践很多,在使用不同的语言开发不同类型的程序时有不同的建议。Go语言中没有使用try...catch类似的异常处理机制,而是提供了panicrecover函数来处理所谓的运行时异常,也就是所谓的错误处理机制。配合defer语句和error接口开发者可以非常灵活地处理运行时的错误和异常。

代码中随便使用panic显然不是一个好方式,我们知道在风险控制中,有所谓已知的未知未知的未知,在Go程序设计中,对于前者我们可以通过预先开发的代码分支来处理,对于后者就比较棘手了:

  1. 如果项目中的代码、使用的标准库以及第三方库在运行时内部捕获了异常并通过合适的error对象返回给调用者,那我们可以尽量少甚至可以不用panic函数。
  2. 如果无法保证上面的情况,那为了确保程序在运行时不会因为未知的未知导致崩溃,那panic函数可能不得不加在任何需要的地方。

我们应该认识到,panic函数是我们和计算机都不希望看到的,应该在设计开发的时候充分考虑使用场景可能出现的情况,处理好已知的未知,在确定需要的地方使用panic机制。

defer

defer关键字用来标记最后执行的Go语句,一般用在资源释放、关闭连接等操作,会在函数关闭前调用。多个defer的定义与执行类似于栈的操作:先进后出,最先定义的最后执行。

package main

import (
	"fmt"
)

func testA() int {
	x := 5
	defer func() {
		x++
		fmt.Printf("testA defer func-1[x=>%d]\n", x)
	}()
	defer func() {
		x++
		fmt.Printf("testA defer funn-2[x=>%d]\n", x)
	}()
	return x
}

func testB() (x int) {
	x = 5
	defer func() {
		fmt.Println("testB defer func")
	}()
	x++
	return
}

func main() {
	a := testA()
	b := testB()
	fmt.Printf("testA return %d\n", a)
	fmt.Printf("testB return %d\n", b)
}

执行后输出结果:

testA defer funn-2[x=>6]
testA defer func-1[x=>7]
testB defer func
testA return 5
testB return 6

分析如上代码,return xxx语句并不是一条原子指令,在其执行的时候语句会分解成返回变量=xxxreturn,最后执行return。之前说defer语句是在函数关闭的时候调用,确切的说是在执行return语句的时候调用,注意是return不是return xxx。在同一个函数里的defer会按照栈的先进后出原则执行。

Error

Go语言通过支持多返回值,让在运行时返回详细的错误信息给调用者变得非常方便。我们可以在编码中通过实现error接口类型来生成错误信息,error接口的定义如下:

type error interface {
	Error() string
}

通过下面的例子来看一下error接口的使用:

package main

import (
	"fmt"
)

type error interface {
	Error() string
}

// DivisionError 类型定义
type DivisionError struct {
	dividend int
	divider  int
}

// Error 方法实现
func (de *DivisionError) Error() string {
	strFormat := `
	Cannot proceed, the divider is zero.
	dividend: %d
	divider: 0`
	return fmt.Sprintf(strFormat, de.dividend)
}

// Divide 函数定义
func Divide(varDividend int, varDivider int) (result int, errorMsg string) {
	if varDivider == 0 {
		dData := DivisionError{
			dividend: varDividend,
			divider:  varDivider,
		}
		errorMsg = dData.Error()
	} else {
		result = varDividend / varDivider
	}
	return
}

func main() {
	result, err := Divide(100, 10)
	if err == "" {
		fmt.Printf("100/10=%d\n", result)
	}
	_, err = Divide(100, 0)
	if err != "" {
		fmt.Printf("errorMsg is: %s\n", err)
	}
}

运行后可以看到如下输出:

100/10=10
errorMsg is:
        Cannot proceed, the divider is zero.
        dividend: 100
        divider: 0

panic和recover

panic和recover是两个内置函数,panic函数用于抛出异常,recover函数用于捕获panic函数的参数信息。recover函数只有在defer语句调用的函数中直接调用时才能生效,如果goroutine没有panic,那recover函数会返回nil。

package main

import (
	"fmt"
)

// SimplePanicRecover panic/recover简单的例子
func SimplePanicRecover() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("Panic info is:", err)
		}
	}()
	panic("SimplePanicRecover panic!")
}

// MultiPanicRecover 多个panic/recover
// 当defer中也调用了panic函数时,最后被调用的panic函数的参数会被后面的recover函数获取到
// 一个函数中可以定义多个defer函数,按照FILO的规则执行
func MultiPanicRecover() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("Panic info is:", err)
		}
	}()
	defer func() {
		panic("MultiPanicRecover defer inner panic!")
	}()
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("Panic info is:", err)
		}
	}()
	panic("MultiPanicRecover panic!")
}

// NoPanicButHasRecover 只有panic没有recover
// 如果函数没有panic函数,调用recover函数不会获取到任何信息,也不会影响当前进程
func NoPanicButHasRecover() {
	if err := recover(); err != nil {
		fmt.Println("NoPanicButHasRecover Panic info is:", err)
	} else {
		fmt.Println("NoPanicButHasRecover Panic info is:", err)
	}
}

func main() {
	SimplePanicRecover()
	MultiPanicRecover()
	NoPanicButHasRecover()
}

运行后可以看到如下输出:

Panic info is: SimplePanicRecover panic!
Panic info is: MultiPanicRecover panic!
Panic info is: MultiPanicRecover defer inner panic!
NoPanicButHasRecover Panic info is: <nil>