> ## Content Index
> Fetch the complete content index at: https://huizhou92.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Golang 1.24: runtime.AddCleanup 改进 runtime.SetFinalizer 的一些问题
- URL: https://huizhou92.com/golang-1-24-runtime-addcleanup-gai-jin-runtime-setfinalizer-de-yi-xie-wen-ti/
- Published: 2024-12-17T01:56:00.000Z
- Updated: 2026-09-08T02:29:52.000Z
- Description: Golang 1.24 introduces `runtime.AddCleanup`, improving upon `runtime.SetFinalizer` by allowing multiple cleanup functions and better handling of object references, thus preventing memory leaks and enabling timely cleanup. #Golang #Go1_24
- Author: huizhou92
- Tags: tech, go1_24, golang, #lang-zh-cn

以前，我写过一篇[文章](https://huizhou92.com/content/files/2026/09/decrypt-go-runtime-setfinalizer-adf19cb5fdcb-2.html)介绍`runtime.SetFinalizer` 这个函数，用于在对象被清理的时候调用，但是这个函数有一些问题，导致它的使用频率比较低。

> - 在 Go 中使用 `SetFinalizer` 方法时，必须确保传递给它的对象引用指向分配内存的起始位置（即分配内存块的第一个字节）。这要求程序员理解“分配”（allocation）的概念，而这个概念通常在 Go 语言的抽象层级中并未明确暴露。
> - 对象只能定义一个`SetFinalizer`。
> - 带有`SetFinalizer`的对象如果涉及到任何引用循环，将无法被释放，`SetFinalizer`也不会运行。
> - 带有`SetFinalizer`的对象至少需要两个 GC 循环才能被释放。  
> [https://github.com/golang/go/issues/67535](https://github.com/golang/go/issues/67535?ref=huizhou92.com)

对于第一条，我们可以用一个例子说明

```go
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函数的原型如下：

```go
func AddCleanup[T, S any](ptr *T, cleanup func(S), arg S) Cleanup

```

基于此，可以写一个RAII(Resource Acquisition Is Initialization)的 Demo 。在[go weak](https://huizhou92.com/content/files/2026/09/go1-24-new-std-lib-weak-6e0a79a27902-2.html) 的文章中，我们实现了一个固定长度的`cache`，改一下代码，添加一个`newElemWeak` 方法，当最旧的elem 被逐出的时候，会自动调用`delete` 将`key` 从`cache` 中删除。不需要我们手动管理。

```go
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](https://gist.github.com/hxzhouh/5bb1d55259dcb4dab87b37beaef9bea2?ref=huizhou92.com)  
[View code snippet on GitHub Gist →](https://gist.github.com/hxzhouh/5bb1d55259dcb4dab87b37beaef9bea2?ref=huizhou92.com)

```go
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](https://go.dev/play/p/zpZ%5FoMnpJRv?v=gotip&ref=huizhou92.com)  
当给一个`ptr`绑定多个`cleanup` 的时候，因为`cleanup` 是在一个独立的`goroutine` 中运行，所以它的运行顺序可能是不固定的。

特别是，如果几个对象相互指向并且同时变成 unreachable，它们的清理函数都可以运行，并且可以以任何顺序运行。即使对象形成一个循环也是如此（`runtime.SetFinalizer` 在这种情况下会产生内存泄露）。  
例如

```go
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](https://gist.github.com/hxzhouh/ca402c723faa78726baba7e56bff573a?ref=huizhou92.com)  
[View code snippet on GitHub Gist →](https://gist.github.com/hxzhouh/ca402c723faa78726baba7e56bff573a?ref=huizhou92.com)

```shell
➜  AddCleanUp git:(main) ✗ gotip run main.go
Cleanup for Y
Cleanup for X

```

`SetFinalizer` 的会阻止 GC，但是 `AddCleanup` 能够正常执行

### 参考资料

1. [https://github.com/golang/go/issues/67535](https://github.com/golang/go/issues/67535?ref=huizhou92.com)