Golang编程-踩坑笔记(5)如何利用defer特性?- 抛异常和一些坑

defer执行时机

Go官方文档中对defer的执行时机做了阐述,分别是

  1. 包裹defer的函数返回时
  2. 包裹defer的函数执行到末尾时
  3. 所在的goroutine发生panic时

利用defer抛异常

如果巧妙的使用第三条特性,defer可以被用来当作golang版本的try-catch
配合runtime库,能够追踪堆栈的内容/方法调用路径,和程序错误位置等。

内置的 recover 函数可用于重新获得对异常程序的控制并恢复正常执行。

调用 recover 将停止展开并返回传递给 panic 的参数。
如果 goroutine 没有异常,则恢复将返回 nil。
因为展开时运行的唯一代码是在 defer 函数内部,所以 recover 仅在此类函数内部有用。

func main() {
	n := foo()
	fmt.Println("main received", n)
}

func foo() int {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()
	m := 1
	panic("foo: fail")
	m = 2
	return m
}
foo: fail
main received 0

要在发生 panic 时返回值,必须使用命名返回值。否则return初始值。

package try
import (
	"bytes"
	"fmt"
	"runtime"
) 
/**
* 捕获异常try...catch
* 用法示例:
  defer try.CatchException(func(e interface{}) {
      log.Println(e)
  })
*/
func CatchException(handle func(e interface{})) {
    if err := recover(); err != nil {
        e := printStackTrace(err)
        handle(e)
    }
}
// 打印堆栈信息
func printStackTrace(err interface{}) string {
    buf := new(bytes.Buffer)
    fmt.Fprintf(buf, "%v\n", err)
    for i := 1; ; i++ {
        pc, file, line, ok := runtime.Caller(i)
        if !ok {
            break
        }
        fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
    }
    return buf.String()
}
package main
import (
    "./try"
    "log"
)
func main() {
    defer try.CatchException(func(e interface{}) {
        log.Println(e)
    })
    zero := 0
    x := 3 / zero
    fmt.Println("x=", x)
}

在这里插入图片描述
能够看出问题出在main.go:14的 x := 3 / zero这一行。

defer执行顺序

压栈的弹出顺序即为defer执行顺序。

坑1:defer在匿名返回值和命名返回值函数中的不同表现

func returnValues() int {
    var result int
    defer func() {
        result++
        fmt.Println("defer")
    }()
    return result
}
func namedReturnValues() (result int) {
    defer func() {
        result++
        fmt.Println("defer")
    }()
    return result
}

两个函数逻辑均相同,区别只有屁股,上面的方法使用了匿名返回值,下面的使用了命名返回值,为什么输出的结果会有区别呢?上面的方法会输出0,下面的方法输出1。

要搞清这个问题首先需要了解defer的执行逻辑,文档中说defer语句在方法返回“时”触发,也就是说return和defer是“同时”执行的。以匿名返回值方法举例,过程如下。

  1. 将result赋值给返回值(可以理解成Go自动创建了一个返回值retValue,相当于执行retValue = result)
  2. 然后检查是否有defer,如果有则执行
  3. 返回刚才创建的返回值(retValue)

在这种情况下,defer中的修改是对result执行的,而不是retValue,所以defer返回的依然是retValue。在命名返回值方法中,由于返回值在方法定义时已经被定义,所以没有创建retValue的过程,result就是retValue,defer对于result的修改也会被直接返回。

即,若程序未给定返回变量,则golang自动给一个retValue作为返回变量,但该变量retValue实在defer执行前传入return的,defer之中的更改无效。

若使用命名返回值,则golang不会创建retValue,返回的是result,defer更改有效。

所以注意两个习惯:

尽量使用命名返回值以确保defer的正确更改。

或者,尽量避免在defer中更改返回值,若使用匿名返回值的话,很难懂。

ε=ε=ε=(~ ̄▽ ̄)~

坑2:在for循环中使用defer可能导致的性能问题

func deferInLoops() {
    for i := 0; i < 100; i++ {
        f, _ := os.Open("/etc/hosts")
        defer f.Close()
    }
}

defer在紧邻创建资源的语句后生命力,看上去逻辑没有什么问题。但是和直接调用相比,defer的执行存在着额外的开销,例如defer会对其后需要的参数进行内存拷贝,还需要对defer结构进行压栈出栈操作。所以在循环中定义defer可能导致大量的资源开销,在本例中,可以将f.Close()语句前的defer去掉,来减少大量defer导致的额外资源消耗。

坑3:判断执行没有err之后,再defer释放资源

resp, err := http.Get(url)
// 先判断操作是否成功
if err != nil {
    return err
}
// 如果操作成功,再进行Close操作
defer resp.Body.Close()

一些获取资源的操作可能会返回err参数,我们可以选择忽略返回的err参数,但是如果要使用defer进行延迟释放的的话,需要在使用defer之前先判断是否存在err,如果资源没有获取成功,即没有必要也不应该再对资源执行释放操作。如果不判断获取资源是否成功就执行释放操作的话,还有可能导致释放方法执行错误。

坑4:调用os.Exit时defer不会被执行

当发生panic时,所在goroutine的所有defer会被执行,但是当调用os.Exit()方法退出程序时,defer并不会被执行。

func deferExit() {
    defer func() {
        fmt.Println("defer")
    }()
    os.Exit(0)
}

上面的defer并不会输出。

参考文献
https://www.jianshu.com/p/79c029c0bd58
https://studygolang.com/articles/27919?fr=sidebar
https://yourbasic.org/golang/recover-from-panic/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值