Today I am going to talk about Dependency Injection in Golang.
What is it?
DI (Dependency Injection) is a technique when your modules receive dependency indirectly. They don’t know about the implementation of dependency, only about the interface.
Why do we need it?
DI can help us to write low coupling code. It means that you would be able to exchange your code whenever you want, and it helps to reuse some of their parts.
Example
We have a tiny project: main.go and two services - logger and cache
├───cmd
│ └───main.go
└───services
├───logger
│ └───logger.go
└───cache
└───cache.go
How do they work? We have cache service which can be used if you want to store something in a fast storage like redis.
package cache
// interface of the logger service which will be injected
// and a good tone to write it in lowercase to hide export
type logger interface {
Error(error)
Info(string)
}
type Cache struct {
logger logger
}
// constructor of our service which recieve interfaces of
// services which will be injected (we can inject a few services) and
// return struct (instance of cache)
func NewCache(logger logger) *Cache {
return &Cache{
logger: logger,
}
}
// some methods
func (r *Cache) Get(key string) (*string, error) {
// ...
}
func (r *Cache) Set(key string, data []byte) error {
// ...
}
How may you see, Cache service know nothing about logger except the method that Cache need. We don't import anything, just describe interface with two methods, Error(error) and Info(string).
Look at the Logger:
package logger
// sentry client interface for service wich to be injected
type sentryClient interface {
// only one method for the all client (with maybe tens methods)
// because we need only one and the logger know only about one
sendMessage(interface{})
}
type Logger struct {
sentry sentryClient
}
// constructor
func NewLogger(sentryClient sentryClient)* Logger{
return &Logger{
sentry: sentryClient,
}
}
// we can see four methods, but Cache know only about two
func (l *Logger) Error(e error) {}
func (l *Logger) Info(msg string) {}
func (l *Logger) Debug(e error) {}
func (l *Logger) Log(e error) {}
So, now we have to use it all together in main.go:
package main
import logger "gopath/project/logger"
import cache "gopath/project/cache"
func main() {
// register services
sentryService := NewSomeSentryService()
loggerService := logger.NewLogger(sentryService)
authService := cache.NewCache(loggerService)
// ... init some other handlers and services
}
Let's summarize
We create two low couple services which can work with any other Loggers and SentryClients, for example we can use logger for test which write to file or stdout.
Bonus
If you want to write tests, DI also can help you! Sometimes you need mocks for tests (to be independent of some modules like logger in tests), you have to generate it! There is an amazing tool for this task in go - GoMock. When you describe only needed methods in DI interface, the GoMock generate mocks just for those methods - it is faster and more readable in practice, than hundreds methods in mocks.
Apologies
It's my first article and I'm not native English speaker. It's very hard for me to learn English, but I try to be better than I was yesterday!
Top comments (1)
This is, I dunno, the seventh or so article on DI that I've read and it's the first one that hits the sweet spot between too abstract (with examples from everyday life - I have understood the idea of DI, but needed to know how it translates to code organization) and too concrete (with example code that mixes DI with other things or confronts me with code organization in a bigger project like all the articles about wire). I finally can imagine how to use DI in my own projects.
Thank you for the article and don't worry about writing in what is not your primary language - it's not mine either and I get your point(s) very well.