go中的finalizer

概述

Go中,finalizer函数是当对象不在被引用,对象被GC进程选中并从内存中移除的时候将其调用,有点类似析构函数。可以使用runtime.SetFinalizer函数向对象添加finalizer

example

让我们看看它是如何工作的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"runtime"
"time"
)

type Test struct {
A int
}

func test() {
// create pointer
t := &Test{}
// add finalizer which just prints
runtime.SetFinalizer(t, func(t *Test) { fmt.Println("I AM DEAD") })
}

func main() {
test()
// run garbage collection
runtime.GC()
// sleep to switch to finalizer goroutine
time.Sleep(1 * time.Millisecond)
}

Output:

1
I AM DEAD

创建一个Test对象t,它是指针并为其设置finalizer,当代码运行完test()函数后,它不在被引用,因此GC能够在自己的goroutine中回收它并调用之前设置的finalizer。注意如果从Test类型中没有字段为空结构,而空结构不分配内存,并且不会被GCfinalizer也不会被调用。

标准库

在标准库中有些对象是默认设置了finalizer的。如

  • os.File对象会在GC时自动关闭文件
  • os.Process对象会在GC时将释放与该进程相关的任何资源
  • net.Conn对象会在GC时关闭网络连接

net.Conn为例,Conn会包含一个含有netFD(socket文件描述符)的conn,而netFD设置了关闭的finalizer。所以忘记调用Close去关闭连接,在GC时连接会被关闭,减少文件描述符泄露。

1
2
3
4
5
6
7
8
9
type conn struct {
fd *netFD
}

func (fd *netFD) setAddr(laddr, raddr Addr) {
fd.laddr = laddr
fd.raddr = raddr
runtime.SetFinalizer(fd, (*netFD).Close)
}

使用

在调试的时候可以利用finalizer添加日志,定位不符合预期的对象回收,跟踪资源的使用。

1
2
3
4
_, file, line, _ := runtime.Caller(1)
runtime.SetFinalizer(conn, func(conn *Conn) {
log.Printf("%s:%d: conn closed", file, line)
})

总结

个人认为大部分场景下都不应该使用finalizer

首先不能依赖finalizer来对对象进行资源管理,它并不能完全避免资源泄露,因为不显示调用runtime.GC()的话,则GC发生的时间不可控,如果在这段时间有太多资源不去手动释放的话很容易导致资源被耗尽。而且在对象被GC进程选中并从内存中移除以前,finalizer都不会执行,即使是程序正常结束或者发生错误。所以要养成在对象使用完手动进行释放的习惯。

而且SetFinalizer最大的问题是延长了对象生命周期。在第一次回收时执行finalizer,且目标对象重新变成被引用状态,直到第二次才真正回收。这对于有大量对象分配的高并发算法,可能会造成很大麻烦。如果使用finalizer进行统计和打印日志,实际上会耗费太多的cpu资源。