We have done a lot on our project so far. In Part 1, we built a basic application, created a Makefile to build it, and created a Docker image for deploying it. In Part 2, we focused on configuration from environment variables and set up a reflection-based tool for filling our configuration structures with values.
This time, we will focus on how we use dependency injection to structure our application and build out a service provider for wiring up all the necessary parts.
Inversion of Control and Dependency Injection
Inversion of Control is a concept in software engineering where a component should not be responsible for creating its own dependencies. Instead, you should reverse the flow of control so that the dependencies come from outside. This principle will help you arrive at a better design since we can swap these dependencies as needed.
Dependency Injection is a way of achieving Inversion of Control by having the dependencies injected into the component from an external component or container.
Dependency Injection Containers
Let's get this out of the way: if you are coming from other programming languages such as Java or C#, you may be used to using a DI framework that handles dependencies automatically and takes over a lot of the work for you.
Go does have similar libraries, like Uber's Dig, Sarulabs DI, and Ozzo DI, which can serve this need; however, I would encourage you to at least try managing your own dependency injection instead for these reasons:
- Managing your own DI probably isn't as complicated as you may think - even if you are working on a complex project.
- A lot of DI frameworks contain a lot of magic, which makes troubleshooting errors difficult.
- In complicated scenarios, you end up doing much of the wiring yourself, and sometimes you end up fighting with the DI system to do so.
Github Repository
Part 3 source code can be found in a branch on this Github Repository
Refactoring
As we make progress in our development, it is essential to take time to refactor our code. We want to avoid our code growing stale with vestigial code that we no longer need.
First, we are going to open internal/config/readconfig.go and remove the three config structures we created last time - we are going to put these settings somewhere more appropriate:
// Delete this code
type AppConfig struct {
Message string `env:"APP_MESSAGE,default=Hello, World!"`
ApiKey string `env:"API_KEY,required"`
TimeoutSeconds int `env:"TIMEOUT_SECONDS,default=200"`
}
type DatabaseConfig struct {
DataSourceName string `env:"DB_DSN,required"`
}
type WebServerConfig struct {
Host string `env:"WEB_HOST,default=0.0.0.0"`
Port int `env:"WEB_PORT,default=8080"`
Now, let's create an internal/data directory and add database.go to that directory:
package data
type DatabaseConfig struct {
DataSourceName string `env:"DB_DSN,required"`
}
type Database struct {
config *DatabaseConfig
}
func NewDatabase(config *DatabaseConfig) *Database {
return &Database{config: config}
}
After that, create a new directory internal/web and add a file called service.go in there:
package web
import (
"github.com/ymiseddy/go-getting-started/internal/data"
)
type WebServerConfig struct {
Host string `env:"WEB_HOST,default=0.0.0.0"`
Port int `env:"WEB_PORT,default=8080"`
}
type WebServer struct {
config WebServerConfig
database *data.Database
}
func NewWebServer(config WebServerConfig, database *data.Database) *WebServer {
return &WebServer{
config: config,
database: database,
}
}
Although it would be fun, we're not actually going to implement a web service this time; we are just creating placeholders to demonstrate dependency management.
Based on these two structs, we have the following dependency graph:
![[dependency_example_01.svg]]
Both of our structs depend on their respective configuration, and the WebServer depends on the Database. Shouldn't be too hard.
The Service Provider
Now we need a way to wire up our dependencies. We will create a directory internal/ioc with a file called provider.go.
First, let's create a struct to hold all our dependencies:
type ServiceProvider struct {
databaseConfig *data.DatabaseConfig
database *data.Database
webServerConfig *web.WebServerConfig
webServer *web.WebServer
}
Pointers
Currently, we are using pointers to concrete types in our struct. Doing this is fine, but there may be cases where we use interface types also - don't be afraid to depend on interfaces. In many cases, this is better.
Eager loading
Now we want to wire up all these dependencies. A good first attempt might be to create a constructor that wires them all up at once:
func NewServiceProvider() *ServiceProvider {
databaseConfig := data.DatabaseConfig{} err := config.ReadConfigInto(&databaseConfig)
if err != nil {
panic(err)
}
database := data.NewDatabase(databaseConfig)
webServerConfig := web.WebServerConfig{}
err = config.ReadConfigInto(&webServerConfig)
if err != nil {
panic(err)
}
webServer := web.NewWebServer(webServerConfig, database)
return &ServiceProvider{
databaseConfig: databaseConfig,
database: database,
webServerConfig: webServerConfig,
webServer: webServer,
}
}
Yes, this works, but there are a few problems with this:
- Poor error handling - we don't want our constructor to panic when something goes wrong.
- Eager loading - what if we don't need the whole system? What if we have a sub-command that only requires the database? Using this constructor will wire up the web server even if we don't need it.
Lazy Loading
Instead of wiring them up ahead of time, let's leave them all null initially:
func NewServiceProvider() *ServiceProvider {
}
Now we can create a set of getter methods that return the types we need. These will have a similar format. First, they will check if the pointer in our ServiceProvider is nil; if so, we will do all the required setup and set the pointer inside ServiceProvider. If it already exists (is not nil). Here, we will build our DatabaseConfig - notice how we perform a call to config.ReadInto as part of the setup:
func (sp *ServiceProvider) GetDatabaseConfig() (*data.DatabaseConfig, error) {
if sp.database == nil {
databaseConfig := data.DatabaseConfig{}
err := config.ReadConfigInto(&databaseConfig)
if err != nil {
return nil, err
}
sp.databaseConfig = &databaseConfig
}
return sp.databaseConfig, nil
}
Now let's wire up our database - see how we chain call our GetDatabaseConfig function to get the config?
func (sp *ServiceProvider) GetDatabase() (*data.Database, error) {
if sp.database == nil {
dbConfig, err := sp.GetDatabaseConfig()
if err != nil {
return nil, err
}
sp.database = data.NewDatabase(dbConfig)
}
return sp.database, nil
}
Design Principle: Dependency Inversion Principle
We can also use our ServiceProvider to return interface implementations rather than specific concrete examples. For instance, we can support multiple database implementations in the future, such as PostgreSQL and SQLite.
If this happens, we could create a database interface (repository) that has concrete implementations depending on which database system we are using behind the scenes.
In this case, our ServiceProvider could return a Repository interface and, based on the configuration, determine which concrete instance to create and return.
GetWebServerConfig works mostly the same as GetDatabaseConfig:
func (sp *ServiceProvider) GetWebServerConfig() (*web.WebServerConfig, error) {
if sp.webServer == nil {
webServerConfig := web.WebServerConfig{}
err := config.ReadConfigInto(&webServerConfig)
if err != nil {
return nil, err
}
sp.webServerConfig = &webServerConfig
}
return sp.webServerConfig, nil
}
Finally, we can implement GetWebServer - note how we need to call GetWebServerConfig and GetDatabase to fulfill the dependencies for our WebServer:
func (sp *ServiceProvider) GetWebServer() (*web.WebServer, error) {
if sp.webServer == nil {
wsConfig, err := sp.GetWebServerConfig()
if err != nil {
return nil, err
}
database, err := sp.GetDatabase()
if err != nil {
return nil, err
}
sp.webServer = web.NewWebServer(*wsConfig, database)
}
return sp.webServer, nil
}
Now we only set up the dependencies we need when we need them. For instance, sometimes, I like to include sub-commands in my application to perform some of the maintenance tasks. When running in this mode, I can use the ServiceProvider to wire up just the components I need to complete the task.
Lifetimes
So far, the examples have been intended to be created once and live for the lifetime of the application. Most of the time, I find this is sufficient; however, there are other cases where we might run into, and we ought to have a way to handle them:
- Named instances that live the lifetime of the application
- Ephemeral instances - which we create every time we call the method and discard them as soon as the job is complete.
- Context lifetimes - sometimes we want to create components that live for the lifetime of a context. For instance, a component that lasts for the duration of a web request.
Named Instances
Let's say we have a tenant service that is different for each known tenant. We may have a limited number of tenants, so we can keep them around once we use them.
Here we have /internal/tenant/service.go:
package tenant
import "github.com/ymiseddy/go-getting-started/internal/data"
type TenantService struct {
name string
database *data.Database
}
func NewTenantService(name string, database *data.Database) *TenantService {
return &TenantService{name: name, database: database}
}
We want to get a different tenant service by name. So, tracking each TenantService can be done using a map in our ServiceProvider:
type ServiceProvider struct {
databaseConfig *data.DatabaseConfig
database *data.Database
webServerConfig *web.WebServerConfig
webServer *web.WebServer
tenantServices map[string]*tenant.TenantService
}
We also want to create our map inside the constructor:
func NewServiceProvider() *ServiceProvider {
return &ServiceProvider{
tenantServices: make(map[string]*tenant.TenantService),
}
}
Then, when we need to retrieve a TenantService, we can use a similar check and create technique, which we do with our other components:
func (sp *ServiceProvider) GetTenantServiceFor(name string) (*tenant.TenantService, error) {
service, exists := sp.tenantServices[name]
if !exists {
database, err := sp.GetDatabase()
if err != nil {
return nil, err
}
service = tenant.NewTenantService(name, database)
sp.tenantServices[name] = service
}
return service, nil
}
Ephemeral Instances
These are probably the easiest to implement, since creating ephemeral instances does not require any checks ahead of time. Here we create internal/ephemeral/service.go:
package ephemeral
import "github.com/ymiseddy/go-getting-started/internal/data"
type EphemeralService struct {
database *data.Database
}
func NewEphemeralService(database *data.Database) *EphemeralService {
{
return &EphemeralService{database: database}
}
}
We do not need to modify the ServiceProvider struct for this one; we only need to add a function call:
func (sp *ServiceProvider) GetAnEphemeralService() (*ephemeral.EphemeralService, error) {
database, err := sp.GetDatabase()
if err != nil {
return nil, err
}
service := ephemeral.NewEphemeralService(database)
return service, nil
}
Naming Conventions
For ephemeral components, I like to name them GetA* or GetAn* to communicate that the lifetime of the component is limited to the scope of the requester.
Context Instances
Occasionally, we want a component to live for the lifetime of a context. Examples include HTTP middleware functions where we populate a value in the middleware function and the request function, or other middleware needs to retrieve it.
Let's say we have a RequestInfo struct in internal/request/info.go:
package request
import "math/rand"
type RequestInfo struct {
ID int64
username string
}
func NewRequestInfo() *RequestInfo {
id := generateUniqueID()
return &RequestInfo{ID: id}
}
func generateUniqueID() int64 {
// Generate random unique ID
id := rand.Int63()
return id
}
We want to store our values by key in the context, so we can check if it exists and set it if not. In Go, it is a good practice to use a specific type for variables within your context to prevent name conflicts. We create the type and value for our request info:
type requestInfoKeyType string
const requestInfoKey requestInfoKeyType = "requestInfo"
We can now use our key to check if the info exists and set it if not:
func (sp *ServiceProvider) GetRequestInfo(ctx context.Context) (*request.RequestInfo, context.Context, error) {
reqInfo, ok := ctx.Value(requestInfoKey).(*request.RequestInfo)
if !ok {
reqInfo = &request.RequestInfo{}
ctx = context.WithValue(ctx, requestInfoKey, reqInfo)
}
return reqInfo, ctx, nil
}
Notice that we need to pass the context into the function and return a context from it. The reason is that in Go, Contexts are immutable, so we need to add the value to the context and return it as well. If the RequestInfo already exists, we can return the existing context as well.
Using the ServiceProvider as a Dependency
A component should never rely on ServiceProvider as a dependency since this can easily result in circular references.
For example, let's say our WebServer needs to get context instances of RequestInfo, so we might be tempted to do something like this:
package web
import (
"github.com/ymiseddy/go-getting-started/internal/data"
"github.com/ymiseddy/go-getting-started/internal/ioc"
)
type WebServerConfig struct {
Host string `env:"WEB_HOST,default=0.0.0.0"`
Port int `env:"WEB_PORT,default=8080"`
}
type WebServer struct {
config WebServerConfig
database *data.Database
serviceProvider ioc.ServiceProvider
}
func NewWebServer(config WebServerConfig, database *data.Database, serviceProvider ioc.ServiceProvider) *WebServer {
return &WebServer{
config: config,
database: database,
}
}
If you try this, your IDE may be yelling at you. Or, if you try to build it, you will certainly run into an import cycle not allowed error:
❯ make
go build -o build/app cmd/app/*.go
package command-line-arguments
imports github.com/ymiseddy/go-getting-started/internal/ioc from main.go
imports github.com/ymiseddy/go-getting-started/internal/web from provider.go
imports github.com/ymiseddy/go-getting-started/internal/ioc from service.go: import cycle not allowed
make: *** [Makefile:5: build] Error 1
This code fails because we added a dependency on the ioc package to our WebServer, but the ServiceProvider also needs access to the WebServer. So what are we to do? Create an interface to help us out.
In the internal/request/info.go file, we can create a RequestInfoProvider interface:
type RequestInfoProvider interface {
GetRequestInfo(ctx context.Context) (*RequestInfo, context.Context, error)
}
Now we can depend on RequestInfoProvider instead of the ServiceProvider:
package web
import (
"github.com/ymiseddy/go-getting-started/internal/data"
"github.com/ymiseddy/go-getting-started/internal/request"
)
type WebServerConfig struct {
Host string `env:"WEB_HOST,default=0.0.0.0"`
Port int `env:"WEB_PORT,default=8080"`
}
type WebServer struct {
config WebServerConfig
database *data.Database
requestInfoProvider request.RequestInfoProvider
}
func NewWebServer(config WebServerConfig, database *data.Database, requestInfoProvider request.RequestInfoProvider) *WebServer {
return &WebServer{
config: config,
database: database,
requestInfoProvider: requestInfoProvider,
}
}
Interface Segregation Principle
If you end up creating a lot of Provider type interfaces, it may be tempting to create a single large one that includes all the methods in the ServiceProvider. It is better to avoid this and instead have several individual interfaces tailored to clients. A client should not be dependent on methods it does not use.
We have solved our dependency cycle, and now we can update our GetWebServer method in the ServiceProvider; we need to pass our sp variable into the constructor:
func (sp *ServiceProvider) GetWebServer() (*web.WebServer, error) {
if sp.webServer == nil {
wsConfig, err := sp.GetWebServerConfig()
if err != nil {
return nil, err
}
database, err := sp.GetDatabase()
if err != nil {
return nil, err
}
sp.webServer = web.NewWebServer(*wsConfig, database, sp)
}
return sp.webServer, nil
}
Implicit Interface Satisfaction
If you are familiar with another programming language, such as C# or Java, you might be wondering where we implement the new RequestInfoProvider interface.
In Go, interfaces are implicitly satisfied. That means that any struct that has all the functions (with matching signatures) of the interface, implicitly implements that interface and can be substituted for that interface.
Conclusion
Dependency injection and Inversion of Control are powerful concepts that you should use to help orchestrate the components in your application. It leads to a stronger application design, which is easier to maintain in the long run.
Using a similar approach to our ServiceProvider, you can easily implement the components your application needs exactly when they are needed and no sooner. Additionally, you can manage the lifetimes of these components, whether they live for the lifetime of the application, are ephemeral, or per-context.