LanguagesWhat is Dependency Injection in Go?

What is Dependency Injection in Go?

Go Programming ``

DI – or dependency injection – is a technique that uses some of the best programming practices to leverage decoupling amongst code. Pieces of code that have no logical correlation should exist separately from one another or be decoupled. Code must be injected to establish this dependency. This is a very rudimentary idea of what dependency injection means. This tutorial will take a deeper look into dependency injection and how it can be implemented in Go and Golang applications.

Read: Best Online Courses to Learn Go and Golang

What are SOLID Principles

To write clean and maintainable code, developers can use a set of coding practices that are known as the SOLID principles. SOLID stands for:

  • Single responsibility: In any well-designed system, objects should only have a single responsibility.
  • Open/closed: A software module should be open for extension but closed for modification.
  • Liskov substitution: If, for each object, O1 of type S, there is an object O2 of type T, such that, for all programs, P is defined in terms of T; the behavior of P is unchanged when O1 is substituted for O2; then, S is a subtype of T.
  • Interface segregation: Clients should not be forced to depend upon the interfaces that they do not use.
  • Dependency Inversion: High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.

This is particularly applied to Object-oriented programming languages. Although Go has no support for classes as such, it does have support for interfaces and structs. Go, therefore, may not support complete OOP principles, but developers can definitely say Go is an object-based programming language. Because of this, SOLID principles can be applied without much issue. And, the last letter – D of SOLID refers to dependency Inversion – which can be implemented without any problem.

What are Design Patterns in Go?

Software projects use design patterns. Design patterns are best practices for solving known software problems and have stood the test of time. Design patterns allow developers to get a predictable outcome when tackling a given problem that a design pattern is meant to solve. This results in better productivity and fewer issues, as programmers do not need to “reinvent the wheel” for known application problems.

DI is also one such design pattern, and it is, of course, not restricted to any language. Dependency injection has become a part of good software engineering principles, often adopted by seasoned programmers as a default practice. In fact, experts use this principle in almost every software project, regardless of the language they program in. Note, however, that each programming language has its own challenges in implementing the dependency injection design pattern. Before going into that, let’s try to understand DI in more detail.

Read: Web Development with Go and Golang

What is Dependency Injection?

When creating software, we write functions or create objects that are dependent on other objects. Imagine a computer, which is actually a composite of many parts or components, such as a CPU, HDD, RAM, motherboard, and so forth. These components are built by numerous third party manufacturers. If we are to build a PC, we need to assemble these components to make our computer.

For example, if a motherboard manufactured by Company A is only compatible with RAM manufactured by Company B, this means they are dependent on each other. This is called tight coupling in software engineering. Now suppose that Company A has built the motherboard in such a way that its RAM slot will work with any company’s RAM, then this is known as loose coupling.

Software engineers try hard to build software according to the principles of loose coupling. It is easier said than done, however, because as we write hard code to create objects of one type that are dependent on another object, we have already coupled the components tightly. Therefore, in order to decouple them, we must be able to create a new object abstracting in such a way that leverages independence. This is made possible by allowing someone else to provide the component. The user component remains unconcerned about the intricacies of dependent object creation. So, using our previous example, if the motherboard needs RAM, instead of calling the manufacturer, it makes an external third-party call to give the service of providing the required component. Therefore, the user component and the serving component are decoupled by the service provider.

The key players of dependency injection include:

  • The component which has the dependency
  • The dependent component
  • The service layer/provider who supplies the dependent object and takes care of the intricacies of their creation, containment, and destruction

In Java there are frameworks like Spring, which provides the container service. In Go and Golang, too, there is a framework called Wire. These frameworks make it easier to implement dependency injection.

Read: How to Use Structures in Go

How to Implement Dependency Injection in Go

To better understand the concept of dependency injection, let’s try to implement a simple example of logging, which takes a list of messages, prints each of them, and finally aggregates them together. Here is some example code demonstrating how to achieve this in Go:

package main

import (
	"bytes"
	"fmt"
)

type Logger struct{}

func (logger *Logger) Log(message string) {
	fmt.Println(message)
}

type Message struct {
	logger *Logger
}

func (m *Message) Get(msg string) string {
	m.logger.Log(msg)
	return "Message: " + msg
}

func NewMessage() *Message {
	logger := &Logger{}
	return &Message{logger}
}

type Service struct {
	logger  *Logger
	message *Message
}

func (service *Service) GetAllMessages(msgss ...string) string {
	service.logger.Log("\n::All Messages::\n")

	var result bytes.Buffer

	for _, msg := range msgss {
		result.WriteString(service.message.Get(msg))
	}

	return result.String()
}

func NewService() *Service {
	logger := &Logger{}
	message := NewMessage()

	return &Service{logger, message}
}

func main() {
	ns := NewService()

	msgss := ns.GetAllMessages(
		"message 1",
		"message 2",
	)
	fmt.Println(msgss)
}

Now the dependency injection implementation of the above code would be as follows:

package main

import (
	"bytes"
	"fmt"
)

type Logger struct{}

func (logger *Logger) Log(message string) {
	fmt.Println(message)
}

type Message struct {
	logger *Logger
}

func (m *Message) Get(msg string) string {
	m.logger.Log(msg)
	return "Message: " + msg
}

func NewMessage(logger *Logger) *Message {
	return &Message{logger}
}

type Service struct {
	logger  *Logger
	message *Message
}

func (service *Service) GetAllMessages(msgss ...string) string {
	service.logger.Log("\n::All Messages::\n")

	var result bytes.Buffer

	for _, msg := range msgss {
		result.WriteString(service.message.Get(msg))
	}

	return result.String()
}

func NewService(logger *Logger, message *Message) *Service {
	return &Service{logger, message}
}

func main() {
	logger := &Logger{}
	msg := NewMessage(logger)
	ns := NewService(logger, msg)

	msgss := ns.GetAllMessages(
		"message 1",
		"message 2",
	)
	fmt.Println(msgss)
}

The above dependency injection implementation gives us a choice of when to create a new instance of the Message object and when to reuse the instance. Also, observe that structs are now loosely coupled and no longer create their dependencies. These are all fine, but the implementation gets really complicated if the scenario is a complex one.

Look at main for instance – it becomes heavy and complicated, even though we tried to implement a simple example. Developers can, however, lessen the burden by creating a separate file called container.go and inserting the following Go function:

package main

func CreateService() *Service {
	logger := &Logger{}
	client := NewMessage(logger)
	return NewService(logger, client)
}

The main method can now be written as follows:

func main() {
	service := CreateService()
	msgss := service.GetAllMessages(
		"message 1",
		"message 2",
	)
	fmt.Println(msgss)
}

Dependency Injection in Go with Wire

The Wire framework is a dependency injection container created by Google that eases the burden of creating a container from the programmer. According to the official documentation, “Wire is a code generation tool that automates connecting components using dependency injection. Dependencies between components are represented in Wire as function parameters, encouraging explicit initialization instead of global variables. Because Wire operates without runtime state or reflection, code written to be used with Wire is useful even for hand-written initialization.”

You can refer to the following link for more details: https://github.com/google/wire.

Final Thoughts on Dependency Injection in Go and Golang

DI is a design pattern where objects or functions interact with other objects or functions in a loosely coupled manner. This is achieved by separating the concerns of object construction and using them without having to be explicitly dependent in the process. In object-oriented languages like Golang, programmers make use of the constructor by injecting all the dependencies as soon as the constructor is invoked to create an object. Although not a complete OOP language, Go, too, can implement the idea with ease, thanks to the Golang library that supports dependency injection in Go.

Read more Go and Golang programming tutorials and software development tips.

Latest Posts

Related Stories