Translation Dependency Injection in the Golang

Source: Internet
Author: User

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:

    1. 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.
    2. 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",  }}

EnabledControls whether the app should return real data. DatabasePathrepresents the database location (we use SQLite). Portrepresents 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}}

PersonRepositoryA 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,  }}

Serverdependent 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.
digThe 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.

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.