Got provides a declarative way of composing dependencies. It integrates nicely with the common constructor patterns in Go applications.
The main advantage of got is removing the need to manually pass references around when instantiating types that depend on one another.
go get github.com/eriicafes/gotgot has a very small API surface. You typically only need to use two functions from this library.
Constructor return values are cached the first time they are requested and future calls return the cached value instead of re-running the constructor. See Transient constructors to opt out of caching.
Constructors are global variables with their names prefixed with
Getas a convention.
// printer.go
package main
import "github.com/eriicafes/got"
type Printer interface {
Print(string) string
}
var GetPrinter = got.Using(func(c *got.Container) Printer {
return &CapsPrinter{}
})
// CapsPrinter is an implementation of Printer interface
type CapsPrinter struct{}
func (*CapsPrinter) Print(s string) string {
return strings.ToUpper(s)
}Retrieve an instance from a constructor by calling GetXXX.From(container)
// office.go
package main
import "github.com/eriicafes/got"
type Office struct {
Printer Printer
}
var GetOffice = got.Using(func(c *got.Container) *Office {
return &Office{
Printer: GetPrinter.From(c),
}
})Create a container to hold cached instances.
When calling a constructor, any dependencies it has will also be cached, ensuring that shared dependencies use the same instance.
// main.go
package main
import "github.com/eriicafes/got"
func main() {
c := got.New()
office := GetOffice.From(c)
// or using the From function from got
office := got.From(c, GetOffice)
office.Printer.Print()
}Constructors can use a container's context while initializing cached dependencies.
got.New() uses context.Background(). To provide a context explicitly, create the
container with got.NewContext(context) and retrieve it in a constructor with
container.Context().
// main.go
package main
import (
"context"
"github.com/eriicafes/got"
)
var GetClient = got.Using(func(container *got.Container) *Client {
return NewClient(container.Context())
})
func main() {
container := got.NewContext(context.Background())
client := GetClient.From(container)
_ = client
}Transient constructors create a new instance each time it is requested.
Use got.NewUsing to create a transient constructor. Its From method does not cache
the returned value. You can also call any constructor's New method directly to create
a value without caching it.
// office.go
package main
import "github.com/eriicafes/got"
type Office struct {
Printer Printer
}
var NewPrinter = got.NewUsing(func(c *got.Container) Printer {
return &CapsPrinter{}
})
var GetOffice = got.Using(func(c *got.Container) *Office {
return &Office{
// a new printer is created each time
Printer: NewPrinter.From(c),
}
})Use got.TryUsing for constructors that return a value and an error. Use
got.TryNewUsing when the constructor should be transient.
// bad_office.go
package main
import "github.com/eriicafes/got"
var GetBadOffice = got.TryUsing(func(c *got.Container) (*Office, error) {
return nil, fmt.Errorf("failed to create office")
})Use got.Using2 for other constructors that return two values. Use got.NewUsing2
when the constructor should be transient.
var GetHTTPAddress = got.Using2(func(c *got.Container) (string, int) {
return "127.0.0.1", 8080
})Call container.Clear() to remove every cached constructor result. Call
container.ClearErrors() to remove only results whose second value is a non-nil error, allowing failed constructors to be retried.
You can mock a constructor using got.Mock or got.Mock2.
// main.go
package main
import "github.com/eriicafes/got"
type MockPrinter struct{}
var GetMockPrinter = got.Using(func(c *got.Container) Printer {
return &MockPrinter{}
})
func (*MockPrinter) Print(s string) string {
return fmt.Sprintf("mocked %s", s)
}
func main() {
mc := got.New()
got.Mock(mc, GetPrinter, GetMockPrinter.New(mc))
office := GetOffice.From(mc)
office.Printer.Print()
}Go prevents you from creating circular dependencies as long as you maintain the convention and use global vars as constructors.
Always declare constructors as top-level vars rather than assigning them lazily, otherwise circular dependencies can go undetected and deadlock.