Services and DI
Services are registered with the DI container through constructors. The
container resolves dependencies, topologically sorts them, validates lifetime
rules, and calls Init() on every Service it holds.
Registering
Section titled “Registering”gas.WithService[*auth.Service](auth.New, gas.ServiceLifetimeSingleton)Shorthands infer the lifetime from the name:
gas.WithSingletonService[*auth.Service](auth.New)gas.WithScopedService[*RequestLog](NewRequestLog)gas.WithTransientService[*Nonce](NewNonce)Pre-built values are registered as instances and treated as singletons.
Pre-built means pre-constructed, not pre-initialized: if the value implements
gas.Service, the container still calls Init() at startup and Close() at
shutdown, so do not call Init() yourself first.
gas.WithServiceInstance[gas.ConfigProvider](cfg)Register under the type your consumers ask for
Section titled “Register under the type your consumers ask for”The type parameter is the key the service is stored under, and lookups are by exact type. The container deliberately does not scan its registrations for something that happens to satisfy an interface, because that search is ambiguous as soon as two services implement the same one.
// migrate asks for gas.DatabaseProvider, but the database is registered// under its concrete type.gas.WithSingletonService[*database.Service](database.New())gas.WithSingletonService[*migrate.Service](migrate.New())building *migrate.Service: resolving dep gas.DatabaseProvider for *migrate.Service: no registration for gas.DatabaseProvidergas.WithSingletonService[gas.DatabaseProvider](database.New())gas.WithSingletonService[gas.MigrationManager](migrate.New())As a rule, register infrastructure under its provider interface and your own services under their concrete type:
app := gas.NewApp( // Infrastructure every module draws on. gas.WithServiceInstance[gas.ConfigProvider](cfg), gas.WithSingletonService[gas.Logger](gaslog.NewSlogLogger()),
// Modules, registered under the provider interface your services inject. gas.WithSingletonService[gas.StorageProvider](storages3.New()),)To reach a backend feature the interface does not expose, type-assert the provider you were injected rather than also registering the concrete type. Each module documents the assertion for its backend.
Lifetimes
Section titled “Lifetimes”| Lifetime | Created | Init() runs |
Notes |
|---|---|---|---|
| Singleton | Once, shared everywhere | During BuildAll() |
The default for infrastructure |
| Scoped | Once per Scope |
On first resolution in the scope | One instance per HTTP request |
| Transient | Fresh on every resolution | Never | Cannot implement Service |
Why transient services cannot implement Service
Section titled “Why transient services cannot implement Service”Because nothing would ever call Close() on them. Singletons are recorded in
the container and scoped instances in their Scope, which is what lets each be
torn down. A transient is handed to the caller and no reference is kept, by
design. Allowing a managed lifecycle there would mean either retaining every
instance, making “transient” an unbounded leak, or never closing them at all.
Service names would collide too, since the kill-switch addresses services by
Name() and every transient instance shares one.
The rule is enforced with a panic at registration, because it is decidable from the type alone.
Service shape
Section titled “Service shape”A registered type that declares Init or Close must implement all three
methods:
type Service interface { Name() string // Unique identifier, e.g. "gas/auth" Init() error // Register routes, middleware, subscriptions Close() error // Clean up internal resources}Anything short of that is rejected at startup with an error naming the missing
or mistyped methods, rather than registering cleanly and then never being
initialized. A type declaring none of them, or only Name(), is an ordinary
dependency and is unaffected.
Resolving
Section titled “Resolving”svc, err := gas.Resolve[*auth.Service](container)svc := gas.MustResolve[*auth.Service](container)When the type is not known at compile time, register and resolve against the container with a type token:
c := w.ServiceContainer()c.RegisterSingletonService(gas.TypePtr[*auth.Service](), auth.New)svc := c.MustResolve(gas.TypePtr[*auth.Service]()).(*auth.Service)The token is dereferenced once, so TypePtr[*T]() registers under *T. Both
forms share registrations and return the same instances.