Introducing go dependency management library

Motivation

Here we talked much about why we need a dependency management library. Here is a short summary:

  • avoiding much of boilerplate code
  • controllable, centralised, predictable initialisation logic
  • reusable resources without globals, manageable internal app state
  • lazy initialisation
  • easy controllable garbage collection
  • cycle detection

A new go dependency management container

Gotainer is my own dependency injection library which I used in many projects I work on. If you are familiar with the Symfony Dependency Injection Container, the concept will be very familiar to you.

If you like a hands on approach without much theoretical texts, we can try to create an email sender struct which needs some configuration and a password manager struct which will give passwords/tokens for authenticating to external services like SMTP Server.

Let's do it:

We create a new Go project:

We install the library:

go get -u github.com/breathbath/gotainer/container

We declare the PasswordManager service:

package passwords

type PasswordManager struct {}

//for each service we suggest to create a constructor function
func NewPasswordManager() PasswordManager {
	return PasswordManager{}
}

We declare the smtp client for sending emails. Since we want to abstract the way of getting passwords, we also declare the login and password provider interface:

package email

//we abstract the login and password provider
type SmtpLoginPasswordProvider interface {
	GetLoginData() (login, pass string, err error)
}

type SmtpClient struct {
	loginPassProvider SmtpLoginPasswordProvider
	fromEmail         string
	fromName          string
}

//constructor
func NewSmtpClient(loginPassProvider SmtpLoginPasswordProvider, fromEmail, fromName string) SmtpClient {
	return SmtpClient{
		loginPassProvider: loginPassProvider,
		fromEmail:         fromEmail,
		fromName:          fromName,
	}
}

func (smtp SmtpClient) SendEmail(receiverEmail, receiverName, subject, body string) error {
	login, pass, err := smtp.loginPassProvider.GetLoginData()
	if err != nil {
		return err
	}

	//we do here the actual logic for sending emails via smtp
}

We make PasswordManager compatible with the SmtpLoginPasswordProvider, so it can be injected as login data provider:

package passwords

type PasswordManager struct {
}

//for each service we suggest to create a constructor function
func NewPasswordManager() PasswordManager {
	return PasswordManager{}
}

func (pm PasswordManager) GetLoginData() (login, pass string, err error) {
	//of course in real life here should be a more complex logic to extract sensitive password data
	return "[email protected]", "1234567", nil
}

Now we can use PasswordManager as a SmtpLoginPasswordProvider. As the next step we put all initialisation logic into container:

package container

import (
	"github.com/breathbath/gotainer/container"
	"gotainerTesting/email"
	"gotainerTesting/passwords"
)

func GetConfig() container.Tree {
	return container.Tree{
		container.Node{
			ID:           "passwordManager",
			NewFunc:      passwords.NewPasswordManager,
		},
		container.Node{
			ID:           "smtpClient",
			NewFunc:      email.NewSmtpClient,
			ServiceNames: container.Services{
				"passwordManager", //with this we say to inject passwordManager into email.NewSmtpClient method
			},
		},
	}
}

You might have noticed that email.NewSmtpClient accepts 3 arguments: 

...
func NewSmtpClient(loginPassProvider SmtpLoginPasswordProvider, fromEmail, fromName string) SmtpClient {
...

we need also fromEmail and fromName as scalar parameters. Let's add the to the container definition and then to the SmtpClient initialisation:

package container
...
func GetConfig() container.Tree {
	return container.Tree{
		container.Node{
			Parameters: map[string]interface{}{
				"fromEmail": "[email protected]", //in a real app this should come from config or env variables
				"fromName":   "Admin",
			},
		},
		container.Node{
			ID:           "smtpClient",
			NewFunc:      email.NewSmtpClient,
			ServiceNames: container.Services{
				"passwordManager",
                                  "fromEmail",  //now we can use aliases of the scalar parameters as dependencies here
                                  "fromName",
			},
		},
	}
}

Now when everything is in place we can create the SmtpClient by the container and run it:

package main

import (
	"example/cont"
	"example/email"
	"github.com/breathbath/gotainer/container"
)

func main() {
	builder := container.RuntimeContainerBuilder{}
	runtimeContainer, err := builder.BuildContainerFromConfig(cont.GetConfig())
	if err != nil {
		panic(err)
	}

	smtpClient := runtimeContainer.Get("smtpClient", true).(email.SmtpClient)
	err = smtpClient.SendEmail("[email protected]", "Client", "Hey", "Hello world")
	if err != nil {
		panic(err)
	}
}

You can find the full code for the example here, as well as detailed description of features and possible integration.