Sharp-Edged Finalizers in Go

2018-04-05, David Crawshaw

For background, see my last post on why in general finalizers do not work.

We cannot use an object finalizer for resource management, because finalizers are called at some unpredictable distant time long after resources need to be reclaimed.

However, a finalizer does provide us with a bound on when a managed resource needs to have been released. If we reach an object finalizer, and a manually managed resource has not been freed, then there is a bug in the program.

So we can use finalizers to detect resource leaks.

package db func Open(path string, flags OpenFlags) (*Conn, error) { // ... runtime.SetFinalizer(conn, func(conn *Conn) { panic("open db connection never closed") }) return conn, nil } func (c *Conn) Close() { // ... runtime.SetFinalizer(conn, nil) // clear finalizer }

This is a sharp-edged finalizer. Misuse the resource and it will cut your program short.

I suspect this kind of aggressive finalizer is off-putting to many, who view resource management something nice to have. But there are many programs for which correct resource management is vital. Leaking a resource can leave to unsuspecting crashes, or data loss. For people in similar situations, you may want to consider a panicing finalizer.

Debugging

One big problem with the code above is the error message is rubbish. You leaked something. OK, great. Got any details?

Ideally the error would point at exactly where we need to release the resource, but this is a hard problem. One cheap and easy alternative is to point to where the resource was originally acquired, which is straightforward:

_, file, line, _ := runtime.Caller(1) runtime.SetFinalizer(conn, func(conn *Conn) { panic(fmt.Sprintf("%s:%d: db conn not closed", file, line)) })

This prints the file name and line number of where the connection was created, which is often useful in tracing a leaked resource.

Why not just log?

Because I have found myself ignoring logs, time and again.