Go语言设计

defer

Go 语言中, defer 会在当前函数返回前执行传入的函数。经常被用于关闭文件、数据库连接等操作。

实际上,Go在编译时会对defer做一些小小的改动,比如如下代码:

func A(){
    defer B()
    // do something
}

编译后:

func A(){
    r := deferproc(8,B)
    // ...
    
    // do something
    
    runtime.deferreturn()
    return
    // ...
}

deferproc()会先在当前的goroutine中注册我们传入的方法,然后runtime.deferreturn()会将注册的函数执行。

每个goroutine在运行时都会有一个对应的结构体runtime.g。其中有一个字段指向_defer链表头,其指向的是一个个_defer结构体。新注册的defer,会添加在该链表的头部。执行时也会从头开始执行注册的_defer

所以,我们后注册的defer会先被调用。

让我们简单看一下_defer的数据结构:

type _defer struct {
	siz     int32 	// 参数和返回值共占多少字节
	started bool	// 是否已执行
	heap    bool	// 是否为堆分配

	openDefer bool		
	sp        uintptr  	// 调用者栈指针
	pc        uintptr  	// 返回地址
	fn        *funcval 	// 注册函数
	_panic    *_panic
	link      *_defer 	// 下一个_defer

	fd   unsafe.Pointer 
	varp uintptr
	framepc uintptr
}

在Go1.12版本中,注册defer时,会在堆上,为_defer分配内存并存储注册的defer。 实际上,Go语言会预分配不同大小的deferpool,只有当没有空闲的或者大小合适的_defer时,才会从堆上直接分配。这样可以避免频繁的堆分配与回收。

在Go1.13中,在编译阶段会增加一些局部变量:

defer 信息保存到当前函数栈帧的局部变量中,然后通过runtime.deferprocStack()将这个_defer()注册到g._defer中。以减少defer在堆上的分配。对于如下的代码:

for i:=0; i<n; i++ {
    defer A(i)
}

因为变量i需要在运行时才能确定,所以无法在编译阶段注册defer

在Go1.14中采用了一种叫做open coded defer的方式,直接在编译时将defer调用函数的参数使用局部变量保存,然后根据df变量在函数返回之前确定是否需要被调用。

这种方式将defer直接展开再调用函数内,从而不需要创建_defer结构体,而且不需要注册在g._defer链表中。 但和1.13版本一样,对于一些需要在运行时确定参数的defer注册,还是需要采用1.12中的方式调用defer。 此外,如果在运行函数是执行了panic,之后的代码将不会被执行。1.14版本中采用的是额外栈扫描的方式来发现并执行defer。这无疑将消耗额外的性能,但通常defer被使用的次数远高于panic,所以总体来看有很高的性能提升。

异常处理

Go语言中,异常分为两类errorpanic

通常error用于抛出程序中函数调用失败,以传递错误信息。panic则被用来处理程序运行时较为严重的错误,如果不处理panic将会停止程序的运行。

Go语言认为我们应该对每个可能的异常进行手动处理。

error

error是Go中提供的一个接口:

type error interface {
    Error() string
}

在标准包errors中提供了多种类型的实现,最常用的是未公开的errorString

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}
// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

通过errors.New()可以直接返回一个errorString,标准包中的fmt将会直接调用errorError()方法来格式化输出。以下是处理求两数的商异常的例子:

func Div(dividend float64, divisor float64) (float64, error) {
	if divisor == 0 {
		return 0, errors.New("math: division by zero")
	}
	return dividend / divisor, nil
}
func main() {
	quotient, err := Div(10, 0)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("The result is:%v \n", quotient)
}

panic

panic的调用用于比较严重的程序错误,如果不进行处理将直接停止程序的运行。

Go提供了recover来恢复panic,然后在执行完当前goroutine_defer链中的函数后结束当前调用函数:

func A()  {
	defer A1()
  defer A2()
	panic("panicA1")
	fmt.Println("A")
}
func A1(){
	r := recover()
	fmt.Printf("recover panic: %v\n",r)
}

运行以上代码后输出:

A2
recover panic: panicA1

值得注意的是,recover()只能在defer中调用。

我们发现,程序先输出了"A2"然后才输出恢复的panic信息。实际上,在调用panic时,Go语言运行时会调用runtime.gopanic()在当前goroutine_panic链中添加一个panic。 然后结束当前 goroutine剩余代码的执行,直接执行当前goroutine中的_defer链。 在_defer链中调用recover()函数时,会在_panic链中将第一个未处理的panic标记为已恢复,然后在该defer中的设置已处理的panic指针。 执行recover的函数正常返回以后,继续执行剩余的_defer链。 将已经设置了panicdefer删除,并标记该panic为丢弃。 然后用相同的方式执行_defer链中的其余函数。 最后当_defer执行完时,遍历_panic链,输出剩余未被丢弃的panic信息。

让我们通过一个例子来看一下panicrecover的运行逻辑。在此之前让我们先了解以下panic的数据结构:

type _panic struct {
	argp      unsafe.Pointer // 指向 defer 调用时参数的指针
	arg       interface{}    // 调用 panic 时传入的参数
	link      *_panic        // link to earlier panic
	pc        uintptr        // 指向应该返回执行defer的栈指针
	sp        unsafe.Pointer // 指向应该返回执行函数的栈指针
	recovered bool           // 是否被回复
	aborted   bool           // 是否被抛弃
	goexit    bool
}
func A(){
	defer A1()
	defer A2()
	panic("panicA")			// 1
	fmt.Println("A")		// 2
}
func A1(){
	fmt.Println("A1")
}
func A2(){
	defer B1()
	panic("panicA2")		// 3
}
func B1(){
	r := recover()
	fmt.Println(r)
}

当执行到1时,会调用gopanic()在当前goroutine_defer链已经添加了B2()B1(),同时将panicA也添加到了当前goroutine_panic链中:

然后由于调用了panic,结束之后代码的执行,即2处的代码不会被执行。然后转而调用goroutine_defer链,即调用A2()。在A2()中又在当前goroutine_defer链中添加了B1(),然后再次调用gopanic(),将panicA2添加到了当前goroutine_panic链中:

此时开始执行B1()recover()_panic链中的第一个panic标记为恢复,然后打印输出:

B1()执行完,返回到上一次的panic触发位置,即3。此时会去检查B2()_panic中创建的panicB2,发现已经被恢复,将其从_panic中移除:

此时A2()也执行完,返回到A()中的1处,继续执行_defer链中的A1()

最后遍历当前goroutine中的_panic,输出panic信息。

最后更新于