runtime.SetFinalizer 不保证执行,不可替代 Close();它仅在对象不可达且 GC 触发时“顺手”调用,存在不执行、延迟执行、类型敏感、阻塞 GC、循环引用致内存泄漏等严重限制,仅适用于 CGO 内存兜底或调试报警。
runtime.SetFinalizer 不保证执行,别当 Close() 用
它不是析构函数,也不是资源清理的可靠路径。gc 只在对象真正不可达、且运行时有空闲时“顺手”调用,程序一退出(main 返回)、或 gc 没触发,finalizer 就永远不跑。常见翻车是:文件没关、c 内存没 free、连接没断,结果服务跑几天后 too many open files 报错。
- 测试中加
runtime.GC()+time.Sleep也不保险——GC 可能跳过小对象,或因栈变量残留误判“可达” - 标准库如
os.File用finalizer是兜底,前提是用户已显式调用Close();你不能反过来依赖它 - 一旦
finalizer里调了obj.Close(),而用户又手动调了一次,就可能 double-close panic
传参类型错一个字节,就静默失效
runtime.SetFinalizer 对参数类型极其敏感,错一点就注册失败,还不报错。
- 第一个参数必须是显式的
*T指针,比如obj := &MyStruct{}; runtime.SetFinalizer(obj, f) - 禁止传
&MyStruct{}(临时对象地址可能被优化掉) - 禁止传
interface{}或any包装后的值——底层结构不同,类型不匹配,finalizer直接丢弃 - 函数签名必须是
func(*T),不能是func(T)(值类型)、也不能是方法值如(t *T).Close(会隐含捕获t,导致对象无法回收)
finalizer 函数里干了重活,GC 就卡住
finalizer 运行在 GC 的辅助 goroutine 中,阻塞它 = 拖慢整个 GC 周期。线上出现 STW 时间飙升、吞吐骤降,常源于此。
- 禁止调用
net.Dial、os.Remove、time.Sleep、log.Printf(内部用锁+channel) - 禁止启动新 goroutine、访问已关闭 channel、解引用可能已释放的字段(如
obj.buf[:]) - 禁止调
runtime.GC()—— 直接死锁 - 推荐只做无副作用的轻量操作:原子计数减一、打一句
log.Println("leaked resource")、或仅closeFD(r.fd)这类无锁系统调用(且确保r.fd有效)
循环引用 + finalizer = 内存永不释放
哪怕只是两个结构体互相存对方指针,再给其中一个注册 finalizer,GC 就判定它们都“可达”,谁也不回收。
- 典型模式:
x.Other = &y; y.Other = &x; runtime.SetFinalizer(&x, f)→x和y永远存活 - finalizer 本身若闭包捕获了原对象(如
func() { use(x) }),也构成隐式引用循环 - 解决办法不是靠 finalizer 清理,而是提前切断引用链;真要用,得在
Close()里显式调runtime.SetFinalizer(obj, nil) - 更隐蔽的是:goroutine 栈、未清空的
map、全局sync.Pool缓存,都可能让对象“意外存活”
Close() + defer + context.Context 这条正路。











