Go’s wrapped return pattern to clean up objects with background goroutines
Protecting against memory leaks
You have a constructor function that creates an object with a background goroutine. You want the object to garbage collect itself even if people don’t explicitly close it. This isn’t possible since since the background goroutine is always running and pointing to your created object.
Wrap the returned object and use a finalizer on the returned object to shut down the background goroutine.
Consider the Go statsd client go-statsd-client. The code to create a new buffered sender is below.
Start function spawns a goroutine to periodically flush the buffering sender.
Let’s now create and use a buffered sender to see what happens.
Initially x is pointed to by the main goroutine, but when we exit
Process BufferedSender is still around because the
start goroutine has not ended.
BufferedSender because we forgot to call
Consider the Go cache library go-cache. You’ll notice the
Cache type is actually a wrapper.
When you create a new
Cache object it runs a janitor that points to the wrapped object, not the object that’s returned.
Let’s now draw what this looks like when you use it.
The important difference here is that the
Cache object can be garbage collected, even if the
cache object cannot yet. We set GC behavior on an object with SetFinalizer. The stopJanitor function signals the background goroutine to stop.
With the background goroutine stopped, nothing is pointing to
At this point, the entire object can be garbage collected.
When to use
Ideally you should defer to users of your library how they want to manage cleanly creating or shutting down background processing. Go’s http.Serve is a great example of this. Notice how it’s not
func NewHTTPServer() *http.Server, but instead the object is used and users can explicitly start (or stop) the server when ready.
Despite this best practice, if you do want to control when background processing starts, you should still expose a
Close function similar to the statsd client to allow users to immediately and explicitly free up a resource. If you feel it’s difficult for users of your library to remember or abstract in an explicit close, you can add a wrapped finalizer to ensure memory and goroutines you create can be eventually freed regardless of mistakes to forget to call