Dependency Injection In Golang

Chidozie C. Okafor
5 min readJan 11, 2023

--

dozie and some go drugs

Dependency injection (DI) is a design pattern that allows a program to have loose coupling between its components. It is used to achieve inversion of control (IoC), which is a principle that states that a program should not be responsible for creating or managing its own dependencies, but instead should have them provided to it.

There are several reasons why dependency injection is useful:

  1. Maintainability: When components are loosely coupled, they are less likely to affect each other when changes are made. This makes the code more maintainable and easier to change or extend.
  2. Testability: Dependency injection allows for easier unit testing, as dependencies can be mocked or replaced with test doubles. This allows for testing individual components in isolation, which can lead to more reliable and accurate test results.
  3. Flexibility: Dependency injection makes it possible to change the implementation of a dependency at runtime, without having to modify the code that uses it. This can be useful for providing different implementations of a dependency for different environments or use cases.
  4. Reusability: Components that are decoupled from their dependencies can be reused in different contexts and with different dependencies. This can help to reduce code duplication and increase overall code reuse.
  5. Decoupling: Dependency injection allows for decoupling between different parts of the application. This makes it possible to have different teams work on different parts of the application without having to worry about the details of how the other parts work.

In Go, dependency injection can be achieved through a variety of techniques, including the use of interfaces, factory functions, and struct embedding.

One common way to implement DI in Go is through the use of interfaces. An interface defines a set of methods that a struct must implement, but does not specify how those methods should be implemented. This allows different structs to be used as implementations of the same interface, which can be passed into a function or struct as a dependency.

For example, let’s say we have an interface Logger and two structs ConsoleLogger and FileLogger which both implement the Logger interface. We can create a struct App which take Logger interface as dependency, So when we create a variable of App struct, we can pass any struct which implements Logger interface.

type Logger interface {
Log(message string)
}

type ConsoleLogger struct {}

func (c *ConsoleLogger) Log(message string) {
fmt.Println(message)
}

type FileLogger struct {}

func (f *FileLogger) Log(message string) {
file, _ := os.OpenFile("log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
defer file.Close()
file.WriteString(message)
}

type App struct {
logger Logger
}

func NewApp(logger Logger) *App {
return &App{logger: logger}
}

func main(){
cl := &ConsoleLogger{}
fl := &FileLogger{}
app1 := NewApp(cl)
app2 := NewApp(fl)
app1.logger.Log("Log message.")
//Log message.
app2.logger.Log("Log message.")
//Log message added to log.txt file
}

In the example above, the App struct depends on a Logger interface, but it doesn't specify which implementation of Logger should be used. Instead, an implementation is passed in as an argument to the NewApp function. This allows the App struct to be reused with different logging implementations, such as a ConsoleLogger and FileLogger.

Advance usage of DI in golang can be achieved through the use of container libraries such as wire and go-micro. These libraries provide a way to automatically resolve and manage dependencies, making it easier to write and maintain large applications.

wire is a code generation tool for Go that generates code for wiring up dependencies. It works by taking a Go program with structs that have wire struct tags and generating the necessary code to wire those structs together.

Here’s an example of how to use wire for dependency injection:

// main.go
package main

import (
"fmt"

"github.com/google/wire"
)

// Define the interface
type Greeter interface {
Greet() string
}

// Define the struct that implements the interface
type EnglishGreeter struct{}

func (e *EnglishGreeter) Greet() string {
return "Hello"
}

// Define the struct that uses the interface
type App struct {
Greeter Greeter
}

// Define the provider set for wire
var AppSet = wire.NewSet(NewApp, NewEnglishGreeter)

// Provider function for App
func NewApp(greeter Greeter) *App {
return &App{greeter}
}

// Provider function for Greeter
func NewEnglishGreeter() Greeter {
return &EnglishGreeter{}
}

func main() {
app, err := AppSet.Build()
if err != nil {
panic(err)
}

fmt.Println(app.Greeter.Greet())
// Output: "Hello"
}

In the example above, EnglishGreeter struct implements the Greeter interface, and App struct depends on Greeter. We defined two provider functions: NewApp and NewEnglishGreeter, that return App and EnglishGreeter structs respectively. The AppSet var is a wire set that lists the providers to be called. Then in the main function, wire builds the app by calling the provider functions, and passes the dependencies to the right places.

go-micro is a more comprehensive framework for microservices development, it is based on the go-micro library, which can be used for Dependency Injection, Configuration Management, Transport, Codec, Router, Load Balance, Circuit Breaker, etc.

package main

import (
"fmt"
"github.com/micro/go-micro"
"github.com/micro/go-micro/server"
)

type Greeter struct {}

func (g *Greeter) Hello(ctx context.Context, req *proto.Request, rsp *proto.Response) error {
rsp.Greeting = "Hello " + req.Name
return nil
}

func main() {
service := micro.NewService(
micro.Name("greeter"),
micro.WrapHandler(AuthWrapper),
)

service.Init()
proto.RegisterGreeterHandler(service.Server(), new(Greeter))

if err := service.Run(); err != nil {
log.Fatal(err)
}
}

Here, the Greeter struct implements a service that will handle requests, it's wrapped with AuthWrapper and and registered with the micro.Server using the proto.RegisterGreeterHandler function.

go-micro also provides a way to register dependencies for use throughout the service by using the micro.Server's Init function, this function takes a micro.Option that allows for passing in dependencies to the service, like so:

type DBService struct{}

func (d *DBService) GetData() string {
return "Data from DB"
}

func main() {
db := &DBService{}

service := micro.NewService(
micro.Name("greeter"),
micro.WrapHandler(AuthWrapper),
micro.Option(server.Injector(func(container server.Container) error {
return container.Provide(db)
}))
)

service.Init()
proto.RegisterGreeterHandler(service.Server(), new(Greeter))

if err := service.Run(); err != nil {
log.Fatal(err)
}
}

This way the DBService can be used by any handler within the service without having to pass it down manually.

wire and go-micro are both powerful libraries for managing dependencies and providing dependency injection in Go. wire is more focused on generating code for wiring dependencies, whereas go-micro is a more comprehensive framework for microservices development and provide additional features like configuration management, transport, codec and more. With these libraries, it is possible to improve the flexibility and maintainability of large Go applications by automating the process of resolving and wiring dependencies.

Some limitations of dependency injection in Go include verbosity and boilerplate code. Go does not have reflection, so resolving dependencies and wiring them together can be more verbose than in languages with reflection. Also, there are many different ways to achieve dependency injection in Go, and it can be difficult to choose the right one for a specific use case.

Dependency injection is not always the best solution for every problem and it does come with a cost of adding complexity and verbosity. It’s important to weigh the benefits against the costs, and consider the specific use case, to decide whether or not to use dependency injection.

--

--

Chidozie C. Okafor
Chidozie C. Okafor

Written by Chidozie C. Okafor

Software Engineer & Backend Magician 🎩 | Python, Rust | TypeScript, Node.js | Golang | Kafka & GRPC

Responses (1)