Golang Automated Resource Locator and Injection Container
The di package provides mechanisms for declarative, scope-capable dependency injection.
Most applications using di will look something like this:
- Create a
di.Registry - Register implementations for required types
- Create a
di.RootProviderfrom thedi.Registry - Resolve the values required to run the application from the
di.RootProvider - Run the application
Many applications can benefit from using isolated sets of values for discrete units of work they perform (handling HTTP requests, processing Kafka messages, etc.). In that case the process will have a few additional steps:
- Create a
di.Registry - Register implementations for required types
- Create a
di.RootProviderfrom thedi.Registry - Resolve any values required to bootstrap the application from the
di.RootProvider - Start accepting units of work
- For each unit of work:
- Create a
di.Scopefrom thedi.RootProvider - Resolve the values required to handle the unit of work from the
di.Scope - Handle the unit of work
- Create a
A di.Registry is essentially a builder for a di.RootProvider. After creating a di.Registry you'll add registrations then build a di.RootProvider that uses those registrations to resolve and provide values.
A registration is mapping from a target type to a factory that returns instances of an implementation type that implements the target type. The factory describes how to obtain a value when the target type is requested from a di.Resolver. The registration also includes a di.Lifetime which indicates when the resolver should initialize new values and when it should reuse values it has already initialized and returned for previous requests.
A target type is the type that a registration describes how to resolve.
An implementation type is the concrete type of the value that will be resolved when a target type is requested. Implementation types MUST implement their corresponding target type and MUST be concrete types.
A di.Resolver is a value that resolves instances of various types on demand at runtime.
The di package provides two di.Resolver implementations: di.RootProvider and di.Scope.
A di.Factory is a function that creates values and initializes their dependencies using a di.Resolver.
The di package can provide default factories for many types, in addition to supporting custom di.Factory implementations.
The di package is able to create and initialize many types without requiring users to provide an explicit factory.
The default factory for any struct type starts with the zero value for the type, then initializes all of the exported members using the di.Resolver. NOTE that the exported members are initialized with whichever factory the di.Resolver has registered for its type which is not necessarily a default factory.
The default factory for bool, numeric, array, and string types provide the zero value. This includes any type whose reflect.Kind is reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.Array, or reflect.String.
The default factory for channels provides an unbuffered channel.
The default factory for maps is equivalent to make(map[T]U). This ensures that the returned value can be written to.
The default factory for slices returns nil. Unlike with maps, a nil slice can be appended to. A zero-length slice can only be written to with an append so semantically there's no difference between nil and an empty slice.
Default factories are unavailable for types whose direct reflect.Kind is [reflect.Uintptr], [reflect.Func], or [reflect.UnsafePointer].
Default factories are unavailable for types whose direct reflect.Kind is [reflect.Interface], but this is irrelevant because interfaces cannot be implementation types.
For types whose reflect.Kind is [reflect.Pointer], a default factory is available if and only if there is a default factory for the pointed-to type. For example there is a default factory for *struct{ X int; Y int } because there is a default factory for { X int; Y int }, and there is no default facotory for *func() because here is no default factory for func(). This applies recursively; i.e. there is a default factory for **struct{ X int; Y int } because there is a default factory for *{ X int; Y int }, and there is no default facotory for **func() because here is no default factory for *func(). Default factories for pointers initialize the pointed-to value using the corresponding type's default factory, and a pointer to that value. Again, this is recursive; i.e. the default factory for **struct{ X int; Y int} initializes a *struct{ X int; Y int } and a non-nil pointer to it. The *struct{ X int; Y int } is also a pointer type so its default factory initializes a struct{ X int; Y int} and a non-nil pointer to it.
A di.Lifetime describes when a di.Resolver should initialize a new instance of a value to return and when it should reuse a value it has already returned.
The di.Transient lifetime specifies that a new value should be initialized every time a type is resolved and can be used with any type.
The di.Scoped lifetime specifies that a single instance of the registered type should be reused every time the type is resolved from the same di.Scope, and the di.Singleton lifetime specifies that a single instance should be reused every time the type is resolved from the same di.RootProvider or any di.Scope created from it. In order to support reusing the same instance the di.Scoped and di.Singleton lifetimes can only be used with sharable types.
It's not actually possible in Go to return the same instance of a value more than once; we can only return a copy. However, for types' whose values are references to the data we're interested a copy will generally point to the same data. In this case we can return distinct values that each reference the data we want to share. We refer to these as "sharable types". NOTE that since it is the value rather than the identifier that refers to the shared value, assigning a new value to a field that currently holds reference to a shared value will not update the shared value.
All pointer types are sharable because their value is just a reference to an instance of their element type. NOTE that pointer types don't provide any mechanisms to make concurrent use safe, that's up to the type being pointed to.
Channels are not technically pointers, but copies of channels are readers and writers of the same stream of data and are safe for concurrent use so channels are considerable sharable.
Arrays are not sharable because the value of the array includes all of its element values. A copy of an array is a copy of each elemen.t After a copy, mutations to one array are not reflected in the other.
Slices hold their element data in underlying arrays by reference, but the portion of the underlying array that the slice exposes and the underlying array itself can change when the slice is modified. As a result, copies of slices are not guaranteed to stay in sync as they are used and are therefore not considered sharable.
Maps are currently not considered sharable. Although maps hold heir data in an underlying structure and copies of maps are consistently observed to reflect writes across instances, the Golang spec does not seem to guarantee that a map write won't result in the written-to map allocating new underlying storage and diverging from the instances with which is was previously consistent (the same way slices can).
A di.RootProvider is a di.Resolver that provides values with di.Transient and di.Singleton lifetimes. In simple applications the di.RootProvider may be used to initialize everything, but for applications requiring scoped values the di.RootProvider will typically be used to initialize the request handling infrastructure, then to initialize a distinct di.Scope for each unit of work.
A di.Scope is a di.Resolver that provides values with di.Transient, di.Scoped, and di.Singleton lifetimes. The intention of a di.Scope is to facilitate initializing values that are shared during the processing of a single unit of work, but not shared across units of work.
- An application with users and permissions may use a
di.Scopedfactory to initialize values with authz context information embedded to facilitate automated enforcement RBAC/ABAC. - An application participating in distributed tracing may use a
di.Scopedtrace context propagator in combination with otherdi.Scopedanddi.Singletonvalues to transparently forward tracing context through to outbound requests. - An application using loggers may use a
di.Scopedlogger factory to initialize logger instances that automatically contain metadata about the scope such as trace info, HTTP request method/path, authz context, etc.
To help support deterministic lifetimes for [di.Scoped] lifetime values the [di.Scope] type has a Close function that will call Close on any values implementing the di.ContextCloser or di.Closer interfaces.