golang implements the dependency injection approach in less than 30 lines of code

  • 2020-06-15 09:18:13
  • OfStack

This article introduces the method of implementing dependency injection in less than 30 lines of golang code and shares it with you as follows:

The project address

go-di-demo

This project depends on

Standard library implementation with no additional dependencies

The advantages of dependency injection

Anyone using java will be familiar with the spring framework 1. The spring core is an IoC(Inversion of Control/dependency injection) container, with one big advantage being decoupling. 1 generally only depends on the container, not on the specific class, when your class has changes, you need to change at most 1 container related code, business code is not affected.

The dependency injection principle of golang

In general, similar to java, the steps are as follows :(golang does not support dynamic object creation, so you need to create the object manually first and then inject it; java can create the object dynamically directly)

Read object dependencies by reflection (golang is implemented by tag) Look in the container for an instance of the object If there is an instance of the object or a factory method to create the object, inject the object or use the factory to create the object and inject it If there is no instance of the object, an error is reported

Code implementation

A typical container implementation is as follows, the dependency type refers to singleton/prototype of spring, object singleton object and instance object respectively:


package di

import (
 "sync"
 "reflect"
 "fmt"
 "strings"
 "errors"
)

var (
 ErrFactoryNotFound = errors.New("factory not found")
)

type factory = func() (interface{}, error)
//  The container 
type Container struct {
 sync.Mutex
 singletons map[string]interface{}
 factories map[string]factory
}
//  Container instantiation 
func NewContainer() *Container {
 return &Container{
  singletons: make(map[string]interface{}),
  factories: make(map[string]factory),
 }
}

//  Register the singleton object 
func (p *Container) SetSingleton(name string, singleton interface{}) {
 p.Lock()
 p.singletons[name] = singleton
 p.Unlock()
}

//  Gets the singleton object 
func (p *Container) GetSingleton(name string) interface{} {
 return p.singletons[name]
}

//  Get the instance object 
func (p *Container) GetPrototype(name string) (interface{}, error) {
 factory, ok := p.factories[name]
 if !ok {
  return nil, ErrFactoryNotFound
 }
 return factory()
}

//  Set up the instance object factory 
func (p *Container) SetPrototype(name string, factory factory) {
 p.Lock()
 p.factories[name] = factory
 p.Unlock()
}

//  dependent 
func (p *Container) Ensure(instance interface{}) error {
 elemType := reflect.TypeOf(instance).Elem()
 ele := reflect.ValueOf(instance).Elem()
 for i := 0; i < elemType.NumField(); i++ { //  Through field 
  fieldType := elemType.Field(i)
  tag := fieldType.Tag.Get("di") //  To obtain tag
  diName := p.injectName(tag)
  if diName == "" {
   continue
  }
  var (
   diInstance interface{}
   err  error
  )
  if p.isSingleton(tag) {
   diInstance = p.GetSingleton(diName)
  }
  if p.isPrototype(tag) {
   diInstance, err = p.GetPrototype(diName)
  }
  if err != nil {
   return err
  }
  if diInstance == nil {
   return errors.New(diName + " dependency not found")
  }
  ele.Field(i).Set(reflect.ValueOf(diInstance))
 }
 return nil
}

//  Gets the name of the dependency to be injected 
func (p *Container) injectName(tag string) string {
 tags := strings.Split(tag, ",")
 if len(tags) == 0 {
  return ""
 }
 return tags[0]
}

//  Checks for singleton dependencies 
func (p *Container) isSingleton(tag string) bool {
 tags := strings.Split(tag, ",")
 for _, name := range tags {
  if name == "prototype" {
   return false
  }
 }
 return true
}

//  Checks for instance dependencies 
func (p *Container) isPrototype(tag string) bool {
 tags := strings.Split(tag, ",")
 for _, name := range tags {
  if name == "prototype" {
   return true
  }
 }
 return false
}

//  Print the instance inside the container 
func (p *Container) String() string {
 lines := make([]string, 0, len(p.singletons)+len(p.factories)+2)
 lines = append(lines, "singletons:")
 for name, item := range p.singletons {
  line := fmt.Sprintf(" %s: %x %s", name, &item, reflect.TypeOf(item).String())
  lines = append(lines, line)
 }
 lines = append(lines, "factories:")
 for name, item := range p.factories {
  line := fmt.Sprintf(" %s: %x %s", name, &item, reflect.TypeOf(item).String())
  lines = append(lines, line)
 }
 return strings.Join(lines, "\n")
}
The most important is the Ensure method, which scans all the export fields of the instance and reads the di tag, starting the injection if it exists. Determine the type of di tag to determine whether to inject an singleton or prototype object

test

There is only one instance of a singleton in the container, so no matter where it is injected, the pointer to a singleton will always be 1. Instance objects are created with the same factory method, so Pointers to each instance cannot be the same.

The following is the test entry code, the complete code is in github warehouse, those who are interested can browse:


package main

import (
 "di"
 "database/sql"
 "fmt"
 "os"
 _ "github.com/go-sql-driver/mysql"
 "demo"
)

func main() {
 container := di.NewContainer()
 db, err := sql.Open("mysql", "root:root@tcp(localhost)/sampledb")
 if err != nil {
  fmt.Printf("error: %s\n", err.Error())
  os.Exit(1)
 }
 container.SetSingleton("db", db)
 container.SetPrototype("b", func() (interface{}, error) {
  return demo.NewB(), nil
 })

 a := demo.NewA()
 if err := container.Ensure(a); err != nil {
  fmt.Println(err)
  return
 }
 //  Print the pointer to ensure that the singleton and instance pointer addresses 
 fmt.Printf("db: %p\ndb1: %p\nb: %p\nb1: %p\n", a.Db, a.Db1, &a.B, &a.B1)
}

After execution, the printed result is:

[

db: 0xc4200b6140
db1: 0xc4200b6140
b: 0xc4200a0330
b1: 0xc4200a0338

]

You can see a pointer to one of the two db instances, indicating that it is the same instance, while the two b Pointers are different, indicating that it is not an instance.

Write in the last

Dependency injection is a good way to manage the instantiation and dependencies between multiple objects. In combination with configuration files, the instances that need to be injected into the container are registered in the application initialization phase, and the container can be injected anywhere in the application at the time of instantiation. No additional dependencies.


Related articles: