thatarif
Go back

How I manage Dependency Injection in Go Apps

Dec 1, 2024

When we first hear about Dependency Injection (DI), it might sound complicated. But once we understand it, we’ll see how simple it is. Let me explain it in a way that makes sense.

There are two key concepts we often discuss in this context: Dependency Injection and Inversion of Control. Both are different things but are often used together.

Dependency Injection

It is a design pattern used in object-oriented programming where a class receives its dependencies from an external source rather than creating them internally.

Instead of hardcoding dependencies, a class defines what it needs. let’s take a look at an example in everybody’s favorite language java

// Class representing a simple dependency
class Printer {
    public void print(String message) {
        System.out.println(message);
    }
}

// Class that depends on the Printer class
class Document {
    private Printer printer;

    // ✅ Constructor Injection
    public Document(Printer printer) {
        this.printer = printer;
    }

	 // ❌ Instead of this
    public Document() {
        this.printer = new Printer();
    }

    public void printDocument(String content) {
        printer.print(content);
    }
}

// Main class to demonstrate Dependency Injection
public class DependencyInjectionExample {
    public static void main(String[] args) {
        // Creating the dependency instance
        Printer printer = new Printer();

        // Injecting the dependency into the Document class
        Document document = new Document(printer);

        // Using the injected dependency to print the document
        document.printDocument("Hello, this is a document.");
    }
}

Explanation:

  • Printer: A simple class that provides a method to print messages.
  • Document: This class depends on the Printer class. It receives an instance of Printer through its constructor, demonstrating Dependency Injection.
  • DependencyInjectionExample: This is the main class where we create an instance of Printer, inject it into Document, and call the method to print a document.

In the above code, there is no loose coupling involved, which is the primary goal of dependency injection. To achieve that, let’s first understand Inversion of Control (IOC).

Inversion of Control

IOC simply means you give control to the user, instead of acting on it on your own. In technical terms your code should not depend on implementations rather on abstractions, in that way it’s flexible. Let’s understand this by an example.

// Service interface
public interface MessageService {
    void sendMessage(String message, String recipient);
}

// Implementation of the MessageService
public class EmailService implements MessageService {
    @Override
    public void sendMessage(String message, String recipient) {
        System.out.println("Email sent to " + recipient + " with message: " + message);
    }
}

// Client class that uses Dependency Injection
public class User {
    private MessageService messageService;

    // Constructor Injection
    public User(MessageService messageService) {
        this.messageService = messageService;
    }

    public void sendMessage(String message, String recipient) {
        messageService.sendMessage(message, recipient);
    }
}

// Main class to demonstrate Dependency Injection
public class DependencyInjectionWithIOC {
    public static void main(String[] args) {
        // Creating the service instance
        MessageService emailService = new EmailService();

        // Injecting the service into the User class
        User user = new User(emailService);

        // Sending a message
        user.sendMessage("Hello, Dependency Injection!", "john.doe@example.com");
    }
}

Explanation:

  • MessageService: An interface that defines the contract for sending messages.
  • EmailService: A concrete implementation of the MessageService interface that sends messages via email.
  • User: A client class that depends on MessageService(on abstraction not on implementation). The dependency is injected through the constructor.
  • DependencyInjectionWithIOC: The main class where we create an instance of EmailService, inject it into the User class, and call the method to send a message.

Dependency Injection is a set of software design principles and patterns that enables you to develop loosely coupled code. The ultimate purpose of using DI is to create maintainable software within the object-oriented paradigm.

There is a common knowledge in design patterns.

Program to an interface, not an implementation.

Loose coupling keeps your code flexible, and that flexibility makes it easier to maintain. Dependency Injection is just a way to achieve loose coupling.

Benefits of Dependency Injection with IOC

  • Enables late bindings (like an app that supports both SQL server and Postgres implementation)
  • Ease in Unit Testing

Now that you have basic understanding about Dependency Injection and IOC, let’s see how DI is being used in various industry standard frameworks like ASP .Net Core and Spring.

Dependency Injection Containers

Spring and ASP.NET Core use Dependency Injection containers to automatically manage dependencies instead of relying on manual injection. This allows developers to focus on their main tasks without worrying about connecting different parts of the code.

Benefits of Using DI Containers:

  1. Easier Maintenance: You can change dependencies without modifying the classes that use them.
  2. Better Testing: You can easily replace real objects with mock objects during testing.
  3. Less Code: It reduces repetitive code, making development faster and simpler.

Opinion on DI Containers

In many cases developers want to control the code they write often not rely on complex mechanisms of frameworks. that’s why we are going to learn how to do manual dependency injection in Golang HTTP apps and I’m sure you will realize in most cases we don’t need mechanism like DI containers.

For your information you can also do automatic wiring of dependency in go apps, using popular libraries like github.com/google/wire and github.com/uber-go/fx .

Manual Dependency Injection in Golang

I like the simplicity go provides, less keywords, less clutter. I wrote a simple web crud API to explain this, the github repository you can find here.

In my demo app, there is a products handler which constitutes all the route handlers related to products. It depends on a logging interface (a simple interface for logging which has implementation - with inbuilt log and other using github.com/sirupsen/logrus package), and product service interface.

type ProductHandler struct {
	logger         logger.Logger
	productService services.ProductService
}

func NewProductHandler(l logger.Logger, service services.ProductService) *ProductHandler {
	return &ProductHandler{
		logger:         l,
		productService: service,
	}
}

In above code, my NewProductHandler depends on logging.Logger and services.ProductService interface, we can pass any implementation of NewProductHandler which implements both interfaces.

type Logger interface {
	Info(msg string)
	Error(msg string)
}

type ProductService interface {
	List() ([]models.Product, error)
	Get(id int) (models.Product, error)
	Create(models.Product) error
}

In my main.go file, you can see how the dependency is being passed

func main() {

	dbConn := db.Connect("go_di_example.sqlite")
	db.CreateTable(dbConn)

	r := chi.NewRouter()

	// creating logrus implementation
	var logrusLog logger.Logger = &logger.LogrusLogger{}

	productStore := stores.NewProductStore(logrusLog, dbConn)

	// first productService object is initialised
	productService := services.NewProductService(logrusLog, productStore)

	// then productService is passed on to NewProductHandler
	productHandler := handlers.NewProductHandler(logrusLog, productService)

	r.Mount("/products", productHandler.HandlerRoutes())

	log.Println("======== Starting server on :8080 =======")
	log.Fatal(http.ListenAndServe(":8080", r))
}

Also let’s take a look, how the product handler is using the dependency in one of its handler method

func (h *ProductHandler) list(w http.ResponseWriter, r *http.Request) {
	// logrus logging is used here as we have passed logrus implemention from main.go
	h.logger.Info("Listing products")

	// List() method is called here from the productService dependency
	products, err := h.productService.List()
	if err != nil {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")
		w.WriteHeader(http.StatusInternalServerError)
		_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
		return
	}
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(http.StatusOK)
	_ = json.NewEncoder(w).Encode(map[string]interface{}{"data": products})
	return
}

In the same manner, you must have noticed Product Service also depends on implementation of Product Store

// product_service.go

type ProductService interface {
	List() ([]models.Product, error)
	Get(id int) (models.Product, error)
	Create(models.Product) error
}

type productService struct {
	logger logger.Logger
	store  stores.ProductStore //depends on ProductStore
}

func NewProductService(l logger.Logger, s stores.ProductStore) ProductService {
	return &productService{logger: l, store: s}
}
// main.go
productStore := stores.NewProductStore(logrusLog, dbConn)
productService := services.NewProductService(logrusLog, productStore)

It’s as simple as it looks, no framework magic, simply calling of functions and implementing interfaces.

I highly suggest, cloning the repo and moving around this tiny codebase to get the feel of it even if it’s slightly difficult to understand.

That’s it, I hope you get the idea of Dependency Injection and how to effectively use it in your next big AI enabled Todo application.

thatarif