I ended up solving this using the atomic.Pointer
API, and rolled it into a simple library for others to use if they're interested: singlet. Re-working the original post with singlet looks like this:
Example library code:
type Cache[T any] struct{}var singleton = &singlet.Singleton{}func getOrCreateCache[T any]() (Cache[T], err) { return singlet.GetOrDo(singleton, func() Cache[T] { return Cache[T]{} })}
Client code:
stringCache := getOrCreateCache[string]()
And the Singlet library code that supports this:
var ErrTypeMismatch = errors.New("the requested type does not match the singleton type")type Singleton struct { p atomic.Pointer[any] mtx sync.Mutex}func GetOrDo[T any](singleton *Singleton, fn func() T) (result T, err error) { maybeResult := singleton.p.Load() if maybeResult == nil { // Lock to guard against applying fn twice singleton.mtx.Lock() defer singleton.mtx.Unlock() maybeResult = singleton.p.Load() // Double check if maybeResult == nil { result = fn() var resultAny any = result singleton.p.Store(&resultAny) return result, nil } } var ok bool result, ok = (*maybeResult).(T) if !ok { return *new(T), ErrTypeMismatch } return result, nil}
I hope that helps anyone else who comes across this situation.