go defer详解|Go主题月

617 阅读2分钟

defer 关键字

很多现代的编程语言中都有 defer 关键字,Go 语言的 defer 会在当前函数返回前执行传入的函数,它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源。

使用 defer 的最常见场景是在函数调用结束后完成一些收尾工作,例如在 defer 中回滚数据库的事务:

func InsertObj() error {
    //获取一个数据库session
    db := db.GetDB().Begin()
    defer db.Rollback()
    
    //do someting
    err := db.Insert()
    
    if err != nil{
     return err
    }
    
    return db.Commit().Error
}

在使用数据库事务时,我们可以使用上面的代码在创建事务后就立刻调用 Rollback 保证事务一定会回滚。哪怕事务真的执行成功了,那么调用 Commit() 之后再执行 Rollback() 也不会影响已经提交的事务。

defer的特性

  • defer 的调用时机以及多次调用 defer 时执行顺序
  • defer 使用传值的方式传递参数时会进行预计算,导致不符合预期的结果
func main() {

	defer fmt.Println(0)
        defer fmt.Println(1)
        defer fmt.Println(2)
        defer fmt.Println(3)
        	
}

上面的代码输出:

3
2
1
0

那么defer的执行顺序就是先进后出

func main() {
    {
        defer fmt.Println("defer runs")
        fmt.Println("block ends")
    }
    
    fmt.Println("main ends")
}

输出:

block ends
main ends
defer runs

可以看出defer不是在退出代码块的作用域时执行的,它只会在当前函数和方法返回之前被调用

预计算

func main() {
	startedAt := time.Now()
	defer fmt.Println(time.Since(startedAt))
	
	time.Sleep(time.Second)
}

输出:

0s

调用 defer 关键字会立刻拷贝函数中引用的外部参数,所以 time.Since(startedAt) 的结果不是在 main 函数退出之前计算的,而是在 defer 关键字调用时计算的,最终导致上述代码输出 0s。

那么如果要解决这个问题可以改写代码

func main() {
	startedAt := time.Now()
	defer func() { fmt.Println(time.Since(startedAt)) }()
	
	time.Sleep(time.Second)
}

输出:

1s

虽然调用 defer 关键字时也使用值传递,但是因为拷贝的是函数指针

defer 常用的案例

捕获异常和崩溃日志:

func process() error {
	defer func() {
                //防止意外之外的错误导致程序崩溃
		if err := recover(); err != nil {
                        //崩溃日志的捕获
			core.LogPnc.Errorf("panic : %v", err)
			core.LogPnc.Errorf("%s", string(debug.Stack()))
		}
	}()
        //do something
	err := obj.CALL(parseYML)
	if err != nil {
		return err
	}
	return nil
}

释放锁:

func runTask() {
        //上锁
	c.lk.Lock()
        //释放锁
	defer c.lk.Unlock()
	//do something
}

在函数返回之前进行业务操作

func (c *ProcessEngine) Process(ph *ProcessHook) {
	defer func() {

		c.lk.Lock()
                //逻辑处理
		c.running--
		c.lk.Unlock()

	
	}()
        //do something
}

如果还有任何问题或者想了解的内容,可以关注我的公众号超级英雄吉姆,在公众号留言,我看到后第一时间回复。