7

Go语言中 defer 使用场景及注意事项,你是要注意的!

 3 years ago
source link: https://studygolang.com/articles/35415
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Go语言中 defer 使用场景及注意事项,你是要注意的!

goCenter · 11天之前 · 156 次点击 · 预计阅读时间 4 分钟 · 大约8小时之前 开始浏览    

文章来自:Go语言圈

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

理解这句话主要在三个方面:

  • 返回前执行,当然函数可能没有返回值
  • 传入的函数,即 defer 关键值后面跟的是一个函数,包括普通函数如(fmt.Println), 也可以是匿名函数 func()

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

func createPost(db *gorm.DB) error {
    tx := db.Begin()
    // 用来回滚数据库事件
    defer tx.Rollback()

    if err := tx.Create(&Post{Author: "Draveness"}).Error; err != nil {
        return err
    }

    return tx.Commit().Error
}

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

1.2 注意事项
使用defer时会遇到两个常见问题,这里会介绍具体的场景并分析这两个现象背后的设计原理:

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

defer 关键字传入的函数会在函数返回之前运行。

假设我们在 for 循环中多次调用 defer 关键字:

package main

import "fmt"

func main() {
    for i := 0; i < 5; i++ {
        // FILO, 先进后出, 先出现的关键字defer会被压入栈底,会最后取出执行
        defer fmt.Println(i)
    }
}

//运行 $ go run main.go
4
3
2
1
0

运行上述代码会倒序执行传入 defer 关键字的所有表达式,因为最后一次调用 defer 时传入了 fmt.Println(4),所以这段代码会优先打印 4。我们可以通过下面这个简单例子强化对 defer 执行时机的理解:

package main

import "fmt"

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 函数运行的时间,可能会写出以下的代码:

package main

import (
    "fmt"
    "time"
)

func main() {
    startedAt := time.Now()
    // 这里误以为:startedAt是在time.Sleep之后才会将参数传递给defer所在语句的函数中
    defer fmt.Println(time.Since(startedAt))

    time.Sleep(time.Second)
}

输出
$ go run main.go
0s

上述代码的运行结果并不符合我们的预期,这个现象背后的原因是什么呢?

经过分析(或者使用debug方式),我们会发现:

调用 defer 关键字会立刻拷贝函数中引用的外部参数

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

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

package main

import (
    "fmt"
    "time"
)

func main() {
    startedAt := time.Now()
    // 使用匿名函数,传递的是函数的指针
    defer func() {
        fmt.Println(time.Since(startedAt))
    }()

    time.Sleep(time.Second)
}

//输出
1.0056135s

2 defer 数据结构

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

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

简单介绍一下 runtime._defer 结构体中的几个字段:

  • siz 是参数和结果的内存大小;
  • sppc 分别代表栈指针和调用方的程序计数器;
  • fndefer 关键字中传入的函数;
  • _panic 是触发延迟调用的结构体,可能为空;
  • openDefer 表示当前 defer 是否经过开放编码的优化;

除了上述的这些字段之外,runtime._defer 中还包含一些垃圾回收机制使用的字段, 这里不做过多的说明

3 执行机制

堆分配、栈分配和开放编码是处理 defer 关键字的三种方法。

早期的 Go 语言会在堆上分配, 不过性能较差 Go 语言在 1.13 中引入栈上分配的结构体,减少了 30% 的额外开销 在1.14 中引入了基于开放编码的 defer,使得该关键字的额外开销可以忽略不计 堆上分配暂时不做过多的说明

3.1 栈上分配
在 1.13 中对 defer 关键字进行了优化,当该关键字在函数体中最多执行一次时,会将结构体分配到栈上并调用。

除了分配位置的不同,栈上分配和堆上分配的runtime._defer 并没有本质的不同,而该方法可以适用于绝大多数的场景,与堆上分配的 runtime._defer 相比,该方法可以将 defer 关键字的额外开销降低 ~30%。

3.2 开放编码
在 1.14 中通过开放编码(Open Coded)实现 defer 关键字,该设计使用代码内联优化 defer 关键的额外开销并引入函数数据 funcdata 管理 panic 的调用3,该优化可以将 defer 的调用开销从 1.13 版本的~35ns 降低至 ~6ns 左右:

然而开放编码作为一种优化 defer 关键字的方法,它不是在所有的场景下都会开启的,开放编码只会在满足以下的条件时启用:

函数的 defer 数量小于或等于8个; 函数的 defer 关键字不能再循环中执行 函数的 return 语句 与 defer 语句个数的成绩小于或者等于15个。


有疑问加站长微信联系(非本文作者))

280

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:701969077


Recommend

  • 57
    • studygolang.com 6 years ago
    • Cache

    Go语言中defer的一些坑

    defer语句是Go中一个非常有用的特性,可以将一个方法延迟到包裹该方法的方法返回时执行,在实际应用中,defer语句可以充当其他语言中try…catch…的角色,也可以用来处理关闭文件句柄等收尾操作。 defer触发时机 A "d...

  • 17
    • www.v2ex.com 6 years ago
    • Cache

    英文读音还是要注意的

    发音 - @leon2013 - 曾经英文六级轻松过,毕业之后很少用,平时读英文比较随意,结果上次面试被鄙视了JIRA “机 ra ”,我读成了“加 ra ”Scrum meeting 的 scrum, /skrʌm/ ,

  • 34

    对于Go语言的defer语句,或许你回经历一个 赞赏 --> 怀疑 --> 肯定 --> 再怀疑 的一个过程,本文带你回顾一下defer的故事,以及如何在代码中...

  • 30

    golang中的slice很灵活,功能也很强悍,不过对于初学者来说会容易被它坑到,此篇文章就尽量提及到使用slice的一些容易容易出错的地方,以下示例使用的golang版本为1.14.2。 作为参数传递 在go语言中的方法的参数都为...

  • 2

    TcaplusDBlist 表的使用场景及注意事项? 开源问答 ...

  • 5

    Go Quiz: 从Go面试题看defer的注意事项第2篇发布于 今天 13:58 这是Go Quiz系列的第5篇,是考察Go语言的defer语义,也是defer语义的第2...

  • 2

    Golang中使用defer时注意io缓冲区刷新问题 2020-06-22 :: 荒野無燈 #golang  #defer  June 22, 2020

  • 4

    生活中有哪些需要注意的防疫要点?市疾控中心专家提出个人防护注意事项 2022-11-25 作者:迪步 生活中需要注意哪些防疫要点?市疾控中心专家针对...

  • 6

    Redis 是一个开源、高性能的内存键值存储系统,支持多种数据结构,例如字符串、哈希表、列表、集合等。它具有...

  • 7

    在Go语言中,init()函数是一种特殊的函数,用于在程序启动时自动执行一次。它的存在为我们提供了一种机制,可以在程序启动时进行一些必要的初始化操作,为程序的正常运行做好准备。 在这篇文章中,我们将详细探讨init()函数的特点...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK