Dependency Injection

I don’t know if you will encounter the interdependency of components when you write services in Go language, object A depends on object B, and object B depends on object C. So before initializing object A, you must first initialize B and C. This is a complicated relationship. Perhaps you will think of another approach, that is, to declare each object as a global variable, I personally do not recommend this way of use, although it is very convenient, but will make the overall structure becomes very complex. This article will introduce a savior tool is the Wire tool developed by the Google team, the official blog can also refer to see. This tool is to solve the following two problems (dependency injection).

  1. Components interdependent intricate relationships
  2. Do not declare global variables

Module dependency problem

All of the following code can be found here.

Module dependency

Referring to the above diagram, if the developer wants to declare User’s struct in main.go, he will need to depend on it layer by layer, so the code will be written as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
cfg, err := config.Environ()
if err != nil {
 log.Fatal().
   Err(err).
   Msg("invalid configuration")
}

c, err := cache.New(cfg)
if err != nil {
 log.Fatal().
   Err(err).
   Msg("invalid configuration")
}

l, err := ldap.New(cfg, c)
if err != nil {
 log.Fatal().
   Err(err).
   Msg("invalid configuration")
}

cd, err := crowd.New(cfg, c)
if err != nil {
 log.Fatal().
   Err(err).
   Msg("invalid configuration")
}

u, err := user.New(l, cd, c)
if err != nil {
 log.Fatal().
   Err(err).
   Msg("invalid configuration")
}

if ok := u.Login("test", "test"); !ok {
  log.Fatal().
    Err(err).
    Msg("invalid configuration")
}

m := graceful.NewManager()
srv := &http.Server{
  Addr:              cfg.Server.Port,
  Handler:           router.New(cfg, u),
  ReadHeaderTimeout: 5 * time.Second,
  ReadTimeout:       5 * time.Minute,
  WriteTimeout:      5 * time.Minute,
  MaxHeaderBytes:    8 * 1024, // 8KiB
}

The above code will be more complicated if we write dozens of components. In fact, we only use user and router in the main program, but we have written a bunch of code just to declare the other components, can we optimize the code into a struct.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type application struct {
  router http.Handler
  user   *user.Service
}

func newApplication(
  router http.Handler,
  user *user.Service,
) *application {
  return &application{
    router: router,
    user:   user,
  }
}

This way, dependencies in the application are handled by wire whenever they are declared.

Using the Wire Tool

1
2
3
4
5
6
7
8
9
func newApplication(
  router http.Handler,
  user *user.Service,
) *application {
  return &application{
    router: router,
    user:   user,
  }
}

To let the wire tool know all the above dependencies, first create an initialization function and note that the wireinject build tag cannot be removed, it is for the wire CLI tool to identify.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//go:build wireinject
// +build wireinject

package main

import (
  "github.com/go-training/example49-dependency-injection/config"

  "github.com/google/wire"
)

func InitializeApplication(cfg config.Config) (*application, error) {
  wire.Build(
    routerSet,
    userSet,
    newApplication,
  )
  return &application{}, nil
}

As you can see we need to tell the wire tool the relationship between router, user and newApplication, so after creating inject_router.go and inject_user.go files respectively, let’s look at the router part first.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var routerSet = wire.NewSet( //nolint:deadcode,unused,varcheck
  provideRouter,
)

func provideRouter(
  cfg config.Config,
  user *user.Service,
) http.Handler {
  return router.New(cfg, user)
}

This side only relies on the config and user objects and passes the http.Handler.

Next is the user part.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var userSet = wire.NewSet( //nolint:deadcode,unused,varcheck
  provideUser,
  provideLDAP,
  provideCROWD,
  provideCache,
)

func provideUser(
  l *ldap.Service,
  c *crowd.Service,
  cache *cache.Service,
) (*user.Service, error) {
  return user.New(l, c, cache)
}

func provideLDAP(
  cfg config.Config,
  cache *cache.Service,
) (*ldap.Service, error) {
  return ldap.New(cfg, cache)
}

func provideCROWD(
  cfg config.Config,
  cache *cache.Service,
) (*crowd.Service, error) {
  return crowd.New(cfg, cache)
}

func provideCache(
  cfg config.Config,
) (*cache.Service, error) {
  return cache.New(cfg)
}

The user relies on the cache, ldap and crowd objects, and when it is done, the wire_gen.go file can be generated by the wire command.

1
wire gen ./...

Open wire_gen.go.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func InitializeApplication(cfg config.Config) (*application, error) {
  service, err := provideCache(cfg)
  if err != nil {
    return nil, err
  }
  ldapService, err := provideLDAP(cfg, service)
  if err != nil {
    return nil, err
  }
  crowdService, err := provideCROWD(cfg, service)
  if err != nil {
    return nil, err
  }
  userService, err := provideUser(ldapService, crowdService, service)
  if err != nil {
    return nil, err
  }
  handler := provideRouter(cfg, userService)
  mainApplication := newApplication(handler, userService)
  return mainApplication, n

You can see that the wire tool automatically takes care of all the dependencies for us by prefixing the provide function with our own definition, so this tool lets you take care of all the dependencies in the main program instead of using global variables.

Thoughts

In addition to using it in the main function, it can also be used in the test, the test is also to deal with the dependencies are finished, so that it is convenient to test. I believe you will definitely encounter this problem when dealing with dependencies. A good practice is not to declare the settings of other packages in the package, which will be very difficult to maintain. The code can be viewed here.

Reference

  • https://github.com/google/wire
  • https://en.wikipedia.org/wiki/Dependency_injection
  • https://blog.wu-boy.com/2022/09/dependency-injection-in-go/