Go’s wrapped return pattern to clean up objects with background goroutines

Jack Lindamood
Jul 2 · 3 min read

Problem

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.

Solution

Wrap the returned object and use a finalizer on the returned object to shut down the background goroutine.

Problem example

Consider the Go statsd client go-statsd-client. The code to create a new buffered sender is below.

The 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.

Inside Process
When leaving Process

We’ve leaked BufferedSender because we forgot to call Close.

Solution example

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.

Inside Process
Outside process

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 cache either.

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 Close.

Jack Lindamood

Written by

Software Engineer

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade