Is there a way to program an automatic cancellation of a goroutine associated with a struct when the struct is no longer needed?
I implemented a simple cache for string values (potentially duplicates). Values are stored for a short period of time, so every entry has an expiration time and cache requires a background job that would remove expired values every N minutes.
So I came up with this solution:
type StrCache struct {
// ... Values storage
expiration time.Duration
}
func NewStrCache(expiration time.Duration) *StrCache {
cache := StrCache{}
cache.expiration = expiration
go cache.backgroundCleanup()
return &cache
}
func (s *StrCache) TryAdd(value string) (alreadyAdded bool) {
// ... Logic of adding a new value
}
func (s *StrCache) backgroundCleanup() {
time.Sleep(s.expiration)
// ... Removing expired values logic
go s.backgroundCleanup()
}
This suites my use case since StrCache
should live until app exits. But I don't like it's potentially leaky design: if at some point I would need to replace one instance of StrCache
by another, the first instance is not going to be cleaned up by GC because a reference to it is held by a self-replicating backgroundCleanup
goroutine.
I also don't want to introduce a separate method that would signal to stop to background goroutine, since it requires additional actions by package user and introduces a chance to make a mistake.
Is there a way to automatically cancel the backgroundCleanup
when struct is no longer used? Or should I opt out into other design and only spawn cleanup goroutines in TryAdd
method? I wanted to avoid it because TryAdd
uses a mutex for all operations on StrCache
and holding it locked for longer is undesirable.
I suggest that you consider a different approach for the background goroutine.
On a high level, you should use a context.Context
in your goroutine which respects context cancellation.
func (s *StrCache) backgroundCleanup(ctx context.Context) {
for {
select {
case <-time.After(s.expiration):
// ... Removing expired values logic
break
case <-ctx.Done():
return
}
}
}
This pattern will loop endlessly, performing the internal cleanup of expired values. The addition of the context value allows you to quit this background goroutine.
[...] if at some point I would need to replace one instance of StrCache by another [...]
... then I think you should cancel the context of the previous StrCache
.
I also don't want to introduce a separate method that would signal to stop to background goroutine, since it requires additional actions by package user and introduces a chance to make a mistake.
Yes, that is an extra thing users of this type must now consider. IMO that is fine since users of any library should respect concurrency related aspects.
There are plenty of std lib types, which are similar, in that they explicitly document that the usage of those types comes with the need to stop things / free resources (time.Timer is an example).
and a cancel function (see context.WithCancel).