Go-关键字deferpanicrecover详解

Posted lady_killer9

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go-关键字deferpanicrecover详解相关的知识,希望对你有一定的参考价值。

目录

defer

调用时机

多次调用时的执行顺序

传参问题

源代码

panic

执行defer

跨协程问题

源代码

recover

使用

源代码

总结

defer

panic

recover

全部源代码

参考


本文进行了关键字defer和内建函数panic、recover的介绍和使用细节。

defer

Go 语言的 defer 会在当前函数返回前执行传入的函数,它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源。

在文章Go-函数详解(参数、返回值、init函数、匿名函数、defer)中进行了简单的使用,但是还不够深入,于是在知乎提了问题,今天做下总结。

调用时机

代码

//------调用时机:所在函数结束或返回前------
func callTime()  {
	{
		defer fmt.Println("defer in callTime()")
		fmt.Println("code block finish..")
	}
	fmt.Println("callTime() finish...")
}

结果

code block finish..
callTime() finish...
defer in callTime()

调用时机在函数/方法结束或返回前

多次调用时的执行顺序

代码

func moreDefer()  {
	for i:=1;i<5;i++{
		defer fmt.Println("defer",i)
	}
	fmt.Println("moreDefer() finish...")
}

结果

moreDefer() finish...
defer 4
defer 3
defer 2
defer 1

栈的顺序调用,先入后出。

传参问题

代码

func deferPara()  {
	i := 0
	defer fmt.Println("defer",i,"in deferPara()")
	i++
	fmt.Println("deferPara finish...,i is ", i)
}

或许你认为结果是这样的:

deferPara finish...,i is  1
defer 1 in deferPara()

因为defer在函数结束前运行嘛,但事实上结果是这样的:

deferPara finish...,i is  1
defer 0 in deferPara()

defer会在到达所在行时,就将变量复制一份传过去。想到的解决方案如下:

  • 引用类型就没有问题了
  • 如果参数是值类型,你的defer不修改参数,你可以传地址。
  • 如果参数是值类型,你也可以将defer放在函数/方法不修改参数后。
  • 如果参数是值类型,你可以使用匿名函数,函数体内再用参数。
func paraFix()  {
	i := 0
	defer fmt.Println("send addr:defer",&i,"in paraFix()")
	defer func() {fmt.Println("no name func: defer",i,"in paraFix()")}()
	i++
	defer fmt.Println("put defer later:defer",i,"in deferPara()")
}

源代码

结构体

 src->runtime->runtime2.go

type _defer struct {
	siz     int32 // 包含参数和结果
	started bool  // 是否开始
	heap    bool  // 是否分配在堆上
	openDefer bool // 是否开放编码
	sp        uintptr  // 栈指针
	pc        uintptr  // 程序计数器
	fn        *funcval // 开放编码时可为nil
	_panic    *_panic  // 正在运行的defer的panic
	link      *_defer  // _defer指针
	fd   unsafe.Pointer // 预分配的函数数据
	varp uintptr        
	framepc uintptr
}

_defer是一个单链表(链栈),采用头插的方式,取的时候先取头的。

编译

src->cmd->compoile->internal->gc->ssa.go stmt方法的一个case

case ODEFER:
		if Debug_defer > 0 {
			var defertype string
			if s.hasOpenDefers {
				defertype = "open-coded"
			} else if n.Esc == EscNever {
				defertype = "stack-allocated"
			} else {
				defertype = "heap-allocated"
			}
			Warnl(n.Pos, "%s defer", defertype)
		}
		if s.hasOpenDefers {
			s.openDeferRecord(n.Left)
		} else {
			d := callDefer
			if n.Esc == EscNever {
				d = callDeferStack
			}
			s.callResult(n.Left, d)
		}

有些defer将在栈上分配,有些在堆上分配。首先是开放编码进行优化,其次是栈,最后是堆,分配到栈上可以节约内存分配带来的额外开销。

panic

执行defer

当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer机制)。随后,程序崩溃并输出日志信息。

func panicDefer()  {
	fmt.Println("code before panic")
	defer fmt.Println("defer in panicDefer")
	panic("something wrong in panic Defer")
	fmt.Println("code after panic")
}

结果

code before panic
defer in panicDefer
panic: something wrong in panic Defer

goroutine 1 [running]:
main.panicDefer()
        E:/Workspace/Go_workspace/learn_go/src/defer_panic_recover/main/main.go:42 +0x10a
main.main()
        E:/Workspace/Go_workspace/learn_go/src/defer_panic_recover/main/main.go:62 +0x27
exit status 2

利用defer就可以实现有panic时也能进行资源释放等。

跨协程问题

panic 只会触发当前 goroutine 的 defer

func panicGoroutine()  {
	defer println("defer in main")
	go func() {
		defer println("defer in goroutine")
		panic("something wrong...")
	}()

	time.Sleep(time.Second)
}

结果:

defer in goroutine
panic: something wrong...

goroutine 6 [running]:
main.panicGoroutine.func1()
        E:/Workspace/Go_workspace/learn_go/src/defer_panic_recover/main/main.go:50 +0x78
created by main.panicGoroutine
        E:/Workspace/Go_workspace/learn_go/src/defer_panic_recover/main/main.go:48 +0x78
exit status 2

协程外面的defer执行不了

源代码

结构体

src->runtime->runtime2.go

type _panic struct {
	argp      unsafe.Pointer // 指向defer栈的函数指针
	arg       interface{}    // panic参数
	link      *_panic        // 先前panic的指针
	pc        uintptr        // 运行时,此panic被绕过时返回到哪
	sp        unsafe.Pointer // 运行时,此panic被绕过时返回到哪
	recovered bool           // 是否此panic结束
	aborted   bool           // 这个panic被终止
	goexit    bool
}

崩溃

src->runtime->panic.go

可以查看gopanic、fatalpanic两个函数,代码过多,不黏贴了,有兴趣可以看看。下面是fatalpanic的部分代码:
	systemstack(func() {
		exit(2)
	})

这就能看出前面panic时为什么是“exit status 2”了

recover

使用

recover 可以中止 panic 造成的程序崩溃,只能在 defer 中发挥作用

代码

func recoverDefer()  {
	//defer println("defer in main") // 执行不到
	defer func() {
		if err := recover();err!=nil{
			println("defer in main")
			println(err)
		}
	}()
	go func() {
		defer println("defer in goroutine")
		panic("something wrong...")
	}()
	panic("something wrong in recoverDefer()")
	time.Sleep(time.Second)
}

结果

defer in main
(0xf4d940,0xf85bc8)
defer in goroutine

源代码

src->runtime->panic.go

func gorecover(argp uintptr) interface{} {
	gp := getg()
	p := gp._panic
	if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
		p.recovered = true
		return p.arg
	}
	return nil
}

可以看到,p!=nil的话才行,也就是说你在panic前使用recover,没在defer中,那么返回的是nil,也就是recover失效了。如果不是nil,在gopanic中会进行处理。

总结

defer

  • 调用时机:函数或方法在返回或结束前执行
  • 多次调用:先写的后调用,栈的顺序
  • 传参问题:值类型时,参数不改变后使用defer、defer的函数不修改则传地址、匿名函数函数体中使用

优点:

  • panic后执行defer,防止异常时忘记释放资源
  • 函数复杂分支返回,写一次即可,简洁,复用性好

panic

  • panic后执行本协程的defer
  • 跨协程问题使用recover解决

recover

  • 终止panic造成的崩溃
  • defer中使用时才有效

全部源代码

package main

import (
	"fmt"
	"time"
)

//------调用时机:所在函数结束或返回前------
func callTime()  {
	{
		defer fmt.Println("defer in callTime()")
		fmt.Println("code block finish..")
	}
	fmt.Println("callTime() finish...")
}
//-----多个defer的顺序------
func moreDefer()  {
	for i:=1;i<5;i++{
		defer fmt.Println("defer",i)
	}
	fmt.Println("moreDefer() finish...")
}
//-----defer传参问题-----
func deferPara()  {
	i := 0
	defer fmt.Println("defer",i,"in deferPara()")
	i++
	fmt.Println("deferPara finish...,i is ", i)
}
//-----defer传参修复方案--------
func paraFix()  {
	i := 0
	defer fmt.Println("send addr:defer",&i,"in paraFix()")
	defer func() {fmt.Println("no name func: defer",i,"in paraFix()")}()
	i++
	defer fmt.Println("put defer later:defer",i,"in deferPara()")
}
//----panic后执行defer-------
func panicDefer()  {
	fmt.Println("code before panic")
	defer fmt.Println("defer in panicDefer")
	panic("something wrong in panic Defer")
	fmt.Println("code after panic")
}
//----跨协程 defer panic问题-----
func panicGoroutine()  {
	defer println("defer in main")
	go func() {
		defer println("defer in goroutine")
		panic("something wrong...")
	}()

	time.Sleep(time.Second)
}
//----------defer中使用recover---------
func recoverDefer()  {
	//defer println("defer in main") // 执行不到
	defer func() {
		if err := recover();err!=nil{
			println("defer in main")
			println(err)
		}
	}()
	go func() {
		defer println("defer in goroutine")
		panic("something wrong...")
	}()
	panic("something wrong in recoverDefer()")
	time.Sleep(time.Second)
}

func main() {
	//callTime()
	//moreDefer()
	//deferPara()
	//paraFix()
	//panicDefer()
	//panicGoroutine()
	recoverDefer()
}

参考

知乎-在go语言中,为什么使用defer?

go-1.16.3源代码

更多Go相关内容:Go-Golang学习总结笔记

有问题请下方评论,转载请注明出处,并附有原文链接,谢谢!如有侵权,请及时联系。如果您感觉有所收获,自愿打赏,可选择支付宝18833895206(小于),您的支持是我不断更新的动力。

以上是关于Go-关键字deferpanicrecover详解的主要内容,如果未能解决你的问题,请参考以下文章

Golang 高效实践之deferpanicrecover实践

go语言之行--golang核武器goroutine调度原理channel详解

【golang详解】go语言GMP(GPM)原理和调度

Go语言备忘录:反射的原理与使用详解

[Golang]函数详解

PHP7之Trait详解