di

package module
v1.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 18, 2022 License: MIT Imports: 10 Imported by: 18

README

goioc/di: Dependency Injection

goioc

Go go.dev reference CodeFactor Go Report Card codecov Quality Gate Status DeepSource

Why DI in Go? Why IoC at all?

I've been using Dependency Injection in Java for nearly 10 years via Spring Framework. I'm not saying that one can't live without it, but it's proven to be very useful for large enterprise-level applications. You may argue that Go follows a completely different ideology, values different principles and paradigms than Java, and DI is not needed in this better world. And I can even partly agree with that. And yet I decided to create this light-weight Spring-like library for Go. You are free to not use it, after all 🙂

Is it the only DI library for Go?

No, of course not. There's a bunch of libraries around which serve a similar purpose (I even took inspiration from some of them). The problem is that I was missing something in all of these libraries... Therefore I decided to create Yet Another IoC Container that would rule them all. You are more than welcome to use any other library, for example this nice project. And still, I'd recommend stopping by here 😉

So, how does it work?

It's better to show than to describe. Take a look at this toy-example (error-handling is omitted to minimize code snippets):

services/weather_service.go

package services

import (
	"io/ioutil"
	"net/http"
)

type WeatherService struct {
}

func (ws *WeatherService) Weather(city string) (*string, error) {
	response, err := http.Get("https://wttr.in/" + city)
	if err != nil {
		return nil, err
	}
	all, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return nil, err
	}
	weather := string(all)
	return &weather, nil
}

controllers/weather_controller.go

package controllers

import (
	"di-demo/services"
	"github.com/goioc/di"
	"net/http"
)

type WeatherController struct {
	// note that injection works even with unexported fields
	weatherService *services.WeatherService `di.inject:"weatherService"`
}

func (wc *WeatherController) Weather(w http.ResponseWriter, r *http.Request) {
	weather, _ := wc.weatherService.Weather(r.URL.Query().Get("city"))
	_, _ = w.Write([]byte(*weather))
}

init.go

package main

import (
	"di-demo/controllers"
	"di-demo/services"
	"github.com/goioc/di"
	"reflect"
)

func init() {
	_, _ = di.RegisterBean("weatherService", reflect.TypeOf((*services.WeatherService)(nil)))
	_, _ = di.RegisterBean("weatherController", reflect.TypeOf((*controllers.WeatherController)(nil)))
	_ = di.InitializeContainer()
}

main.go

package main

import (
	"di-demo/controllers"
	"github.com/goioc/di"
	"net/http"
)

func main() {
	http.HandleFunc("/weather", func(w http.ResponseWriter, r *http.Request) {
		di.GetInstance("weatherController").(*controllers.WeatherController).Weather(w, r)
	})
	_ = http.ListenAndServe(":8080", nil)
}

If you run it, you should be able to observe a neat weather forecast at http://localhost:8080/weather?city=London (or for any other city).

Of course, for such a simple example it may look like an overkill. But for larger projects with many interconnected services with complicated business logic, it can really simplify your life!

Looks nice... Give me some details!

The main component of the library is the Inversion of Control Container that contains and manages instances of your structures (called "beans").

Types of beans
  • Singleton. Exists only in one copy in the container. Every time you retrieve the instance from the container (or every time it's being injected to another bean) - it will be the same instance.
  • Prototype. It can exist in multiple copies: a new copy is created upon retrieval from the container (or upon injection into another bean).
  • Request. Similar to Prototype, however it has a few differences and features (since its lifecycle is bound to a web request):
    • Can't be injected to other beans.
    • Can't be manually retrieved from the Container.
    • Request beans are automatically injected to the context.Context of a corresponding http.Request.
    • If a Request bean implements io.Closer, it will be "closed" upon corresponding request's cancellation.
Beans registration

For the container to become aware of the beans, one must register them manually (unlike Java, unfortunately, we can't scan classpath to do it automatically, because Go runtime doesn't contain high-level information about types). How can one register beans in the container?

  • By type. This is described in the example above. A structure is declared with a field tagged with di.scope:"<scope>". This field can be even omitted - in this case, the default scope will be Singleton. Than the registration is done like this:
di.RegisterBean("beanID", reflect.TypeOf((*YourAwesomeStructure)(nil)))
  • Using pre-created instance. What if you already have an instance that you want to register as a bean? You can do it like this:
di.RegisterBeanInstance("beanID", yourAwesomeInstance)

For this type of beans, the only supported scope is Singleton, because I don't dare to clone your instances to enable prototyping 😅

  • Via bean factory. If you have a method that is producing instances for you, you can register it as a bean factory:
di.RegisterBeanFactory("beanID", Singleton, func(context.Context) (interface{}, error) {
		return "My awesome string that is going to become a bean!", nil
	})

Feel free to use any scope with this method. By the way, you can even lookup other beans within the factory:

di.RegisterBeanFactory("beanID", Singleton, func(context.Context) (interface{}, error) {
		return di.GetInstance("someOtherBeanID"), nil
	})

Note that factory-method accepts context.Context. It can be useful for request-scoped beans (the HTTP request context is set in this case). For all other beans it will be context.Background().

Beans initialization

There's a special interface InitializingBean that can be implemented to provide your bean with some initialization logic that will we executed after the container is initialized (for Singleton beans) or after the Prototype/Request instance is created. Again, you can also lookup other beans during initialization (since the container is ready by that time):

type PostConstructBean1 struct {
	Value string
}

func (pcb *PostConstructBean1) PostConstruct() error {
	pcb.Value = "some content"
	return nil
}

type PostConstructBean2 struct {
	Scope              Scope `di.scope:"prototype"`
	PostConstructBean1 *PostConstructBean1
}

func (pcb *PostConstructBean2) PostConstruct() error {
	instance, err := di.GetInstanceSafe("postConstructBean1")
	if err != nil {
		return err
	}
	pcb.PostConstructBean1 = instance.(*PostConstructBean1)
	return nil
}
Beans post-processors

The alternative way of initializing beans is using so-called "beans post-processors". Take a look at the example:

type postprocessedBean struct {
	a string
	b string
}

_, _ := RegisterBean("postprocessedBean", reflect.TypeOf((*postprocessedBean)(nil)))

_ = RegisterBeanPostprocessor(reflect.TypeOf((*postprocessedBean)(nil)), func(instance interface{}) error {
    instance.(*postprocessedBean).a = "Hello, "
    return nil
})

_ = RegisterBeanPostprocessor(reflect.TypeOf((*postprocessedBean)(nil)), func(instance interface{}) error {
instance.(*postprocessedBean).b = "world!"
    return nil
})

_ = InitializeContainer()

instance := GetInstance("postprocessedBean")

postprocessedBean := instance.(*postprocessedBean)
println(postprocessedBean.a+postprocessedBean.b) // prints out "Hello, world!"
Beans injection

As was mentioned above, one bean can be injected into another with the PostConstruct method. However, the more handy way of doing it is by using a special tag:

type SingletonBean struct {
	SomeOtherBean *SomeOtherBean `di.inject:"someOtherBean"`
}

... or via interface ...

type SingletonBean struct {
	SomeOtherBean SomeOtherBeansInterface `di.inject:"someOtherBean"`
}

Note that you can refer dependencies either by pointer, or by interface, but not by value. And just a reminder: you can't inject Request beans.

Sometimes we might want to have optional dependencies. By default, all declared dependencies are considered to be required: if some dependency is not found in the Container, you will get an error. However, you can specify an optional dependency like this:

type SingletonBean struct {
	SomeOtherBean *string `di.inject:"someOtherBean" di.optional:"true"`
}

In this case, if someOtherBean is not found in the Container, you will get nill injected into this field.

In fact, you don't need a bean ID to preform an injection! Check this out:

type SingletonBean struct {
	SomeOtherBean *string `di.inject:""`
}

In this case, DI will try to find a candidate for the injection automatically (among registered beans of type *string). Cool, ain't it? 🤠 It will panic though if no candidates are found (and if the dependency is not marked as optional), or if there is more than one candidates found.

Finally, you can inject beans to slices and maps. It works similarly to the ID-less inections above, but injects all candidates that were found:

type SingletonBean struct {
	someOtherBeans []*string `di.inject:""`
}
type SingletonBean struct {
	someOtherBeans map[string]*string `di.inject:""`
}
Circular dependencies

The problem with all IoC containers is that beans' interconnection may suffer from so-called circular dependencies. Consider this example:

type CircularBean struct {
	Scope        Scope         `di.scope:"prototype"`
	CircularBean *CircularBean `di.inject:"circularBean"`
}

Trying to use such bean will result in the circular dependency detected for bean: circularBean error. There's no problem as such with referencing a bean from itself - if it's a Singleton bean. But doing it with Prototype/Request beans will lead to infinite creation of the instances. So, be careful with this: "with great power comes great responsibility" 🕸

What about middleware?

We have some 😎 Here's an example with gorilla/mux router (but feel free to use any other router). Basically, it's an extension of the very first example with the weather controller, but this time we add Request beans and access them via request's context. Also, this example demonstrates how DI can automatically close resources for you (DB connection in this case). The proper error handling is, again, omitted for simplicity.

controllers/weather_controller.go

package controllers

import (
	"database/sql"
	"di-demo/services"
	"github.com/goioc/di"
	"net/http"
)

type WeatherController struct {
	// note that injection works even with unexported fields
	weatherService *services.WeatherService `di.inject:"weatherService"`
}

func (wc *WeatherController) Weather(w http.ResponseWriter, r *http.Request) {
	dbConnection := r.Context().Value(di.BeanKey("dbConnection")).(*sql.Conn)
	city := r.URL.Query().Get("city")
	_, _ = dbConnection.ExecContext(r.Context(), "insert into log values (?, ?, datetime('now'))", city, r.RemoteAddr)
	weather, _ := wc.weatherService.Weather(city)
	_, _ = w.Write([]byte(*weather))
}

controllers/index_controller.go

package controllers

import (
	"database/sql"
	"fmt"
	"github.com/goioc/di"
	"net/http"
	"strings"
	"time"
)

type IndexController struct {
}

func (ic *IndexController) Log(w http.ResponseWriter, r *http.Request) {
	dbConnection := r.Context().Value(di.BeanKey("dbConnection")).(*sql.Conn)
	rows, _ := dbConnection.QueryContext(r.Context(), "select * from log")
	columns, _ := rows.Columns()
	_, _ = w.Write([]byte(strings.ToUpper(fmt.Sprintf("Requests log: %v\n\n", columns))))
	for rows.Next() {
		var city string
		var ip string
		var dateTime time.Time
		_ = rows.Scan(&city, &ip, &dateTime)
		_, _ = w.Write([]byte(fmt.Sprintln(city, "\t", ip, "\t", dateTime)))
	}
}

init.go

package main

import (
	"context"
	"database/sql"
	"di-demo/controllers"
	"di-demo/services"
	"github.com/goioc/di"
	"os"
	"reflect"
)

func init() {
	_, _ = di.RegisterBean("weatherService", reflect.TypeOf((*services.WeatherService)(nil)))
	_, _ = di.RegisterBean("indexController", reflect.TypeOf((*controllers.IndexController)(nil)))
	_, _ = di.RegisterBean("weatherController", reflect.TypeOf((*controllers.WeatherController)(nil)))
	_, _ = di.RegisterBeanFactory("db", di.Singleton, func(context.Context) (interface{}, error) {
		_ = os.Remove("./di-demo.db")
		db, _ := sql.Open("sqlite3", "./di-demo.db")
		db.SetMaxOpenConns(1)
		_, _ = db.Exec("create table log ('city' varchar not null, 'ip' varchar not null, 'time' datetime not null)")
		return db, nil
	})
	_, _ = di.RegisterBeanFactory("dbConnection", di.Request, func(ctx context.Context) (interface{}, error) {
		db, _ := di.GetInstanceSafe("db")
		return db.(*sql.DB).Conn(ctx)
	})
	_ = di.InitializeContainer()
}

main.go

package main

import (
	"di-demo/controllers"
	"github.com/goioc/di"
	"github.com/gorilla/mux"
	_ "github.com/mattn/go-sqlite3"
	"net/http"
)

func main() {
	router := mux.NewRouter()
	router.Use(di.Middleware)
	router.Path("/").HandlerFunc(di.GetInstance("indexController").(*controllers.IndexController).Log)
	router.Path("/weather").Queries("city", "{*?}").HandlerFunc(di.GetInstance("weatherController").(*controllers.WeatherController).Weather)
	_ = http.ListenAndServe(":8080", router)
}

Okaaay... More examples?

Please, take a look at the unit-tests for more examples.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Close added in v1.6.0

func Close()

Close destroys the IoC container - executes io.Closer for all beans which implements it. This is responsibility of consumer to call Close method. If io.Closer returns an error it will just log the error and continue to Close other beans.

func GetBeanScopes added in v1.3.0

func GetBeanScopes() map[string]Scope

GetBeanScopes returns a map (copy) of bean scopes registered in the Container.

func GetBeanTypes added in v1.2.0

func GetBeanTypes() map[string]reflect.Type

GetBeanTypes returns a map (copy) of beans registered in the Container, omitting bean factories, because their real return type is unknown.

func GetInstance

func GetInstance(beanID string) interface{}

GetInstance function returns bean instance by its ID. It may panic, so if receiving the error in return is preferred, consider using `GetInstanceSafe`.

func GetInstanceSafe

func GetInstanceSafe(beanID string) (interface{}, error)

GetInstanceSafe function returns bean instance by its ID. It doesnt panic upon explicit error, but returns the error instead.

func InitializeContainer

func InitializeContainer() error

InitializeContainer function initializes the IoC container.

func Middleware added in v1.1.0

func Middleware(next http.Handler) http.Handler

Middleware is a function that can be used with http routers to perform Request-scoped beans injection into the web request context. If such bean implements io.Closer, it will be attempted to close upon corresponding context cancellation (but may panic).

func RegisterBean

func RegisterBean(beanID string, beanType reflect.Type) (overwritten bool, err error)

RegisterBean function registers bean by type, the scope of the bean should be defined in the corresponding struct using a tag `di.scope` (`Singleton` is used if no scope is explicitly specified). `beanType` should be a reference type, e.g.: `reflect.TypeOf((*services.YourService)(nil))`. Return value of `overwritten` is set to `true` if the bean with the same `beanID` has been registered already.

func RegisterBeanFactory

func RegisterBeanFactory(beanID string, beanScope Scope, beanFactory func(ctx context.Context) (interface{}, error)) (overwritten bool, err error)

RegisterBeanFactory function registers bean, provided the bean factory that will be used by the container in order to create an instance of this bean. `beanScope` can be any scope of the supported ones. `beanFactory` can only produce a reference or an interface. Return value of `overwritten` is set to `true` if the bean with the same `beanID` has been registered already.

func RegisterBeanInstance

func RegisterBeanInstance(beanID string, beanInstance interface{}) (overwritten bool, err error)

RegisterBeanInstance function registers bean, provided the pre-created instance of this bean, the scope of such beans are always `Singleton`. `beanInstance` can only be a reference or an interface. Return value of `overwritten` is set to `true` if the bean with the same `beanID` has been registered already.

func RegisterBeanPostprocessor added in v1.4.0

func RegisterBeanPostprocessor(beanType reflect.Type, postprocessor func(bean interface{}) error) error

RegisterBeanPostprocessor function registers postprocessors for beans. Postprocessor is a function that can perform some actions on beans after their creation by the container (and self-initialization with PostConstruct).

Types

type BeanKey added in v1.1.0

type BeanKey string

BeanKey is as a Context key, because usage of string keys is discouraged (due to obvious reasons).

type ContextAwareBean added in v1.7.0

type ContextAwareBean interface {
	// SetContext method will be called on a bean after its creation.
	SetContext(ctx context.Context)
}

ContextAwareBean is an interface marking beans that can accept context. Mostly meant to be used with Request-scoped beans (HTTP request context will be propagated for them). For all other beans it's gonna be `context.Background()`.

type InitializingBean

type InitializingBean interface {
	// PostConstruct method will be called on a bean after the container is initialized.
	PostConstruct() error
}

InitializingBean is an interface marking beans that need to be additionally initialized after the container is ready.

type Scope

type Scope string

Scope is an enum for bean scopes supported in this IoC container.

const (
	// Singleton is a scope of bean that exists only in one copy in the container and is created at the init-time.
	// If the bean is singleton and implements Close() method, then this method will be called on Close (consumer responsibility to call Close)
	Singleton Scope = "singleton"
	// Prototype is a scope of bean that can exist in multiple copies in the container and is created on demand.
	Prototype Scope = "prototype"
	// Request is a scope of bean whose lifecycle is bound to the web request (or more precisely - to the corresponding
	// context). If the bean implements Close() method, then this method will be called upon corresponding context's
	// cancellation.
	Request Scope = "request"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL