Translation Dependency Injection in the Golang
Article source: Dependency injection in Go
About the author: Drew Olson
Author blog: Software is fun
Translator by: This article introduces di and Golang di library dig simple use, suitable for go have a certain understanding of the developers.
I recently used go to create a small project, and I was immediately shocked by the lack of dependency injection (DI) in the Go language ecosystem because I had been using Java in recent years. I decided to try to create my project with Uber's dig library, and the results were pretty good.
I found that di helped me solve a lot of the problems encountered in previous go applications-- init
overuse of functions, misuse of global variables, and complex application initialization settings.
In this article, I'll introduce Di and show the difference between an app and the use of Di (using the Dig library).
DI overview
Dependency injection is a concept in which your components (usually in Go structs
) should receive their dependencies when they are created. This is in contrast to the reverse pattern of creating your own dependencies during component initialization . Let's take a look at an example.
Suppose you have a Server
structure that requires a structure to implement its function Config
. One way to do this is to build it on its own at the Server
time of initialization Config
.
type Server struct { config *Config}func New() *Server { return &Server{ config: buildMyConfigSomehow(), }}
This looks convenient and the caller doesn't even need to know what we Server
need to access Config
. These are hidden from the user of the function.
However, this also has some shortcomings. First, if we want to change the Config
way we create it, we have to modify all the code that invokes its creation method. Suppose, for example, that our buildMyConfigSomehow
function now requires a parameter, and each call will have to access that parameter, and it needs to be passed to the buildMyConfigSomehow
method.
Also, mocks Config
can become tricky. We have to somehow touch the inside of the New
function to Config
create the tinker.
Here's how to use di:
type Server struct { config *Config}func New(config *Config) *Server { return &Server{ config: config, }}
Now Server
that creation is Config
decoupled from creation, we are free to choose the logic we created and Config
pass the result data to the New
method.
Moreover, if Config
it is an interface, the mock will become easier, and as long as the interface is implemented, it can be passed to the New
method. This makes it easy for us to Server
Mock.
The main downside is that we Server
have to create it manually before we create Config
it, which is more painful. We created a "dependency graph"--we had to create Config
it first because Server
it depended on it. In real-world applications, dependency graphs can become very large, which can cause the logic of creating all the components your application needs to be very complex.
This is where the DI framework can provide help. A DI framework typically has two main functions:
- A mechanism to "provide" a new component. In general, this mechanism tells the DI framework what other components you need to create yourself ( a program that can be understood as one of your programs ) and how to create it after you have these components.
- A mechanism for "fetching" the created components.
A DI framework is typically based on a "provider" that you tell the framework to create a graph (graph) and decide how to create your object. Abstract concepts are more difficult to understand, so let's look at a suitable example.
An example app
We're going to look at a piece of code, a GET
people
HTTP server that sends a JSON response when a client request is received. For simplicity's sake, we put the code in the main
bag, but don't do it in reality. All the instance code can be downloaded here.
First, let's look at the Person
structure, except for some JSON tag, which does not have any behavior definitions.
type Person struct { Id int `json:"id"` Name string `json:"name"` Age int `json:"age"`}
One Person
has Id
, Name
and Age
, nothing else.
Next, let's Config
take a look, like Person
, it doesn't have any dependencies, unlike that Person
, we'll provide a constructor (constructor).
type Config struct { Enabled bool DatabasePath string Port string}func NewConfig() *Config { return &Config{ Enabled: true, DatabasePath: "./example.db", Port: "8000", }}
Enabled
Controls whether the app should return real data. DatabasePath
represents the database location (we use SQLite). Port
represents the port that the server is listening on while it is running.
This function is used to open a database connection, which relies on Config
and returns one *sql.DB
.
func ConnectDatabase(config *Config) (*sql.DB, error) { return sql.Open("sqlite3", config.DatabasePath)}
And then we'll see PersonRepository
. This structure is responsible for retrieving the people data from the database and deserializing it into the Person
structure.
type PersonRepository struct { database *sql.DB}func (repository *PersonRepository) FindAll() []*Person { rows, _ := repository.database.Query( `SELECT id, name, age FROM people;` ) defer rows.Close() people := []*Person{} for rows.Next() { var ( id int name string age int ) rows.Scan(&id, &name, &age) people = append(people, &Person{ Id: id, Name: name, Age: age, }) } return people}func NewPersonRepository(database *sql.DB) *PersonRepository { return &PersonRepository{database: database}}
PersonRepository
A database connection to be created is required. It exposes a function called, which FindAll
uses a database connection and returns a sequential table containing the database data Person
.
To add a new layer to the HTTP server and the PersonRepository
middle, we'll create one PersonService
.
type PersonService struct { config *Config repository *PersonRepository}func (service *PersonService) FindAll() []*Person { if service.config.Enabled { return service.repository.FindAll() } return []*Person{}}func NewPersonService(config *Config, repository *PersonRepository)*PersonService { return &PersonService{config: config, repository: repository}}
We are PersonService
dependent on Config
and PersonRepository
. It exposes a FindAll
function called, depending on whether the application is enabled to invoke PersonRepository
.
Finally, we came Server
, responsible for running an HTTP Server, and assigning requests to PersonService
processing.
type Server struct { config *Config personService *PersonService}func (s *Server) Handler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/people", s.people) return mux}func (s *Server) Run() { httpServer := &http.Server{ Addr: ":" + s.config.Port, Handler: s.Handler(), } httpServer.ListenAndServe()}func (s *Server) people(w http.ResponseWriter, r *http.Request) { people := s.personService.FindAll() bytes, _ := json.Marshal(people) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(bytes)}func NewServer(config *Config, service *PersonService) *Server { return &Server{ config: config, personService: service, }}
Server
dependent on PersonService
and Config
.
Well, we now know all the components of our system. Now how the hell do you initialize them and start our system?
Scary Main ()
func main() { config := NewConfig() db, err := ConnectDatabase(config) if err != nil { panic(err) } personRepository := NewPersonRepository(db) personService := NewPersonService(config, personRepository) server := NewServer(config, personService) server.Run()}
We first create Config
and then use the Config
Create database connection. Next we can create PersonRepository
, so we can create PersonService
, and finally, we use these to create Server
and launch it.
Call, the operation was too complicated. Worse, our complexity continues to grow when our applications become more complex main
. Each time one of our components adds a dependency, in order to create this component, we have to ponder the main
order and logic of this dependency in.
So you might guess that a dependency injection framework can help us solve this problem, so let's try it out.
Create a container
"Container (Container)" This belongs in the DI framework, which usually means that you store "provider (provider)" and get a place to create good objects.
dig
The library provides us with Provide
functions for adding our own providers, as well as Invoke
functions for getting the created objects from the container.
First, we create a new container.
container := dig.New()
Now we can add a new provider, the method is simple, we call the function with container Provide
, this function takes a parameter: a function. This parameter function can have any number of arguments (representing the dependencies of the component to be created) and one or two return values (the component provided by the function, and an optional error).
container.Provide(func() *Config { return NewConfig()})
The code above says, "I provide one Config
to the container, in order to create it, I don't need anything else." ”。 Now that we've told the container how to create a Config
type, we can use it to create other types.
container.Provide(func(config *Config) (*sql.DB, error) { return ConnectDatabase(config)})
This part of the code says, "I provide a *sql.DB
type to the container, in order to create it, I need a Config
, I might return an optional error".
In these two examples, we are a bit verbose, because we have defined NewConfig
and ConnectDatabase
functions, we can directly use them as providers to the container.
container.Provide(NewConfig)container.Provide(ConnectDatabase)
Now, we can request a constructed component directly to the container, any type we give the provider is OK. We use Invoke
the function to do this operation. The Invoke
function receives a parameter--a function that receives an arbitrary number of arguments, which is exactly the component we want the container to create for us.
container.Invoke(func(database *sql.DB) { // sql.DB is ready to use here})
The operation of the container is very clever, it does this:
* 容器识别到我们在请求一个`*sql.DB`* 它发现我们的函数`ConnectDatabase`提供了这种类型* 接下来它发现`ConnectDatabase`依赖一个`Config`* 它会找到`Config`的提供者,`NewConfig`函数* `NewConfig`函数没有任何依赖,于是被调用 * `NewConfig`的返回值是一个`Config`,被作为参数传递给`ConnectDatabase`* `ConnectionData`的结果是一个`*sql.DB`,被传回给`Invoke`的调用者
The container does a lot of things for us, and in fact it does more. Containers only provide one instance for each type, which means we never accidentally create a second database connection, even if we call in multiple places (such as multiple repository).
A better Main ()
Now that we know dig
how the container works, let's use it to create a better main.
func BuildContainer() *dig.Container { container := dig.New() container.Provide(NewConfig) container.Provide(ConnectDatabase) container.Provide(NewPersonRepository) container.Provide(NewPersonService) container.Provide(NewServer) return container}func main() { container := BuildContainer() err := container.Invoke(func(server *Server) { server.Run() }) if err != nil { panic(err) }}
Except Invoke
for the error
return, we have seen the others. If any of the Invoke
used providers return an error, the call is stopped and an error is Invoke
returned.
Even if the example is small, it should be enough to see some of the benefits of this approach compared to the "standard" main. As our applications grow in size, the benefits will be even more pronounced.
One of the most important benefits is the creation of decoupled components and the creation of dependencies. Let's say we PersonRepository
need access Config
, we just need to modify the constructor NewPersonRepository
, add a parameter to it Config
, and nothing else will change.
There are also some other benefits that reduce the overall state, reduce init
the number of calls (dependencies will be created when needed, and will only be created once, avoiding an error prone init
call), making it easier to test independent components. Imagine creating a container to request a created object for testing, or to mock all dependencies and create objects. All of this will be easy with the introduction of Di.
An idea worth promoting.
I believe that dependency injection helps us create a more robust and easier-to-test program. This is even more irrefutable as the size of the application grows. Go is great for creating large applications and dig
is a good di tool. I think the go community should accept Di and use it in as many applications as possible.
Update
Google recently released its own DI container, called a wire. It avoids runtime reflection in a way that code is generated. I recommend wire to dig.