Goij is a recursive dependency injector. Use Goij to bootstrap and wire together S.O.L.I.D, clean Go applications.
IMPORTANT NOTE: This was a experiment that I created for fun. Please don't use it in production, it's not stable. The idea itself is nice, and if you decide you are willing to accept the tradeoffs of runtime resolving vs compile-time then this is of course your prerogative. This will be left here for others to see some crazy ideas I had. You could look at google/wire if you want to generate factories, otherwise just wire everything up yourself until we have generics in Go. :-)
type X struct { Dependency Y }
type Y struct { Dependency Z }
type Z struct { }
x := injector.Make("X").(*X)
z := x.Dependency.Dependency // z is == Z{}.
Some of the features of the injector are as follows:
- Recursive initialisation of all public fields on structs
- Create structs from strings
- Binding of interfaces to a specific implementation
- Shared objects to inject copies of the same instance around the application
- Delegation of struct initialisation to factories
- A very simple API
Among other things, Goij instantiates public struct dependencies based on their types at runtime. This requires the use of reflection and a type registry, which can be auto-generated. This allows structs to be initialised from strings.
You can see how I'm using the injector in a real live microservice using GRPC, CQRS, some DDD and other goodies here.
Make()
initialises objects, and their public fields, recursively. If any of these objects have a factory, have been
Share()
d previously, or have a user-provided Delegate()
(factory) with the New
convention, these are invoked to
return the relevant field.
The objectName
parameter can be either the fully qualified name of the struct, which must exist in the TypeRegistry
,
or it can be the short name. For example: my/app/Logger.Logger
or Logger.Logger
. Short names only work when there
is only one Logger.Logger
in the registry. It is advised to use the fully qualified name to avoid issues.
Read more in Basic Recursive Instantiation.
Bind()
tells the injector that, upon encountering an interface for one of the fields during recursive initialisation,
to inject the given specific struct type implementing that interface.
This also works when Delegate()
arguments are encountered of the interface kind.
Note that if there is exactly one implementing interface in the type registry then you do not need to Bind()
that
single specific implementation as it will be automatically provisioned and injected for you.
Read more in Interface Binding.
Share()
tells the injector that, upon encountering a type that matches the type of the Share()
d object during
recursive initialisation, to inject this specific already initialised object instead of provisioning a new one. Any
properties of this injected Share()
d instance are not recursively provisioned.
Delegate()
tells the injector that, upon encountering the given struct during recursive initialisation, to invoke
the given factory function that returns this object. The factory function can also be a lambda.
Note that if there is exactly one factory in the type registry then you do not need to Delegate()
that single specific
factory as it will be automatically invoked and the result injected for you.
Read more in Initialisation Delegates.
There's a lot more that the injector can do for us, so let's move onto the guide.
- Creator Note
- Requirements and Installation
- Initialisation
- Basic Recursive Instantiation
- Interface Binding
- Injection Definitions
- Instance Sharing
- Initialisation Delegates
- Injecting third-party Dependencies
- Example Use-Cases
- FAQ
Contributors are welcome! There are many things to be improved in Goij before it is released out of alpha. Here are some of the potential improvements. Your input, thoughts and PRs are very welcome, so open an issue and let's talk. I'd also appreciate the help in getting this to something much more usable.
- Better visualisation and logging for the object initialisation path, more standardised logging message to help users debug their problems much faster than currently - maybe even a UI for this as debugging is a nightmare right now
- Documentation in the form of a diagram on the logic and ordering of injection, depending on delegates etc
- Make logger optional through variadic
InjectionConfiguration
arguments to simplifyNew()
- Add more logging in all the places it is necessary (factories, for example)
- Document logger and
InjectionConfiguration
options - Change from panics to errors? A discussion on this is needed
- Do not inject a copy on encountering a pointer; allow the user to utilise the same address, blame them when things go wrong
- Goij requires you to be using go modules
go get github.com/j7mbo/goij
Because Go doesn't actually allow you to create an object from a string due to the lack of a global registry, you need a central registry of types. You can either build one yourself, or you can generate one automatically with the gen binary included with the injector.
The location of this binary will depend on how you get it. If you do a go get
, you'll have to find it in your go
directory and execute it there.
../path_to_goij/bin/gen -o Registry.go -dir ./src/ -exclude oneDir -exclude twoDir
This generates an output file (-o
) containing a registry of all public structs, interfaces and factory functions
within the given directory (-dir
). You can then feed these maps to the injector for later initialisation.
You can also compile this yourself. You can google the correct arguments for your arch. The included one is built on MacOS.
CGO_ENABLED=0 go build -a -ldflags '-extldflags "-static"' -o ../path_to_goij/bin/gen ./gen
To start using the injector, simply create a type registry, and pass it into the Injector
factory method.
registry := GetRegistry() // After running ../path_to_vendor/bin/gen
injector := Goij.NewInjector(TypeRegistry.New(registry), nil)
If a struct only asks for struct dependencies, you can use the injector to initialise and inject them without specifying any injection definitions.
type Object struct{
Dep DepOne
}
type DepOne struct{}
object := injector.Make("Object").(*Object) // Compiles and runs successfully.
This is fully recursive, so any dependencies of DepOne
would also be initialised and injected, and so on and so forth.
You may have noticed that the previous example demonstrated initialisation of structs with struct dependencies. Obviously, many of your objects won't fit this mould. Some structs will depend on interfaces.
In the case that there is exactly one struct implementing an interface then that one implementing struct will be provisioned and injected automatically.
type Object struct{
InterfaceDependency AnInterface
}
type AnInterface interface {
AMethod()
}
type DepOne struct{}
func (*DepOne) AMethod() {}
dep := injector.Make("Object").(*Object).InterfaceDependency // Instance of DepOne.
You can also create an interface directly using the same logic:
dep := injector.Make("AnInterface") // Instance of DepOne.
In the case that there are more than one structs implementing an interface in the type registry, we will need to assist the injector by telling it exactly which struct to inject when the interface is encountered during recursive initialisation.
type Object struct{
InterfaceDependency AnInterface
}
type AnInterface interface {
AMethod()
}
type DepOne struct{}
func (*DepOne) AMethod() {}
type DepTwo struct{}
func (*DepTwo) AMethod() {}
injector.Bind("AnInterface", "DepTwo")
injector.Make("Object").(*Object).InterfaceDependency // Instance of DepTwo.
In the case that you want a specific instance of an object injected in a type when the injector encounters it, you can
do this with Define()
:
type Dependency struct {
Int int
}
type Object struct {
Dep Dependency
}
injector.Define("Object", "Dep", Dependency{Int: 42})
injector.Make("Object").(*Object).Dependency.Int // 42.
Note that the above overrides Share()
, so Object
will always get a Dependency
with Int
being 42, but all the
rest of the objects that depend on Dependency
will have either the Share()
d object injected, or else an empty object
with Int
being set to it's zero value.
Some of your structs will specify primitive types such as strings and integers. In such cases we need to assist the Injector by telling it exactly what we want to inject.
type Object struct {
HostName string
Port int
}
injector.Define("Object", "HostName", "http://www.github.com")
injector.Define("Object", "Port", 80)
injector.Make("Object").(*Object).Port // 80.
If you know that you want to always inject a value for a given field name, you can do do this as follows:
type Object struct {
AFieldHere string
}
injector.DefineGlobal("AFieldHere", "Hello World")
injector.Make("Object").(*Object).AFieldHere // Hello World.
Note: Globally defined definitions should be used with care as the matching is only done on parameter name.
One of the problems plaguing software architecture in Go is utilising global state to pass around objects. In fact, Go's context package was built for the purpose of sharing cancellable channels around the application simply because they are not being dependency injected. Goij makes this problem a triviality with the ability to inject the same value around the application.
type Database struct {
Config DBConfiguration
}
injector.Share(DBConfiguration{ Hostname: "http://www.github.com", Port: 80 })
injector.Make("Database").(*Database).Config.Hostname // http://www.github.com.
Instance sharing enables the single-time initialisation of, for example, a Configuration
object, a Logger
, a
Database
connection or even the sharing of the Context
from the context package in your composition root without
injecting factories where they are not needed and without duplicating initialisation code everywhere.
Often the factory method pattern is used to initialise an object. Goij allows you to add factories into the injection process by specifying initialisation delegates on a per-class basis. In the case that there is single factory in the type registry for the requested type, the factory function will automatically be used to initialise the object.
Let's look at a very basic example to demonstrate the concept of initialisation delegates:
type MyComplexService {
SomeValue int
}
func SomehowBuildComplexService() *MyComplexService {
val := calculateSomeValue() // Do some stuff, get 1337.
return &MyComplexService{SomeValue: val}
}
type Controller {
Service MyComplexService
}
injector.Delegate("MyComplexService", SomehowBuildComplexService)
injector.Make("Controller").(*Controller).Service.SomeValue // 1337.
If there are multiple factories in the type registry, or you want to explicitly tell the inject to use a factory not in the type registry, you can with a a first class function type as shown above, or with a lambda:
injector.Delegate("MyComplexService", func() *MyComplexService {
val := calculateSomeValue() // Do some stuff, get 1337.
return &MyComplexService{SomeValue: val}
})
Initialisation delegates can also require parameters to fulfil their objectives as a factory. If your factory method asks for dependencies, they will be provisioned and injected into the function upon invocation.
initialisedService := MyComplexService{}
injector.Share(initialisedService)
injector.Delegate("AnObject", func(s *MyComplexService) *AnObject {
value := s.CalculateSomeValue()
return &AnObject(val: Value)
})
There are also cases where a package only provides two things for the user: an interface and a factory that returns that interface. Some consider this a best practice whilst others cry foul that it is not idiomatic. Regardless, the injector can work with interface delegates also:
type AnObject interface {}
injector.Delegate("AnObject", func() *AnObject {
return AnObject.New()
})
Note: Delegate dependency resolution works with structs and interfaces (resolved to the correct struct), but not with scalar definitions (global or otherwise) because Go does not allow retrieving function argument names.
To be able to inject third-party dependencies with the injector, they also need to be in the registry. You can generate
a separate "vendor registry" and pass it to TypeRegistry.New()
. Simply change the function name in the generated
registry file if you generate it into the same directory as the other registry.
You may find it useful to first run go mod vendor
to have a directory of dependencies just for your project.
Generate a vendor registry from the vendor directory:
vendor/github.com/j7mbo/goij/bin/gen -o ./VendorRegistry.go -dir ./vendor/
Then rename the generated function to GetVendorRegistry()
, and use it in Go like this:
Goij.NewInjector(TypeRegistry.New(GetRegistry(), GetVendorRegistry()))
The injector will then be able to utilise your own registry and the vendor registry as well.
The injector provides a handy method to call a method dynamically:
injector.Invoke(TheObject{}, "methodName", arg1, arg2, etc)
Goij resolves dependencies in the following order:
- Factory / delegate, with arguments recursively initialised following the same logic
- Cached / shared object, no recursive initialisation here
- Recursive initialisation on object
- Defined scalars are injected, else the encounted scalar will be zero'd
Go is a statically typed language, and there is no central registry of types available to the user. As a result, types
cannot be initialised from a string such as var := "MyStruct"; obj := new(var)
. The type literal must exist in the
code for the Go compiler to compile and use it. As a result, a type registry is needed.
Goij includes a utility to generate a registry. It utilises the AST (abstract syntax tree) to search for all public structs, interfaces and factory methods (more on the logic behind this below), and writes them to a file with their fully qualified package name.
The reason fully qualified package names such as github.com/j7mbo/goij/Injector.Injector
are used is because simply
using injector.Make("Injector")
could not work if there are two types in the type registry with the same name,
ignoring the fact that two identical keys cannot exist in a map[string]interface{}
.
The gen binary also tries to guess the package name for the generated file, but this is not always correct especially when generating into the project's root directory. Be aware you may need to rename the package before using the file.
Note: Whilst using short names is currently enabled for the public api, it is safer to rely on fully qualified package names when using Goij.
The gen command has the following options:
Usage:
gen [arguments]
The arguments are:
o The output file such as "Registry.go" or "/path/to/dir/FileRegistry"
dir The directory to scan for structs, interfaces, factories etc
exclude A directory to exclude from searching (useful for vendor/ etc), can use multiple times in command
reset Resets the registry back to the default empty template if used with -o
All paths can be relative or absolute.
The logic for which types are added to the registry are as follows:
- All exported struct types are added
- All exported interface types that have at least one implementing exported struct are added
- All exported functions beginning with
New
(idiomatic convention) with a return type are added as factories - These are written to a file containing the function:
func GetRegistry() Registry
, which you can feed to the injector on initialisation.
The types returned from registry generation are also available for the end user. Sometimes it is easier or preferable to build your own registry.
The registry contains arrays of TypeRegistry.RegistryStruct
, TypeRegistry.RegistryInterface
and
TypeRegistry.RegistryFactory
, typically looking like this:
registry := TypeRegistry.Registry{
RegistryStructs: []TypeRegistry.RegistryStruct{
TypeRegistry.RegistryStruct{ Name: "github.com/j7mbo/goij/src/TypeRegistry.Registry", Implementation: TypeRegistry.Registry{}},
},
RegistryInterfaces: []TypeRegistry.RegistryInterface{
TypeRegistry.RegistryInterface{ Name: "github.com/j7mbo/Goij.Injector", Implementation: (*Injector)(nil)},
},
RegistryFactories: []TypeRegistry.RegistryFactory{
TypeRegistry.RegistryFactory{ Name: "github.com/j7mbo/goij/src/TypeRegistry.AutoRegistryGenerator", Implementations: []interface{}{ TypeRegistry.NewAutoRegistryGenerator }})
},
}
The registry
variable is then ready to be passed to the injector and is used as a lookup for all string-related
operations during recursive initialisation.
Not only can you pass multiple registries to the injector as the TypeRegistry.New
function signature accepts variadic
Registry
objects, but sometimes it may be preferable to do this to maintain separation between a registry of the types
in your own application and a registry of the types from a third party library for quick removal.
Note: It is important that you follow the conventions of the registry above. You MUST NOT add pointers to the struct registry, interfaces must be nil interfaces to retrieve the type at runtime, factories MUST follow similar conventions, and names in the registry MUST be the fully qualified package path followed by the type name.
Injectors should be used to wire together the disparate objects of your application into a cohesive functional unit,
generally at the bootstrap or front-controller stage of the application, also known as the 'composition root'. In Java
and go this would be main()
, in PHP it would be index.php
, in swift it would be the AppDelegate
.
Note: Goij is NOT a service locator. DO NOT turn it into one. Service locator is an anti-pattern for rare and very extreme edge-cases; it hides class dependencies, makes code more difficult to maintain, reason with and test, and makes a liar of your object API! The only places that an injector should be used are the composition root or factories.
One such usage provides an elegant solution for one of the thorny problems in web applications: how to initialise and utilise a route through the application dynamically where the needed dependencies are not known at compile-time and depend on the route hit by the user.
Here is an incomplete example of what can be achieved with the injector.
routes:
route: /
controller: IndexController
action: Invoke
package main
func main() {
injector := Goij.NewInjector(TypeRegistry.New(GetRegistry), nil)
router := mux.NewRouter()
routes := RouteLoader.Load("routes.yml")
for _, routeData := range routes {
router.HandleFunc(routes.route, func(w http.ResponseWriter, r *http.Request) {
injector.Invoke(injector.Make(routes.controller), routes.action, w, r)
})
}
}
New routes can be added and used immediately which can help in rapid application development and this is just the start.
In distributed systems, a correlation id is necessary to track messages between services and also throughout an application. One practice is to pass the correlation id throughout the application, including into factories, and into many places where the id is not even needed and is only required to be passed through to a sub-dependency.
With Goij, a dependency somewhere down the object hierarchy can simply add a public correlation id property and the shared id will be injected and ready to use, removing the need for ugly and unecessary APIs.
It's entirely possible the Go application will be the first point where a correlation id is created, but imagine the correlation id is retrieved from the request each time instead.
package main
func main() {
injector := Goij.NewInjector(TypeRegistry.New(GetRegistry), nil)
injector.Delegate("CorrelationId", func(r *http.Request) {
return r.Header.Get("correlation-id") // Eg: 1337.
})
handleRoutes(injector)
}
func handleRoutes(i Injector) {
router := mux.NewRouter()
routes := RouteLoader.Load("routes.yml")
for _, routeData := range routes {
router.HandleFunc(routes.route, func(w http.ResponseWriter, r *http.Request) {
i.Share(r) // Important!
i.Invoke(i.Make(routes.controller), routes.action, w, r)
})
}
}
type IndexController struct {
MyService *Service
}
func (c *IndexController) Invoke() {
c.MyService.DoSomething()
}
type MyService struct {
CorrId CorrelationId
Logger Logger
}
func (m *MyService) DoSomething() {
m.Logger.Log("Something happened!", {correlationId: m.CorrID})
}
This is a contrived example. What you could do is inject the correlation id directly into the logger so that wherever anything is logged there is always a correlation id available. However this example does show the ability to delegate the creation of a correlation id to a factory retrieving the correlation id from a request header, and then using this id anywhere it is required throughout the application.
Utilising the above example of dynamic routing, you can require model objects from the model layer by adding them as public properties of your controllers, and require database access or more by adding them as public properties of your model objects.
package main
func main() {
db := NewDb(
os.Getenv("hostname"),
os.GetPort("port"),
/* etc etc... */
)
ij = Goij.NewInjector(TypeRegistry.New(GetRegistry()), nil)
ij.Share(db)
/* -- SNIP -- Perform routing and pass in request to Controller. -- SNIP -- */
ij.Invoke(ij.Make("IndexController"), "Invoke", request, responseWriter)
}
/* Controller. */
type IndexController {
Users Users
}
func (c *IndexController) Invoke(request http.Request, responseWriter http.ResponseWriter) {
/* Naively assumes the user id is in a GET parameter for simplicity of example... */
userId := request.URL.Query().Get("user_id")
user := c.Users.FindById(userId)
fmt.Fprintf(w, "Hello, your username is: %s", user.GetUsername())
}
/* User entity. */
type User struct {
id string
username string
}
func (u *User) GetUsername() string {
return u.username
}
func (u *User) NewFromResultSet(resultSet []string) *User {
return &User{id: resultSet[0]["id"], username: resultSet[0]["id"]}
}
/* User repository interface for the domain. */
type Users interface {
FindById(id int) User
}
/* User repository implementation for the infrastructure layer. */
type UserRepository {
/* Configured and share()d in the composition root (main). */
DB Database
}
func (u *UserRepository) FindById(id int) *User {
/* Obviously escape the id. */
resultSet := u.DB.Query("SELECT * FROM users WHERE id = " + id)
return User.NewFromResultSet(resultSet)
}
As soon as the injector creates the IndexController
, it also injects Users
, with the concrete implementation being
the UserRepository
, which already has the Database
provisioned and injected in.
In this example, assuming you had also shared a Logger
in the composition root, you could add a Logger
public
property in the controller, in the model, in the repository... anywhere, and immediately have it provisioned and ready
for use.
- Provide an optional
InjectionConfiguration
to allow users to customise the injector:- Inject all public properties
- Inject all properties with a given struct tag
- Inject all private properties with a given struct tag
- Only inject via factories ("constructor style")
What is the current status of the project?
Absolute massive here be dragons. Goij is in active development and is ALPHA. This means you can expect the API to change before hitting v1.0.0 and, although it is being used on production in an enterprise environment, it is not currently considered stable. There are plenty of bugs/
I got an error, why is the injector not working?!
Double check you are doing exactly what the documentation specifies before opening a new issue. Perhaps the functionality you are looking for has not been implemented yet. The injector is currently in alpha mostly because it is following 'the happy path' and doesn't provide particularly nice errors for things that could go wrong.
What does the injector return, a pointer or a struct?
By default the injector returns pointers unless registered factories or delegates return otherwise. Either way, the return values are copies of the originals, so that any changes are not propagated back to the type registry or cache and therefore also not to any other places that they have been injected.
What happens with pointers?
Goij injects copies of the type in the registry or the cache. If a dependency is of the pointer kind, Goij will inject a pointer copy. Goij does not currently allow multiple pointers to be pointing to the same address in the registry or cache as this would greatly increase complexity due to shared application state, race conditions, mutex usage etc.
In the future this might change. For example, the idea of duplicating a database connection isn't really necessary and it would be better to actually inject the single instance of it, although this would be a pointer so could cause problems if used over multiple threads. I'm open to discussing this in an issue.
Is Goij thread safe?
Goij currently makes no promises on thread safety - you would possibly need to guard for this in your application.
Isn't copying objects everywhere expensive?
It is arguable that, whilst pointers can avoid copying memory, there are tradeoffs such as additional indirection and increased work for the garbage collector. Computers are very fast at copying memory so do not just use pointers because you think they might give you better performance. Default to using values except when you need the semantics a pointer provides.
Why does the type registry not use lambdas?
Potentially initialising empty structs for every type in the application on GetRegistry()
can be costly. However if
you look at the memory consumption of an individual struct with several properties, the memory allocation is actually
very small.
The problem with utilising lambdas would be that every one of these would need to be executed (they would return
interface{}
) and then reflected on to get the return type when searching the type registry, and this could be costly.
Given a struct containing the following properties: 3 struct pointers, 2 strings, 2 bools and an int32, the size in memory in bytes is ~40 bytes. See here. Assuming a type registry containing 1000 structs contains the aforementioned properties, the total size in memory would be ~40kb. The memory overhead is more acceptable than the additional computation required with lambdas.
Reflection is slow
You may have heard that "reflection is slow". Let's clear something up: anything can be "slow" if you're doing it wrong. Reflection is an order of magnitude faster than disk access and several orders of magnitude faster than retrieving information (for example) from a remote database. Go, as a language, is extremely fast in it's own right. Goij caches some of the structs it encounters to minimize the potential performance impact. After the initial caching, injection is as fast as a map lookup.
Go was not designed for this
Go provides not only a powerful language but also a toolset enabling developers to build anything they find useful.
I don't like magic
That depends on how you define magic. Magic at a distance is bad. A well-tested isolated box can be magic to some, or useful to others.
Dependency Injection Container XYZ already exists
Goij is not a dependency injection container, and it has a very simple API compared to many other libraries.
I found a bug or a better way of implementing something
Please read CONTRIBUTING.md and feel free to open a new Github issue where we can discuss it.
How can I recompile the gen command?
This depends on your environment, so check out the docs and look for GOOS
and GOARCH
flags. Here's what Goij used:
CGO_ENABLED=0 go build -a -ldflags '-extldflags "-static"' -o bin/gen ./cmd/gen/