Singleton
What Are Singletons?
A singleton is a design pattern that ensures an object has only one instance throughout the application’s lifecycle. It provides a global point of access to this instance, making it ideal for managing shared resources like configuration, logging, or database connections.
Eager Singletons vs. Lazy Singletons
-
Eager Singletons:
- The instance is created immediately when the application starts or when the singleton is registered.
- Pros:
- Guarantees the instance is ready to use when needed.
- Can help detect initialization issues early in the application lifecycle.
- Cons:
- Increases startup time.
- Wastes resources if the singleton is never used.
-
Lazy Singletons:
- The instance is created only when it is first accessed.
- Pros:
- Optimizes resource usage by delaying initialization until necessary.
- Reduces startup overhead.
- Cons:
- Initialization errors occur at runtime, potentially causing delays or failures during execution.
Both types are useful depending on the context—eager singletons for critical, always-needed services, and lazy singletons for optional or resource-heavy components.
Sample Service
type MyService interface {
SayHello()
}
type myServiceImpl struct{}
func (s *myServiceImpl) SayHello() {
fmt.Println("Hello from MyService!")
}
Eager Singleton
// initialize the service
myService := &myServiceImpl{}
// registering `myService` as an eager singleton
ore.RegisterSingleton[MyService](myService)
// resolving the eager singleton service `MyService`
myService, ctx := ore.Get[MyService](context.Background())
Lazy Singleton
When registering a service as lazy singleton (or scoped, transient), Ore will initialize the service later when it's needed. So Ore needs a way to construct the service. There is two ways to tell Ore how to construct a service:
- Using
Creator[T]
interface - Using anonymous functions
we will explore later on how to use both methods, but for now we'll use an anonymous function
// registering `myService` as a lazy singleton
ore.RegisterFunc[MyService](ore.Singleton, func(ctx context.Context) (MyService, context.Context) {
return &myServiceImpl{}, ctx
})
as you see, we called ore.RegisterFunc[MyService]
,
- first, we've passed the lifetime we want, which is
ore.Singleton
- second, we've declared an anonymous function which takes 1 argument
(ctx context.Context)
and it returns 2 values(MyService, context.Context)
- inside the anonymous func body, we've constructed a new instance of
myServiceImpl
which implementsMyService
, and returned it alongside thectx
.
Now, Ore knows exactly how to construct a singleton instance of MyService
when you need it:
// resolving the lazy singleton service `MyService`
myService, ctx := ore.Get[MyService](context.Background())
On the first call to ore.Get[MyService](ctx)
, Ore will make a new instance of MyService
and return it,
and it will return the same instance of every subsequent call to ore.Get[MyService](ctx)
.