​解密 Go runtime.SetFinalizer 的使用

解密 Go runtime.SetFinalizer 的使用

原创 Go Official Blog Go Official Blog
 

如果我们想在对象 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 合集 · 目录
上一篇Some Go web dev notes
阅读 710
 
posted @   技术颜良  阅读(101)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 全程使用 AI 从 0 到 1 写了个小工具
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)
历史上的今天:
2022-10-06 为什么有HTTP协议,还要有websocket协议
2020-10-06 redis集群
2020-10-06 RabbitMQ集群搭建-镜像模式
点击右上角即可分享
微信分享提示