Golang Tutorial – How to do Dependency Injection with Go

Dependency Injection with Golang Tutorial

Have you heard the term “Dependency Injection” before but struggled to grok what it means? How do you properly do Dependency Injection in your Go applications, and more importantly, why? What does Dependency Injection enable you to do, and why is preferred over other methods?

These are the questions I hope to answer with this post. Hopefully, after reading, you’ll feel more confident and better equipped to answer the above questions and educate your team on best practices for managing internal dependencies in your code.

Overview

Before we get started, however, we should probably define Dependency Injection and why it’s widely considered a best practice, especially in the Go community.

“Dependency Injection is a 25-dollar term for a 5-cent concept.”

— James Shore

note: There are tons of articles written over the past 20 years or so about Inversion of Control/Dependency Injection so I won’t go too in-depth in this post, but I do want to cover the basics.

I think Dependency Injection (DI) is when you ‘pass in’ the resources (dependencies) that your code needs to ‘do its job’ instead of your code ‘reaching out’ for those resources.

An example in a basic Go application would be providing a type with a sql.DB instance so that it can interact with the database. Let’s create and write some code.

First Attempt with Golang

Here’s what this might look like without using Dependency Injection with Golang:

// services/user.go

// UserService queries and mutates users in the database.
type UserService struct {
   db *sql.DB
}

// NewUserService 'constructs' a UserService that is ready to use.
func NewUserService() (*UserService, error) {
   db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/db")
   if err != nil {
       return nil, err
   }

   // TODO: how do we close the db connection when we are done?
   // defer db.Close()

   return &UserService{db}, nil
}

This is probably how most beginning developers would fulfill the requirement that a UserService needs to be able to maintain a connection to the database. Let’s go over why implementing it this way is a bad idea:

  1. This UserService type is extremely hard to test since this code assumes you will always use a MySQL instance with the given connection string. This could be mitigated somewhat using environment variables, however, we’ll discuss why this isn’t ideal later on.
  2. It’s best practice to always close the sql.DB connection when you are done with it. Here since our DB connection is created in the NewUserService, we have no easy way to call db.Close() other than adding a Close() method on the UserService itself.. which is kind of weird when you think about it. Why would a thing called UserService need to close? It should just contain the business logic to handle users in our system.
  3. Each time we call sql.Open we are likely creating a new pool of connections (this is driver specific) as the sql.DB docs states: “The returned DB is safe for concurrent use by multiple goroutines and maintains its own pool of idle connections. Thus, the Open function should be called just once”. This means that if we had another ‘Service’ type, it would also be opening its connection(s) to the database and unable to use the existing pooled idle connections.
  4. It’s unclear from an external ‘API’ perspective that the UserService does anything with a database at all since our database initialization is ‘hidden’ within. This makes the code harder to read and understand at a glance.

Global State with Golang

Instead of opening a connection each time you instantiate a new Service type, you could create the sql.DB handle once and use it wherever you need it in your application like so: