解密 Go runtime.SetFinalizer 的使用
原创 Go Official Blog Go Official Blog 2024年10月05日 18:45 中国香港 听全文如果我们想在对象 GC 之前释放一些资源,可以使用 returns.SetFinalizer。这就像在函数返回前执行 defer 来释放资源一样。例如:
1:使用 runtime.SetFinalizer
type MyStruct struct {
Name string
Other *MyStruct
}
func main() {
x := MyStruct{Name: "X"}
runtime.SetFinalizer(&x, func(x *MyStruct) {
fmt.Printf("Finalizer for %s is called\n", x.Name)
})
runtime.GC()
time.Sleep(1 * time.Second)
runtime.GC()
}
官方文档[1]解释说,SetFinalizer 会将终结器函数与对象关联起来。当垃圾收集器(GC)检测到一个不可访问的对象有一个关联的终结器时,它会执行终结器并解除关联。如果该对象是不可到达的,并且不再有关联的终结器,那么它将在下一个 GC 周期被收集。
重要考虑因素
虽然 runtime.SetFinalizer 很有用,但有几个关键点需要注意:
-
延迟执行:SetFinalizer 函数在对象被选中进行垃圾回收之前不会执行。因此,应避免将 SetFinalizer 用于将内存中的内容刷新到磁盘等操作。
-
扩展对象生命周期:SetFinalizer 会无意中延长对象的生命周期。终结器函数会在第一个 GC 循环期间执行,目标对象可能会再次变得可触及,从而延迟其最终销毁。这在具有大量对象分配的高并发算法中可能会造成问题。
-
循环引用的内存泄漏:与循环引用一起使用 runtime.SetFinalizer 可能会导致内存泄漏。
2:运行时.SetFinalizer 的内存泄漏
type MyStruct struct {
Name string
Other *MyStruct
}
func main() {
x := MyStruct{Name: "X"}
y := MyStruct{Name: "Y"}
x.Other = &y
y.Other = &x
runtime.SetFinalizer(&x, func(x *MyStruct) {
fmt.Printf("Finalizer for %s is called\n", x.Name)
})
time.Sleep(time.Second)
runtime.GC()
time.Sleep(time.Second)
runtime.GC()
}
在这段代码中,对象 x 永远不会被释放。正确的方法是在不再需要该对象时,显式地移除终结器:runtime.SetFinalizer(&x, nil)。
实际应用
虽然 runtime.SetFinalizer 很少在业务代码中使用(我从未使用过),但它在 Go 源代码本身中使用得更为普遍。例如,考虑一下 net/http 包中的以下用法:
func (fd *netFD) setAddr(laddr, raddr Addr) {
fd.laddr = laddr
fd.raddr = raddr
runtime.SetFinalizer(fd, (*netFD).Close)
}
func (fd *netFD) Close() error {
if fd.fakeNetFD != nil {
return fd.fakeNetFD.Close()
}
runtime.SetFinalizer(fd, nil)
return fd.pfd.Close()
}
go-cache[2] 还展示了 SetFinalizer 的一个用法:
func New(defaultExpiration, cleanupInterval time.Duration) *Cache {
items := make(map[string]Item)
return newCacheWithJanitor(defaultExpiration, cleanupInterval, items)
}
func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {
c := newCache(de, m)
C := &Cache{c}
if ci > 0 {
runJanitor(c, ci)
runtime.SetFinalizer(C, stopJanitor)
}
return C
}
func runJanitor(c *cache, ci time.Duration) {
j := &janitor{
Interval: ci,
stop: make(chan bool),
}
c.janitor = j
go j.Run(c)
}
func stopJanitor(c *Cache) {
c.janitor.stop <- true
}
func (j *janitor) Run(c *cache) {
ticker := time.NewTicker(j.Interval)
for {
select {
case <-ticker.C:
c.DeleteExpired()
case <-j.stop:
ticker.Stop()
return
}
}
}
在 newCacheWithJanitor 中,当 ci 参数大于 0 时,会启动一个后台程序,通过 ticker 定期清理过期的缓存条目。一旦从停止通道读取到一个值,异步程序就会退出。
stopJanitor 函数定义了 Cache 指针 C 的终结器。当业务代码中不再引用 Cache 时,GC 进程会触发 stopJanitor 函数,并向内部 stop 通道写入一个值。这将通知异步清理 goroutine 退出,从而提供了一种优雅且与业务无关的资源回收方式。
参考资料[1]SetFinalizer Doc: https://pkg.go.dev/runtime#SetFinalizer
[2]go-cache: https://github.com/patrickmn/go-cache/blob/46f407853014144407b6c2ec7ccc76bf67958d93/cache.go#L1123
Go Official Blog
你的肯定是对我最大的鼓励
Go blog 合集131 Go blog 合集 · 目录 上一篇Some Go web dev notes 阅读 710 标签:GC,func,time,Go,SetFinalizer,runtime From: https://www.cnblogs.com/cheyunhua/p/18449203