Problem Link to heading

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 Link to heading

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

Problem example Link to heading

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

func NewBufferedSender(addr string, flushInterval time.Duration, flushBytes int) (Sender, error) {
    simpleSender, err := NewSimpleSender(addr)
    if err != nil {
        return nil, err
    }

    sender := &BufferedSender{
        flushBytes:    flushBytes,
        flushInterval: flushInterval,
        sender:        simpleSender,
        buffer:        senderPool.Get(),
        shutdown:      make(chan chan error),
    }

    sender.Start()
    return sender, nil
}

The Start function spawns a goroutine to periodically flush the buffering sender.

func (s *BufferedSender) Start() {
    // write lock to start running
    s.runmx.Lock()
    defer s.runmx.Unlock()
    if s.running {
        return
    }

    s.running = true
    s.bufs = make(chan *bytes.Buffer, 32)
    go s.run()
}

Let’s now create and use a buffered sender to see what happens.

func Process() {
    x := statsd.NewBufferedSender("localhost:2125", time.Second, 1024)
    x.Inc("stat", 1, .1)
}

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 Link to heading

Picture of process objects

Inside Process

Outside process Link to heading

Picture of process objects

When leaving Process

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

Solution example Link to heading

Consider the Go cache library go-cache. You’ll notice the Cache type is actually a wrapper.

type Cache struct {
    *cache
    // If this is confusing, see the comment at the bottom of New()
}

type cache struct {
    defaultExpiration time.Duration
    items             map[string]Item
    mu                sync.RWMutex
    onEvicted         func(string, interface{})
    janitor           *janitor
}

When you create a new Cache object it runs a janitor that points to the wrapped object, not the object that’s returned.

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)
    // This trick ensures that the janitor goroutine (which--granted it
    // was enabled--is running DeleteExpired on c forever) does not keep
    // the returned C object from being garbage collected. When it is
    // garbage collected, the finalizer stops the janitor goroutine, after
    // which c can be collected.
    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)
}

Let’s now draw what this looks like when you use it.

func Process() {
    x := cache.New(time.Second, time.Minute)
}
Picture of process objects

Inside Process (with Janitor)

Picture of objects outside the running process

Outside Process (with Janitor)

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.

runtime.SetFinalizer(C, stopJanitor)
...
...
func stopJanitor(c *Cache) {
    c.janitor.stop <- true
}

With the background goroutine stopped, nothing is pointing to cache either.

Picture of objects outside the running process with Janitor object working

Cache with Janitor

At this point, the entire object can be garbage collected.

When to use Link to heading

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.