likes
comments
collection
share

你是否了解 defer 的特性

作者站长头像
站长
· 阅读数 34

defer 函数是 go 语言用来延迟执行的语句,defer 语句会在函数返回之前执行,可以用来释放资源、解锁、关闭文件等操作

关于 defer 的一些特性,你是否足够了解?

defer 表达式会影响 return 的返回值

函数 fn 返回结果是啥?

func fn(n int) (r int) {
  defer func() {
    r += n
  }()
  r = n + n
  return
}
fmt.Println(fn(2)) // ?

函数 fn2 返回结果又是啥

func fn2(n int) int {
  var a int
  defer func() {
    a += n
  }()
  a = n + n
  return a
}
fmt.Println(fn2(2)) // ?

答案是:fn 函数的返回结果是 6fn2 函数的返回结果是 4

为什么 fnfn2 两个函数的返回结果不一样呢?

因为使用命名返回值 (r int) 相当于返回了 r 的地址(实际不是),但是返回值是 int,表示返回值已经确定是值,不是地址(在编译阶段确定的)

虽然 fn2 函数在 defer 中修改了 a 的值,但是返回值在编译阶段已经确定了,所以 fn2 函数的返回结果不受 defer 影响

我们再来看一个例子 fn3

func fn3(n int) *int {
  a := new(int)
  defer func() {
    *a = *a + n
  }()
  *a = n + n
  return a
}
fmt.Println(fn3(2)) // 6

我们将 fn3 的返回值修改为 *intdefer 函数就能影响返回值了

defer 语句修改返回值

有了上面的例子,我们再来看下面两个例子

fnfn2 函数的返回结果是啥?

func fn(n int) (r int) {
  defer func() {
    r = 2
  }()
  return 1
}
func fn2(n int) (r int) {
  r = 2
  return 1
}

fn 函数的返回结果是 2fn2 函数的返回结果是 1

为什么会出现这种现象呢?

return 是函数最后执行的语句,所以 return 后面表达式的结果会被赋值给命名返回值 (r int),所以 fn2 的返回结果是 1

但是 defer 语句是在 return 之后执行的,仅管已经确定了返回值,但由于是命名返回值,所以 defer 语句可以修改返回值,所以 fn 的返回结果是 2

defer 执行时机

deferreturn 谁先执行?

你可以会想到这样写

func main() {
  fmt.Println(fn())
}

func fn() string {
  defer func() {
    fmt.Println("defer")
  }()
  return "return"
}

输出结果是

// "defer"
// "return"

你可能会觉得 defer 是在 return 之前运行的

用这种方式判断其实错误的

因为这两个 fmt.Println 并不在同一个栈中,所以没办法判断到底哪个先运行的

你可能会想到,go 能不能 return 一个立即执行函数呢?

非常不好意思,go 不支持 return 一个立即执行函数

那有什么方法可以测试 defer 先执行还是 return 先执行呢?

我们可以通过打印时间戳的方法来判断 return 先执行还是 defer 先执行

因为在编译时,就已经确定了返回值,我们就可以通过这种方式来判断谁先执行的

func main() {
  fmt.Println(fn())
}

func fn() string {
  defer func() {
    now := time.Now()
    fmt.Printf("defer : %s\n", now.Format("2006-01-02 15:04:05.999999999"))
  }()

  now := time.Now()
  return fmt.Sprintf("return: %s", now.Format("2006-01-02 15:04:05.999999999"))
}

// defer : 2024-08-10 22:01:03.748488049
// return: 2024-08-10 22:01:03.748433719

从运行结果可以看出,defer 输出的时间晚于 return 的时间

defer 估值时刻

函数 fnfn2 的输出结果是啥?

func fn() {
  a := 1
  defer fmt.Println(a)
  a = 2
}

func fn2() {
  a := 1
  defer func() { fmt.Println(a) }()
  a = 2
}

fn 函数的输出结果是 1fn2 函数的输出结果是 2

因为 defer 后面的函数在编译阶段就已经确定了,不会改变,所以 fn 函数会输出 1

但是如果 defer 后面是一个闭包函数,那么这个闭包函数在执行时才能确定内部 a 的变量,所以 fn2 会输出 2

defer 调用 receive 函数输出结果

下面两个 defer 语句的输出结果是啥?

type number int

func (n number) print() {
  fmt.Println("print: ", n)
}

func (n *number) printptr() {
  fmt.Println("printptr: ", *n)
}

func main() {
  var num number
  defer num.print()
  defer num.printptr()
  num = 3
}

defer 是倒序执行,先声明的后执行,所以 n.printptr() 先执行,输出 3n.print() 后执行,输出 0

是不是很神奇?

n.printptr 输出 3 比较好理解,因为这是 printptr 方法的接收者是指针类型,所以 n 会拿到最新的值

n.print 虽然是值类型接收器,不是说 go 会隐式实现 func (n *number) print() 方法吗?那为什么拿到的还是 0 呢?

因为 func (n number) print()go 确实会为类型隐式地实现 func (n *number) print()这个方法。但这并不意味着 number 类型会获得一个可以解引用的指针方法,它只是意味着可以用 *number 类型的变量调用 print() 方法,也就是说 var num numbervar num *number = new(number) 都可以调用 print() 方法`

defer 不推荐使用的场景

  1. 在循环中使用 defer,会导致 defer 语句在循环结束时才执行,可能会导致资源泄露
func main() {
  for i := 0; i < 1000000; i++ {
    defer fmt.Println("deferred", i)
  }
}

改进,将 for 循环中的 defer 语句放到一个函数中

func main() {
  for i := 0; i < 1000000; i++ {
    func() {
      defer fmt.Println("deferred", i)
    }()
  }
}
  1. 不要在 defer 中使用大对象或者大的变量,可能会导致内存泄露
func main() {
  data := make([]byte, 102*1024*1024)
  defer func() {
    fmt.Println("defer", data)
  }()
}

改进,显示的释放资源

func main() {
  data := make([]byte, 102*1024*1024)
  defer func() {
    fmt.Println("defer", data)
    data = nil
  }()
}

defer 结构体

  • runtime.deferproc 函数用于将一个函数和其他参数封装成一个 _defer 结构体,并插入当前协程的 defer 栈中
  • runtime.deferreturn 函数用于在函数返回时执行当前 defer 栈中的 _defer 结构体
  • runtime.deferredFunc 函数用于执行 _defer 结构体中封装的函数,并且在执行中捕获 panic
type _defer struct {
  // 参数和返回值的大小
  siz int32
  // 表示该 defer 语句是否已经开始执行
  started bool
  // 表示盖 _defer 语句的优先级
  // 当一个 _defer 语句被执行时,它会被添加到 _defer 语句中,而 heap 字段则用于将 _defer 语句添加到一个优先队列中
  // 以便在函数返回时按照一定的顺序执行 _defer 语句。在 _defer 链表中,后添加的 _defer 语句会被先执行,而在优先队列中
  // heap 值较小的 _defer 语句会被先执行。这个字段的值是在 _defer 语句被添加到 _defer 链表是柑橘一定规则计算出来的
  // 通常是根据 _defer 语句的执行顺序和做用户等因素计算而得。在函数返回时,go 语言会按照 heap 值的大小顺序执行 z_defer 语句
  // 如果多个 _defer 语句的 heap 值相同,那么它们会按照添加的顺序执行
  // 这个机制可以确保 _defer 语句按照一定的顺序执行,从而避免了一些潜在的问题
  heap bool
  // 表示该 _defer 用于具有开放式编码 _defer 的帧。开放式编码 _defer 是指在编译时已经确定 _defer 语句的数量和位置
  // 而不是在运行时动态添加 _defer 语句。在一个帧中,可能会有多个 _defer 语句,但只会有一个 _defer 结构体记录了所有 _defer 语句的信息
  // 而 openDefer 就是用来表示该 _defer 结构体是否是针对开放式编码 _defer 的
  openDefer bool
  // _defer 语句所在栈帧的栈指针(stack pointer)
  // 在函数调用时,每个函数都会创建一个新的栈帧,用于保存函数的局部变量,参数和返回值等信息
  // 而 _defer 语句也被保存在这个栈帧中,因此需要记录栈指针以便在函数返回时找到 _defer 语句
  // 当一个 _defer 语句被执行时,它会被添加到 _defer 链表中,并记录当前栈帧的栈指针
  // 在函数返回时,go 语言会遍历 _defer 链表,并执行其中的 _defer 语句,而在执行 _defer 语句时
  // 需要之用保存在 _defer 结构体中的栈指针来访问 _defer 语句所在栈帧中的局部变量和参数等信息
  // 需要注意的是,由于 _defer 语句是在函数返回之前执行的,因此在执行 _defer 语句时,函数的栈帧可能已经被销毁
  // 因此,sp 字段的值不能直接使用,需要通过一些额外的处理来确保 _defer 语句能够正确地访问栈帧中的信息
  sp uintptr
  // _defer 语句的程序计数器(program counter)
  // 程序计数器是一个指针,指向正在执行的函数中的下一条指令,在 _defer 语句被执行时,它会被添加 _defer 链表中
  // 并记录当前函数的程序计数器,当函数返回时,go 语言会遍历 _defer 链表,并执行其中的 _defer 语句
  // 而在执行 _defer 语句时,需要让程序计数器指向 _defer 语句中的函数调用,以便正确地执行 _defer 语句中的代码
  // 这就是为什么 _defer 语句需要记录程序计数器的原因。需要注意的是,由于 _defer 语句是在函数返回之前执行的
  // 因此在执行 _defer 语句时,程序计数器可能已经指向了其他的函数或代码块。因此,在执行 _defer 语句时
  // 需要使用保存在 _defer 结构体中的程序计数器来确保 _defer 语句中的代码能够正确的执行
  pc uintptr // pc 计数器,程序计数器
  // _defer 语句的函数地址,也就是验收执行的函数
  fn *funcval
  // _defer 的 panic 结构体
  _panic *_panic
  // 用于将多个 defer 链接起来,形成一个 defer 栈
  // 当程序执行到一个 defer 语句时,会将该 defer 语句封装成一个 _defer 结构体,并将其插入到 defer 栈的顶部
  // 当函数返回时,程序会从 defer 栈的顶部开始依此执行每个 defer 语句,直到 defer 栈为空为止
  // 每当 _defer 结构体中的 link 字段指向下一个 _defer 结构体,从而将多个 _defer 结构体链接在一起
  // 当程序执行完一个 defer 语句后,会将该 defer 语句从 defer 中弹出,并将其 link 字段指向下一个 _defer 结构体,设置为当前的 defer 栈顶
  // 这样,当函数返回时,程序会依此执行每个 defer 语句,从而实现 defer 语句的反转执行顺序的效果
  // 需要注意的是,由于 _defer 结构体实在运行时动态创建的,因此 defer 栈的大小是不固定的
  // 在编写程序时,应该避免在单个函数中使用大量的 defer 语句,以免导致 defer 栈溢出
  link *_defer
}
转载自:https://juejin.cn/post/7401327691893243930
评论
请登录