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.