Container-runtime dependency injection container for Go

Posted May 25, 20208 min read

Container is a runtime dependency injection library developed for Go language. The language features of Go language determine that it is not easy to implement a type-safe dependency injection container, so Container makes extensive use of Go's reflection mechanism. If your usage scenario is not that demanding on performance, then Container is for you.

It's not that you can't use it in an environment with strict performance requirements. You can use Container as an object dependency management tool to obtain dependent objects during the initialization of your business.

How to use

go get github.com/mylxsw/container

To create a Container instance, use the containier.New method

cc:= container.New()

An empty container is created.

You can also use container.NewWithContext(ctx) to create a container. After creation, you can automatically add the existing context.Context object to the container, which is managed by the container.

Object binding

Before using it, we need to tell the container what we want to host. Container supports three types of object management

  • Singleton object Singleton
  • Prototype objects(multiple objects) Prototype
  • String value object binding Value

All object binding methods will return a error return value to indicate whether the binding is successful, the application must actively check this error when using it.

Make sure that the object will be bound successfully(generally does not violate the parameter signature method described in the document, it will always be successful) or that the object must be bound successfully(usually we all require this, otherwise how to do dependency management) You can use the Must series of methods, such as the Singleton method corresponding to MustSingleton, when the creation error, the method will directly panic.

When binding objects, the methods Singleton, Prototype, and BindValue can only be bound once for the same type. If you bind the creation function of an object of the same type multiple times, an error of ErrRepeatedBind will be returned.

Sometimes, I hope that the object creation function can be rebound multiple times, so that more extensibility can be applied, and the object creation method can be replaced at any time, such as the injection of the Mock object during testing. At this time we can use the Override series method:

  • SingletonOverride
  • PrototypeOverride
  • BindValueOverride

When using the Override series method, you must ensure that the Override series method is used for the first binding, otherwise you cannot rebind.

In other words, you can bind SingletonOverride-> SingletonOverride, SingletonOverride-> Singleton, but once Singleton appears, you cannot rebind the object later.

Singleton Object

Use the Singleton series of methods to host singleton objects to the container. Singleton objects will only be created automatically the first time they are used, and all subsequent access to the object will automatically create the objects that have been created Inject.

The commonly used method is the Singleton(initialize interface {}) error method, which will complete the registration of the singleton object according to the initialize function or object you provide.

The parameter initialize supports the following forms:

  • Object creation function func(deps ...) object return value

    such as

      cc.Singleton(func() UserRepo {return & userRepoImpl {}})
      cc.Singleton(func()(* sql.DB, error) {
          return sql.Open("mysql", "user:pwd @ tcp(ip:3306)/dbname")
      })
      cc.Singleton(func(db * sql.DB) UserRepo {
          //The userRepoImpl object we created here depends on the sql.DB object and only needs to be in the function
          //In the parameters, list the dependencies and the container will automatically create these objects
          return & userRepoImpl {db:db}
      })
  • Object creation function with error return value func(deps ...)(object return value, error)

    The object creation function supports up to two return values, and requires the first return value to be the object to be created, and the second return value to be the error object.

      cc.Singleton(func()(Config, error) {
          //Suppose we want to create a configuration object, and the configuration is read from a file when the object is initialized
          content, err:= ioutil.ReadFile("test.conf")
          if err! = nil {
              return nil, err
          }
    
          return config.Load(content), nil
      })
  • Directly bind objects

    If the object has been created, and you want Container to manage it, you can directly pass the object to the Singleton method

      userRepo:= repo.NewUserRepo()
      cc.Singleton(userRepo)

When the object is used for the first time, Container caches the execution result of the object creation function, so that the same object can be obtained at any time after access.

Prototype objects(multiple objects)

Prototype objects(multiple objects) refer to the creation process of objects managed by Container, but each time you use dependency injection to get a newly created object.

Use the Prototype series of methods to host the creation of prototype objects to the container. The commonly used method is Prototype(initialize interface {}) error.

The acceptable type of the parameter initialize is exactly the same as the Singleton series of functions. The only difference is that when the object is used, the singleton object returns the same object every time, while the prototype object returns new creation every time. Object.

String value object binding

This binding method is to bind an object to Container, but unlike the Singleton series method, it requires that you must specify a string type of Key, each time the object is obtained When using the Get series function to get the bound object, just pass the string Key directly.

The commonly used binding method is BindValue(key string, value interface {}).

cc.BindValue("version", "1.0.1")
cc.MustBindValue("startTs", time.Now())
cc.BindValue("int_val", 123)

Dependency injection

When using bound objects, we usually use the Resolve and Call series methods.

Resolve

The Resolve(callback interface {}) error method execution body callback can only perform dependency injection, and does not receive the return value of the injected function. Although there is a error return value, this value only indicates whether an error occurs when the object is injected. .

For example, we need to obtain information about a certain user and its role, using the Resolve method

cc.MustResolve(func(userRepo repo.UserRepo, roleRepo repo.RoleRepo) {
    //Query the user with id = 123, the query fails directly panic
    user, err:= userRepo.GetUser(123)
    if err! = nil {
        panic(err)
    }
    //Query the user role. When the query fails, we ignore the returned error
    role, _:= roleRepo.GetRole(user.RoleID)

    //do something you want with user/role
})

Using the Resolve method directly may not meet our daily business needs, because when executing a query, we will always encounter various errors, and directly discarding will generate many hidden bugs, but we are not inclined Use Panic this violent way to solve.

Container provides the ResolveWithError(callback interface {}) error method. When using this method, our callback can accept a error return value to tell the caller that something is wrong here.

err:= cc.ResolveWithError(func(userRepo repo.UserRepo, roleRepo repo.RoleRepoo) error {
    user, err:= userRepo.GetUser(123)
    if err! = nil {
        return err
    }

    role, err:= roleRepo.GetRole(user.RoleID)
    if err! = nil {
        return err
    }

    //do something you want with user/role

    return nil
})
if err! = nil {
    //Custom error handling
}

Call

The Call(callback interface {})([]interface {}, error) method not only completes the object's dependency injection, but also returns the callback return value, which is an array structure.

such as

results, err:= cc.Call(func(userRepo repo.UserRepo)([]repo.User, error) {
    users, err:= userRepo.AllUsers()
    return users, err
})
if err! = nil {
    //err here is an error during the dependency injection process, such as the failure to create a dependent object
}

//results is an array of type []interface {}, which contains the return value of the callback function in order
//results [0]-[]repo.User
//results [1]-error
//Since each return value is of type interface {}, you need to perform type assertion when you use it, convert it to a specific type and then use it
users:= results [0].([]repo.User)
err:= results [0].(error)

Provider

Sometimes we want to bind different function modules for different function implementations. For example, in the Web server, the handler function of each request needs to access the request/response object related to this request. After the request is completed, in the Container The request/response object is useless, and different requests do not get the same object. We can use CallWithProvider(callback interface {}, provider func() []* Entity)([]interface {}, error) with Provider(initializes ... interface {})(func() []* Entity , error) method to achieve this function.

ctxFunc:= func() Context {return ctx}
requestFunc:= func() Request {return ctx.request}

provider, _:= cc.Provider(ctxFunc, requestFunc)
results, err:= cc.CallWithProvider(func(userRepo repo.UserRepo, req Request)([]repo.User, error) {
    //The Request object we injected here is only valid for the current callback
    userId:= req.Input("user_id")
    users, err:= userRepo.GetUser(userId)

    return users, err
}, provider)

AutoWire structure attribute injection

The AutoWire method can be used to inject the bound object of the structure property. To use this feature, we need to add the autowire tag to the structure object that needs to be injected.

type UserManager struct {
    UserRepo * UserRepo `autowire:" @ "json:"-"`
    field1 string `autowire:" version "`
    Field2 string `json:" field2 "`
}

manager:= UserManager {}
//After executing AutoWire on the manager, the values ​​of UserRepo and field1 are automatically injected
if err:= c.AutoWire(& manager); err! = nil {
    t.Error("test failed")
}

Structure attribute injection supports the injection of public and private fields. If the object is injected by type, use autowire:" @ " to mark the attribute; if you use the object whose string is the key bound by BindValue, useautowire:"KeyNAME"to mark Attributes.

Since AutoWire wants to modify the object, the pointer of the object must be used, and the structure type must use&.

Other methods

HasBound/HasBoundValue

Method signature

HasBound(key interface {}) bool
HasBoundValue(key string) bool

Used to determine whether the specified Key has been bound.

Keys

Method signature

Keys() []interface {}

Get all the object information bound to Container.

CanOverride

Method signature

CanOverride(key interface {})(bool, error)

Determine whether the specified Key can be overwritten and rebind the creation function.

Extend

Extend is not a method on the Container instance, but an independent function used to generate a new Container from an existing Container. The new Container inherits all the object bindings of the existing Container.

Extend(c Container) Container

After container inheritance, when searching for dependency injection objects, it will first search from the current Container, and when the object is not found, then from the parent object.

On the Container instance, there is a method named ExtendFrom(parent Container), which is used to specify that the current Container inherits from parent.

Sample project

For a simple example, please refer to the example directory of the project.

The following projects use Container as a dependency injection management library. Those interested can refer to it.

  • Glacier An application management framework, there is currently no written documentation, the framework integrates Container, which is used to manage the object instantiation of the framework.
  • Adanos-Alert uses an alarm system developed by Glacier, which focuses not on monitoring but on alarms, which can aggregate various alarm information, According to the configuration rules to achieve diversified alarms, it is generally used in conjunction with Logstash to complete business and error log alarms, and in conjunction with Prometheus, OpenFalcon and other mainstream monitoring frameworks to complete service-level alarms. It is still under development, but basic functions are already available.
  • Sync uses Glacier to develop a cross-host file synchronization tool, has a friendly web configuration interface, and uses GRPC to synchronize files between different servers.