Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

defer的使用方法和行为分析 #51

Open
kevinyan815 opened this issue Jan 26, 2021 · 0 comments
Open

defer的使用方法和行为分析 #51

kevinyan815 opened this issue Jan 26, 2021 · 0 comments

Comments

@kevinyan815
Copy link
Owner

kevinyan815 commented Jan 26, 2021

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

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

func createData(db *gorm.DB) error {
    tx := db.Begin()
    defer func () {
        if err != nil {
            tx.Rollback()
        } else {
            tx.Commit()
        }
    }
    defer tx.Rollback()
    
    if err := tx.Create(&Post{Name: "Kevin"}).Error; err != nil {
        return err
    }
    
}

使用 defer 常遇见的两个问题

我们在 Go 语言中使用 defer 时会遇到两个常见问题,这里会介绍具体的场景并分析这两个现象背后的设计原理:

  • defer 关键字的调用时机以及多次调用 defer 时执行顺序是如何确定的;
  • defer 关键字使用传值的方式传递参数时会进行预计算,导致不符合预期的结果;

倒序执行

向 defer 关键字传入的函数会在当前函数返回之前运行。假设我们在 for 循环中多次调用 defer 关键字:

func main() {
	for i := 0; i < 5; i++ {
		defer fmt.Println(i)
	}
}

$ go run main.go
4
3
2
1
0

运行上述代码会倒序执行传入 defer 关键字的所有表达式,因为最后一次调用 defer 时传入了 fmt.Println(4),所以这段代码会优先打印 4。

作用域

我们可以通过下面这个简单例子强化对 defer 执行时机的理解:

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

$ go run main.go
block ends
main ends
defer runs

从上述代码的输出我们会发现,defer 传入的函数不是在退出代码块的作用域时执行的,它只会在当前函数和方法返回之前被调用。

预计算参数

Go 语言中所有的函数调用都是传值的,虽然 defer 是关键字,但是也继承了这个特性。假设我们想要计算 main 函数运行的时间,可能会写出以下的代码:

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

$ go run main.go
0s

然而上述代码的运行结果并不符合我们的预期,这个现象背后的原因是什么呢?经过分析,我们会发现调用 defer 关键字会立刻拷贝函数中引用的外部参数,所以 time.Since(startedAt) 的结果不是在 main 函数退出之前计算的,而是在 defer 关键字调用时计算的,最终导致上述代码输出 0s。

想要解决这个问题的方法非常简单,我们只需要向 defer 关键字传入匿名函数:

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

$ go run main.go
1s

defer 的数据结构

defer 关键字在 Go 语言源代码中对应的数据结构如下:

type _defer struct {
	siz       int32
	started   bool
	openDefer bool
	sp        uintptr
	pc        uintptr
	fn        *funcval
	_panic    *_panic
	link      *_defer
}

runtime._defer 结构体是延迟调用链表上的一个元素,所有的结构体都会通过 link 字段串联成链表。后调用的 defer 函数会被追加到当前 goroutine 的 _defer 链表的最前面;而在当前函数返回前,运行时会从延迟调用链表上从前到后取出 runtime._defer 依次执行,这也就是为什么后调用的 defer 函数会先执行

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant