# Packages
# README
Container
This package provides a concurrently safe DI container. It supports scoped variables, and atomic hot swapping. For bigger projects, it provides a tool for the code generation.
go get -u github.com/gontainer/gontainer-helpers/v2/container@latest
Why?
Automatically build and inject scope-aware dependencies.
Let's imagine we work on an endpoint that allows for transferring funds between different accounts. We have to operate on an SQL transaction, and we have to inject the same transaction into many different objects.
Or maybe you need to reload the configuration without restarting the app? See hotswap.
See code
type TransactionHistory struct {
tx *sql.Tx
}
func (*TransactionHistory) Record(accountID int, amount int) error {
// TODO
}
type FundsTransferer struct {
tx *sql.Tx
}
func (*FundsTransferer) Transfer(fromID int, toID int, amount int) error {
// TODO
}
type Factory interface {
Tx(context.Context) *sql.Tx
TransactionHistory(context.Context) *TransactionHistory
FundsTransferer(context.Context) *FundsTransferer
}
func NewTransferFundsHandler(f Factory) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
/*
There is an *sql.Tx object here, and two other that rely on *sql.Tx.
Container creates a new instance of *sql.Tx and injects it to the scope of the current context only.
You do not need to create a new transaction manually and inject it in many dependencies.
*/
var (
tx = f.Tx(ctx)
transferer = f.FundsTransferer(ctx)
history = f.TransactionHistory(ctx)
)
var err error
defer func() {
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_ = tx.Rollback()
}
}()
fromID := 1
toID := 2
amount := 100
err = transferer.Transfer(fromID, toID, amount)
if err != nil {
return
}
err = history.Record(fromID, -amount)
if err != nil {
return
}
err = history.Record(toID, amount)
if err != nil {
return
}
_ = tx.Commit()
})
}
Quick start
package main
import (
"fmt"
"github.com/gontainer/gontainer-helpers/v2/container"
)
type God struct {
Name string
}
func NewGod(name string) God {
return God{Name: name}
}
type Team struct {
Gods []God
}
func buildContainer() *container.Container {
// describe Poseidon
poseidon := container.NewService()
poseidon.SetValue(God{})
poseidon.SetField("Name", container.NewDependencyValue("Poseidon")) // field injection
poseidon.Tag("olympians", 0)
// describe Athena
athena := container.NewService()
athena.SetValue(God{
Name: "Athena",
})
athena.Tag("olympians", 0)
// describe Zeus
zeus := container.NewService()
zeus.SetConstructor(
NewGod,
container.NewDependencyValue("Zeus"), // constructor injection
)
zeus.Tag("olympians", 1) // Zeus has a higher priority
// describe Olympians
olympians := container.NewService()
olympians.SetValue(Team{})
olympians.SetField("Gods", container.NewDependencyTag("olympians"))
c := container.New()
c.OverrideService("poseidon", poseidon)
c.OverrideService("athena", athena)
c.OverrideService("zeus", zeus)
c.OverrideService("olympians", olympians)
return c
}
func main() {
c := buildContainer()
olympians, _ := c.Get("olympians")
fmt.Printf("%+v\n", olympians)
// Output: {Gods:[{Name:Zeus} {Name:Athena} {Name:Poseidon}]}
}
Overview
Definitions
- Service - any struct, variable, func that you use in your application, e.g.
*sql.DB
. - Parameter - a variable that holds a configuration. E.g. a password can be a parameter.
- Provider - a function that returns one or two values. First return may be of any type. Second return if exists must be of a type error.
- Wither - a method that returns a single value always. Withers in opposition to setters are being used to achieve immutable structures.
Sample providers
See code
func GetPassword() string {
return os.Getenv("PASSWORD")
}
func NewDB(username, password string) (*sql.DB, error) {
return sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/test", username, password))
}
Sample wither
See code
type Person struct {
Name string
}
// WithName is a wither.
func (p Person) WithName(n string) Person {
p.Name = n
return p
}
Scopes
Scopes are applicable to services only. Parameters are not scope-aware, once created parameter is cached forever,
although HotSwap
lets overriding them.
- Shared - once created service is cached forever.
- Contextual - service is cached and shared in the current context only.
The scope is determined by a single invocation of
Get
orGetTaggedBy
, or by thecontext.Context
used in methodsGetInContext
andGetTaggedByInContext
. - NonShared - each invocation of a such service will create a new variable.
- Default - a runtime-determining scope. If the given service has at least one direct or indirect contextual dependency, its scope will be contextual, otherwise it will be shared.
Dependencies
Dependencies describe values we inject to our services.
Value
Hardcoded value. The simplest possible dependency.
container.NewDependencyValue("https://go.dev/")
Tag
Search in the container for all services with the given tag. Sort them by priority first (descending), then by name (alphabetically), and return a slice of them.
container.NewDependencyTag("employee")
Service
It refers to a service with the given id in the container.
container.NewDependencyService("db")
Param
It refers to a param with the given id in the container.
container.NewDependencyParam("db.password")
Provider
A function that is being invoked whenever the given dependency is requested.
container.NewDependencyProvider(func() string {
return os.Getenv("DB_PASSWORD")
})
Container
It refers to the container.
container.NewDependencyContainer()
Services
Creating a new service
Use either SetConstructor
or SetValue
. Constructor MUST be a provider (see definitions).
See code
package main
import (
"fmt"
"github.com/gontainer/gontainer-helpers/v2/container"
)
func main() {
type Person struct {
Name string
}
tonySvc := container.NewService()
tonySvc.SetValue(Person{Name: "Tony"}) // create the service using a value
peterSvc := container.NewService()
peterSvc.SetConstructor(
func(n string) Person { // use a constructor to create a new service
return Person{
Name: n,
}
},
container.NewDependencyValue("Peter"), // inject the value "Peter" to the constructor
)
c := container.New()
c.OverrideService("tony", tonySvc)
c.OverrideService("peter", peterSvc)
tony, _ := c.Get("tony")
peter, _ := c.Get("peter")
fmt.Println(tony, peter)
// Output: {Tony} {Peter}
}
Setter injection
Use AppendCall
.
See code
package main
import (
"fmt"
"github.com/gontainer/gontainer-helpers/v2/container"
)
type Person struct {
Name string
}
func (p *Person) SetName(n string) {
p.Name = n
}
func main() {
s := container.NewService()
s.SetConstructor(func () *Person {
return &Person{} // it must be a pointer, because `SetName` requires a pointer receiver
})
s.AppendCall("SetName", container.NewDependencyValue("Jane"))
c := container.New()
c.OverrideService("jane", s)
jane, _ := c.Get("jane")
fmt.Printf("%+v\n", jane)
// Output: &{Name:Jane}
}
Withers
Use AppendWither
.
See code
package main
import (
"fmt"
"github.com/gontainer/gontainer-helpers/v2/container"
)
type Person struct {
Name string
}
func (p Person) WithName(n string) Person {
p.Name = n
return p
}
func main() {
s := container.NewService()
s.SetValue(Person{})
s.AppendWither("WithName", container.NewDependencyValue("Jane"))
c := container.New()
c.OverrideService("jane", s)
jane, _ := c.Get("jane")
fmt.Printf("%+v\n", jane)
// Output: {Name:Jane}
}
Field injection
Use SetField
or SetFields
.
See code
package main
import (
"fmt"
"github.com/gontainer/gontainer-helpers/v2/container"
)
type Person struct {
name string // unexported fields are supported :)
}
func main() {
s := container.NewService()
s.SetValue(Person{})
s.SetField("name", container.NewDependencyParam("name"))
// alternatively:
// s.SetFields(map[string]container.Dependency{
// "name": container.NewDependencyValue("name"),
// })
c := container.New()
c.OverrideService("jane", s)
c.OverrideParam("name", container.NewDependencyValue("Jane"))
jane, _ := c.Get("jane")
fmt.Printf("%+v\n", jane)
// Output: {name:Jane}
}
Tagging
To tag a service use the function Tag
. The first argument is a tag name, the second one is a priority,
used for sorting services, whenever the given tag is requested.
See code
package main
import (
"fmt"
"github.com/gontainer/gontainer-helpers/v2/container"
)
func main() {
type God struct {
Name string
}
type Gods struct {
Gods []God
}
loki := container.NewService()
loki.SetValue(God{Name: "Loki"})
loki.Tag("norse-god", 0) // tag
thor := container.NewService()
thor.SetValue(God{Name: "Thor"})
thor.Tag("norse-god", 0) // tag
team := container.NewService()
team.SetValue(Gods{})
team.SetField("Gods", container.NewDependencyTag("norse-god")) // inject tagged services
c := container.New()
c.OverrideService("loki", loki)
c.OverrideService("thor", thor)
c.OverrideService("norseGods", team)
norseGods, _ := c.Get("norseGods")
fmt.Println(norseGods)
// Output: {[{Loki} {Thor}]}
}
Scope
To define the scope of the given service, use one of the following methods:
SetScopeDefault
SetScopeShared
SetScopeContextual
SetScopeNonShared
See code
s := container.NewService()
s.SetScopeContextual()
Parameters
Parameters are being registered as dependencies. See dependencies.
See code
package main
import (
"fmt"
"math"
"github.com/gontainer/gontainer-helpers/v2/container"
)
func main() {
c := container.New()
c.OverrideParam("pi", container.NewDependencyValue(math.Pi))
pi, _ := c.GetParam("pi")
fmt.Printf("%.2f\n", pi)
// Output: 3.14
}
Usage
HotSwap
HotSwap lets us gracefully change anything in the container in real time. It waits till all contexts attached to the container are done, then blocks attaching other contexts, and other operations on the container, and modifies the container.
Let's create an HTTP handler that will use the container:
See code
func MyHTTPEndpoint(c *container.Container) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// it creates a new context and guarantees
// that the container won't be modified during that request
// if you use the HotSwap function
ctx := container.ContextWithContainer(r.Context(), c)
r = r.Clone(ctx)
// your code
// ...
})
}
...or even easier, use built-in HTTPHandlerWithContainer
...
var (
h http.Handler
c *container.Container
)
// create your HTTP handler
h = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// your code
// ...
})
// decorate your HTTP handler, this function automatically binds contexts with the container
h = container.HTTPHandlerWithContainer(h, c)
...or override server.Handler
...
var (
s *http.Server
c *container.Container
)
// your code
// ...
s.Handler = container.HTTPHandlerWithContainer(s.Handler, c)
Now we need to change the configuration. Instead of restarting the server, we can use the HotSwap function. E.g.:
See code
// RefreshConfigEveryMinute refreshes the configuration of the container every minute
func RefreshConfigEveryMinute(c *container.Container) {
go func () {
for {
<-time.After(time.Minute)
// HotSwap guarantees atomicity
c.HotSwap(func(c container.MutableContainer) {
// override the value of a param
// the cache for that param is automatically invalidated
c.OverrideParam("my-param", container.NewDependencyValue(125))
// override a service
// the cache for that service is automatically invalidated
db := container.NewService()
db.SetConstructor(
sql.Open,
container.NewDependencyValue("mysql"),
container.NewDependencyParam("dataSourceName"),
)
// invalidate the cache for the given params...
c.InvalidateParamsCache("paramA", "paramB")
// ... or for all of them
c.InvalidateAllParamsCache()
// invalidate the cache for the given service...
c.InvalidateServicesCache("serviceA", "serviceB")
/// or for all of them
c.InvalidateAllServicesCache()
})
}
}()
}
Contextual scope
DB transactions are the best example to explain the benefits of that feature. When we have an HTTP server, transaction usually exists in a scope of the given request. Sometimes we have to inject a transaction into many different structures, and make sure we won't share it with other HTTP requests.
One approach could be to use SetTx
methods:
func (r *MyRepository) SetTx(tx *sql.Tx) {
r.tx = tx
}
That solution is error-prone, you can accidentally inject the same instance of MyRepository
into different requests.
Moreover, when you have many repositories, sometimes even nested ones, injecting *sql.Tx
manually can be difficult.
Container can solve that problem, you need to instruct it only that the given dependency is contextual, and voilà!
See code
// let's wrap the original container by a custom struct.
// it will let us adding custom getters later
type myContainer struct {
*container.Container
}
// let's create the constructor for new transactions
func NewTx(db *sql.DB) (*sql.Tx, error) {
return db.Begin()
}
func buildContainer() *container.Container {
c := container.New()
tx := container.NewService()
tx.SetConstructor(
NewTx,
container.NewDependencyService("db"),
)
/*
Here we instruct the container that the scope of tx is contextual.
By default, the scope of all parent dependencies will be contextual as well.
*/
tx.SetScopeContextual()
c.OverrideService("tx", tx)
/*
We will define NewHTTPHandler in the next step,
now let's register it only in the container
*/
myHandler := container.NewService()
myHandler.SetConstructor(
NewHTTPHandler,
container.NewDependencyContainer(),
)
myHandler.Tag("http-handler", 0)
c.OverrideService("myHandler", myHandler)
// TODO define other dependencies
/*
The following code will automatically wrap all http handlers registered to the container
by func `container.HTTPHandlerWithContainer`.
It is an equivalent of the following raw code:
var handler http.Handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// your code here
})
handler = decorateHTTPHandler(handler, c)
*/
c.AddDecorator(
"http-handler",
decorateHTTPHandler,
container.NewDependencyContainer(),
)
return c
}
func decorateHTTPHandler(p container.DecoratorPayload, c *container.Container) http.Handler {
return container.HTTPHandlerWithContainer(p.Service.(http.Handler), c)
}
Let's build our endpoint now.
See code
type UserRepository struct {
tx *sql.Tx
}
type ImageRepository struct {
tx *sql.Tx
}
// let's define custom getter, they are easier to use
func (c *myContainer) Tx(ctx context.Context) *sql.Tx {
tx, err := c.GetInContext(ctx, "userRepository")
// we expect all services to be defined correctly,
// so we can panic here in case of an error
if err != nil {
panic(err)
}
return tx.(*sql.Tx)
}
func (c *myContainer) UserRepository(ctx context.Context) *UserRepository {
u, err := c.GetInContext(ctx, "userRepository")
if err != nil {
panic(err)
}
return u.(*UserRepository)
}
func (c *myContainer) ImageRepository(ctx context.Context) *ImageRepository {
i, err := c.GetInContext(ctx, "imageRepository")
if err != nil {
panic(err)
}
return i.(*ImageRepository)
}
func NewHTTPHandler(c *myContainer) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// `tx` refers to the same instance that has been injected to `userRepository` and `imageRepository`
var (
tx = c.Tx(r.Context())
userRepository = c.UserRepository(r.Context())
imageRepository = c.ImageRepository(r.Context())
)
var err error
defer func() {
// do not forget about committing or rolling back the transaction
if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
}()
// todo add your logic
})
}
Circular dependencies
Container automatically detects circular dependencies, and returns a proper error.
Do not need to worry about fatal error: stack overflow
:)
See code
package main
import (
"fmt"
"github.com/gontainer/gontainer-helpers/v2/container"
)
type Spouse struct {
Name string
Spouse *Spouse
}
func main() {
wife := container.NewService()
wife.SetConstructor(func() *Spouse {
return &Spouse{}
})
wife.SetField("Name", container.NewDependencyValue("Hera"))
wife.SetField("Spouse", container.NewDependencyService("husband"))
husband := container.NewService()
husband.SetConstructor(func() *Spouse {
return &Spouse{}
})
husband.SetField("Name", container.NewDependencyValue("Zeus"))
husband.SetField("Spouse", container.NewDependencyService("wife"))
c := container.New()
c.OverrideService("wife", wife)
c.OverrideService("husband", husband)
_, err := c.Get("wife")
fmt.Println(err)
// Output: get("wife"): circular dependencies: @husband -> @wife -> @husband
}
Type conversion
In GO assignments between different types requires explicit type conversion. Container automatically converts values for more developer-friendly experience. It supports even a bit more sophisticated conversions of maps and slices, see copier.
See code
type Employee struct {
name string
age uint
}
func buildContainer() *container.Container {
c := container.New()
jane := container.NewService()
jane.SetValue(Employee{})
jane.SetField("name", container.NewDependencyValue("Jane Doe"))
// The following value "53" is of the type "int", although we need an "uint"
// See [Employee.age]
// Container automatically converts values for more developer-friendly experience :)
jane.SetField("age", container.NewDependencyValue(53))
return c
}
Transactions
As an exercise, let's build a small framework that wraps our endpoints and manages transactions automatically.
ErrorAwareHTTPHandler
A new type for our endpoints that informs us whether an error has occurred.
See code
type ErrorAwareHTTPHandler interface {
Handle(http.ResponseWriter, *http.Request) error
}
type ErrorAwareHTTPHandlerFunc func(http.ResponseWriter, *http.Request) error
func (e ErrorAwareHTTPHandlerFunc) Handle(w http.ResponseWriter, r *http.Request) error {
return e(w, r)
}
WrapTransactionHandler
A small wrapper over the newly created type that implements http.Handler
interface,
and automatically handles transactions.
See code
func WrapTransactionHandler(h ErrorAwareHTTPHandler, c *myContainer) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// create a copy of the current request, and inject a container aware context
ctx := container.ContextWithContainer(r.Context(), c)
r = r.Clone(ctx)
tx := c.Tx()
// do not forget about rolling back when your code panics
defer func() {
rec := recover()
if rec != nil {
_ = tx.Rollback()
panic(rec)
}
}()
err := h.Handle(w, r)
if err != nil {
_ = tx.Rollback()
} else {
_ = tx.Commit()
}
})
}
Our first endpoint
See code
func myHTTPHandler(h http.ResponseWriter, r *http.Request) error {
// todo some logic
return errors.New("unexpected error")
}
The final container
Let's connect all the dots.
See code
type myContainer struct {
*container.Container
}
func (m *myContainer) Tx() *sql.Tx {
tx, err := m.Get("tx")
if err != nil {
panic(err)
}
return tx.(*sql.Tx)
}
func buildContainer() *myContainer {
c := &myContainer{container.New()}
// decorate all services tagged by "http-errors"
c.AddDecorator(
"http-errors",
func(p container.DecoratorPayload) http.Handler {
return WrapTransactionHandler(
p.Service.(ErrorAwareHTTPHandler),
c,
)
},
)
// define your error aware http handler
myHandler := container.NewService()
myHandler.SetValue(ErrorAwareHTTPHandlerFunc(myHTTPHandler))
myHandler.Tag("http-errors", 0)
c.OverrideService("myHandler", myHandler)
// I assume we may need more endpoints later,
// let's use the built-in multiplexer [http.ServeMux]
m := container.NewService()
m.SetConstructor(http.NewServeMux)
m.AppendCall(
"Handle",
container.NewDependencyValue("/my-error-aware-endpoint"),
container.NewDependencyService("myHandler"),
)
return c
}
Errors
This package aims to be as developer-friendly as possible.
To ease the debugging process all errors are as descriptive as possible.
Sometimes you may have more than a single reason why something does not work,
so whenever it is possible you get a multiline error.
Multiline error is a collection of few independent errors.
You can extract them using grouperror.Collection
, see grouperror.
See code
package main
import (
"fmt"
"github.com/gontainer/gontainer-helpers/v2/container"
)
func main() {
janeDoe := container.NewService()
janeDoe.SetValue(
struct {
name string
}{},
)
janeDoe.SetField("firstname", container.NewDependencyValue("Jane"))
janeDoe.SetField("lastname", container.NewDependencyValue("Doe"))
c := container.New()
c.OverrideService("janeDoe", janeDoe)
_, err := c.Get("janeDoe")
fmt.Println(err)
// Output:
// get("janeDoe"): set field "firstname": set (*interface {})."firstname": field "firstname" does not exist
// get("janeDoe"): set field "lastname": set (*interface {})."lastname": field "lastname" does not exist
}
Examples
See examples.
Code generation
The entire code can be built using a YAML-configuration files. See gontainer/gontainer.