以前,我写过一篇文章介绍runtime.SetFinalizer
这个函数,用于在对象被清理的时候调用,但是这个函数有一些问题,导致它的使用频率比较低。
- 在 Go 中使用
SetFinalizer
方法时,必须确保传递给它的对象引用指向分配内存的起始位置(即分配内存块的第一个字节)。这要求程序员理解“分配”(allocation)的概念,而这个概念通常在 Go 语言的抽象层级中并未明确暴露。
- 对象只能定义一个
SetFinalizer
。
- 带有
SetFinalizer
的对象如果涉及到任何引用循环,将无法被释放,SetFinalizer
也不会运行。
- 带有
SetFinalizer
的对象至少需要两个 GC 循环才能被释放。
https://github.com/golang/go/issues/67535
对于第一条,我们可以用一个例子说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import (
"fmt"
"runtime"
)
type MyStruct struct {
Field int
}
func main() {
obj := &MyStruct{Field: 42}
runtime.SetFinalizer(obj, func(m *MyStruct) {
fmt.Println("Finalizer called for:", m)
})
// 错误示例:传递字段引用而非对象本身
// runtime.SetFinalizer(&obj.Field, func(m *int) {
// fmt.Println("Finalizer called for:", *m)
// })
}
|
基于上面的原因,在Golang 1.24
中添加了一个新的函数runtime.AddCleanUp
来替换 runtime.SetFinalizer
。
注意: runtime.AddCleanup
和 runtime.SetFinalizer
都不保证 清理函数一定会被执行。
AddCleanup的设计目标是解决runtime.SetFinalizer
的诸多问题,特别是避免对象复活,从而允许对象的及时清理,并支持对象的循环清理。
AddCleanup函数的原型如下:
1
|
func AddCleanup[T, S any](ptr *T, cleanup func(S), arg S) Cleanup
|
基于此,可以写一个RAII(Resource Acquisition Is Initialization)的 Demo 。在go weak
的文章中,我们实现了一个固定长度的cache
,改一下代码,添加一个newElemWeak
方法,当最旧的elem 被逐出的时候,会自动调用delete
将key
从cache
中删除。不需要我们手动管理。
1
2
3
4
5
6
7
|
func (c *WeakCache) newElemWeak(elem *list.Element) weak.Pointer[list.Element] {
elemWeak := weak.Make(elem)
runtime.AddCleanup(elem, func(name string) {
delete(c.cache, name)
}, elem.Value.(*CacheItem).key)
return elemWeak
}
|
需要注意的几个问题
AddCleanup
对ptr
的约束很少,支持为同一个指针附加多个清理函数。不过,如果ptr
可以从cleanup
或arg
中可达,ptr
将永远不会被回收(memory leak),清理函数也永远不会运行,目前来看这种情况也不会panic。 或许以后会用 GODEBUG=gccheckmark=1
这种模式来检测?
比如
https://gist.github.com/hxzhouh/5bb1d55259dcb4dab87b37beaef9bea2
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func NewFileResource(filename string) (*os.File, error) {
file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return nil, err
}
runtime.AddCleanup(file, func(f *os.File) {
fmt.Println("close f")
_ = f.Close()
}, file)
return file, nil
}
|
fmt.Println("close f")
不会被打印,file 也不会被关闭。
Try It
当给一个ptr
绑定多个cleanup
的时候,因为cleanup
是在一个独立的goroutine
中运行,所以它的运行顺序可能是不固定的。
特别是,如果几个对象相互指向并且同时变成 unreachable,它们的清理函数都可以运行,并且可以以任何顺序运行。即使对象形成一个循环也是如此(runtime.SetFinalizer
在这种情况下会产生内存泄露)。
例如
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
|
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)
//})
//runtime.SetFinalizer(&y, func(y *MyStruct) {
// fmt.Printf("Finalizer for %s is called\n", y.Name)
//})
xName := x.Name
runtime.AddCleanup(&x, func(name string) {
fmt.Println("Cleanup for", x)
}, xName)
yName := y.Name
runtime.AddCleanup(&y, func(name string) {
fmt.Println("Cleanup for", x)
}, yName)
time.Sleep(time.Millisecond)
runtime.GC()
time.Sleep(time.Millisecond)
runtime.GC()
}
|
https://gist.github.com/hxzhouh/ca402c723faa78726baba7e56bff573a
1
2
3
|
➜ AddCleanUp git:(main) ✗ gotip run main.go
Cleanup for Y
Cleanup for X
|
SetFinalizer
的会阻止 GC,但是 AddCleanup
能够正常执行
参考资料
- https://github.com/golang/go/issues/67535