Decrypt Go: `runtime.SetFinalizer`

Decrypt Go: `runtime.SetFinalizer`. If we want to do some resource release before an object is GC, we can use returns.SetFinalizer . It’s like executing de。

分享
Decrypt Go: `runtime.SetFinalizer`

If we want to do some resource release before an object is GC, we can use returns.SetFinalizer. It’s like executing defer to free resources before a function returns.
For example:

List 1: Using 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()  
}

The official documentation explains that SetFinalizer associates a finalizer function with an object. When the garbage collector (GC) detects that an unreachable object has an associated finalizer, it will execute the finalizer and disassociate it. The object will be collected on the next GC cycle if it is unreachable and no longer has an associated finalizer.

Important Considerations

While runtime.SetFinalizer can be helpful, there are a few critical points to keep in mind:

  • Deferred Execution: The SetFinalizer function will not execute until the object is selected for garbage collection. Therefore, avoid using SetFinalizer for actions like flushing in-memory content to disk.
  • Extended Object Lifecycle: SetFinalizer can inadvertently extend an object's lifecycle. The finalizer function executes during the first GC cycle, and the target object may become reachable again, delaying its final destruction. This can be problematic in high-concurrency algorithms with numerous object allocations.
  • Memory Leaks with Cyclic References: Using runtime.SetFinalizer, in conjunction with cyclic references, can lead to memory leaks.

List 2: Memory Leak with runtime.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() 
}

In this code, the object x will never be released. The correct approach explicitly removes the finalizer when the object is no longer needed: runtime.SetFinalizer(&x, nil).

Practical Applications

While runtime.SetFinalizer is rarely used in business code (I've never used it); it is more commonly employed within the Go source code itself. For instance, consider the following usage in the net/http package:

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()  
}

The go-cache library also demonstrates a use case for 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
		}
	}
}

In newCacheWithJanitor, when the ci parameter is greater than 0, a background goroutine is started to clean up expired cache entries periodically via a ticker. The asynchronous goroutine exits once a value is read from the stop channel.

The stopJanitor function defines a finalizer for the Cache pointer C. When there are no more references to Cache in the business code, the GC process triggers the stopJanitor function, which writes a value to the internal stop channel. This notifies the asynchronous cleanup goroutine to exit, providing a graceful and business-agnostic way to reclaim resources.

阅读更多

那些意外变成特性的 Bug:当代码漏洞改写计算史

那些意外变成特性的 Bug:当代码漏洞改写计算史

那些意外变成特性的 Bug:当代码漏洞改写计算史. 比起草台班子将错就错的神话,面对异常数据时的观测定力才是真的 社交网络上常年流传着一类爽文:某家公司的程序员把代码写崩了,结果不仅没被开除,这个 Bug 反而成了拳头产品,妥妥的爽文主角剧本。 最常被搬出来的有三个:Audio Hijack 写错试用期拯救公。

By huizhou92