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:
- 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. - 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 theNewUserService
, we have no easy way to calldb.Close()
other than adding aClose()
method on theUserService
itself.. which is kind of weird when you think about it. Why would a thing calledUserService
need to close? It should just contain the business logic to handle users in our system. - Each time we call
sql.Open
we are likely creating a new pool of connections (this is driver specific) as thesql.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. - 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:
There is no ads to display, Please add some