kanna-di writes the constructor of a container struct. Each field gets its value from a provider — the function next to the field's type, or one next to the container — and the providers those need in turn, called in order in plain Go.
Part of the kanna family of code generators. Requires Go 1.26. There is nothing to import at run time: the generated code calls your own functions and nothing else.
Install
go get -tool github.com/go-kanna/di/cmd/kanna-diUse
Take an application in layers: the domain declares a repository and a mail sender, use cases depend on them, the infrastructure implements them, and handlers call the use cases. Each layer keeps its constructors next to its types:
// package user, the domain
type Repository interface {
Save(ctx context.Context, u User) error
Find(ctx context.Context, id ID) (User, error)
}
// package mail, the domain
type Sender interface {
Send(ctx context.Context, m Message) error
}
// package usecase
func NewRegisterUser(repo user.Repository, mailer mail.Sender) *RegisterUser
func NewGetUser(repo user.Repository) *GetUser
// package postgres, the infrastructure
func NewUserRepository(db *sql.DB) *UserRepository
// package sendgrid, an external API
func NewSender(cfg config.Config) *Sender
// package handler
func NewUser(registerUser *usecase.RegisterUser, getUser *usecase.GetUser) *User
// package config
func Load() (Config, error)The composition root already has a struct that holds what the program is made of — the server, with its handlers and
its routes. Mark it //kanna:container. What only this root can decide goes next to it: which implementation serves
each of the domain's interfaces, and how to open what the standard library provides.
package main
//go:generate go tool kanna-di
// server serves the shop's HTTP API.
//
//kanna:container
type server struct {
_ user.Repository `di:"with=postgres.NewUserRepository"`
_ mail.Sender `di:"with=sendgrid.NewSender"`
users *handler.User
}
func (s *server) routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("POST /users", s.users.Register)
mux.HandleFunc("GET /users/{id}", s.users.Get)
return mux
}
func openDB(cfg config.Config) (*sql.DB, func(), error) {
db, err := sql.Open("pgx", cfg.DSN)
if err != nil {
return nil, nil, err
}
return db, func() { db.Close() }, nil
}go generate ./... writes di_gen.go next to the container:
// Code generated by kanna-di. DO NOT EDIT.
package main
import (
"example.com/shop/internal/config"
"example.com/shop/internal/handler"
"example.com/shop/internal/infra/postgres"
"example.com/shop/internal/infra/sendgrid"
"example.com/shop/internal/usecase"
)
// newServer initializes dependencies and constructs server.
func newServer() (*server, func(), error) {
cfg, err := config.Load()
if err != nil {
return nil, nil, err
}
db, cleanup, err := openDB(cfg)
if err != nil {
return nil, nil, err
}
repo := postgres.NewUserRepository(db)
mailer := sendgrid.NewSender(cfg)
registerUser := usecase.NewRegisterUser(repo, mailer)
getUser := usecase.NewGetUser(repo)
users := handler.NewUser(registerUser, getUser)
return &server{
users: users,
}, cleanup, nil
}func main() {
s, cleanup, err := newServer()
if err != nil {
log.Fatal(err)
}
defer cleanup()
log.Print(http.ListenAndServe(":8080", s.routes()))
}*usecase.RegisterUser comes from usecase.NewRegisterUser, the function next to its type; *sql.DB from openDB,
next to the container; user.Repository and mail.Sender, the domain's interfaces, from the implementations the
container names. config.Config is loaded once, and openDB and sendgrid.NewSender share it. Nothing is registered.
When a constructor's parameters change, the next go generate changes the code that calls it.
Providers
A provider is a top-level function whose first result is a type declared in a package — a struct, an interface, or any other named type — or a pointer to one:
| Results | The constructor |
|---|---|
T |
|
T, error |
returns an error too, and passes this one on as is |
T, func() |
returns a cleanup too; see Cleanup |
T, func(), error |
both |
A variadic parameter is left empty. A provider is called once per constructor, and everything that needs its result
shares it. A string or a []byte is never looked up, so that a function returning one for its own reasons is not
taken for a provider; give such a value a type of its own, such as type DSN string, or take it with
di:"arg=<name>". A must variant beside the function it wraps, MustNewDeps beside NewDeps, is passed over for that
function, so that the error reaches the constructor instead of a panic.
For each type it needs, kanna-di takes the first of these that has one:
- an argument of the constructor (
di:"arg") - the provider the container chooses for the type (
di:"with=...") - what the container embeds: an exported field of a struct it embeds or declares as
_, or thewith=of a container it builds in place (see Containers) - a function in the container's package, exported or not
- an exported function in the package declaring the type, when that package is in your module or your go.work
Finding none stops generation, and so do two at the same level. The error traces the need back to the field:
$ go generate
./server.go:11:2: error: nothing provides mail.Sender, which usecase.NewRegisterUser needs
via server.users → handler.NewUser → usecase.NewRegisterUser
return one from a function in package main, take it with di:"arg", or choose a provider with di:"with=..."
./server.go:11:2: error: 2 functions in package usecase return *usecase.GetUser
usecase.NewGetUser (../../internal/usecase/get_user.go:12:6)
usecase.NewCachedGetUser (../../internal/usecase/get_user.go:30:6)
via server.users → handler.NewUser
choose one with _ *usecase.GetUser `di:"with=usecase.NewGetUser"`
./server.go:11:2: error: dependency cycle: usecase.NewRegisterUser → mail.NewWelcome → usecase.NewRegisterUser
via server.users → handler.NewUserA function anywhere else is used only when the container names it. A helper in a testutil package, or a fixture
returning the same type, changes nothing about what the container gets. Nor are the standard library and other modules
searched: the only function of io that returns an io.Writer is MultiWriter, which, called with nothing, writes
nowhere. For such a type, write a function next to the container — most need configuring anyway, as sql.Open needs a
DSN — or choose one with with=.
Choosing a provider
di:"with=<ref>" chooses the provider of the field's type, for the whole container:
<ref> |
Names |
|---|---|
NewUserRepository |
a function in the container's package |
postgres.NewUserRepository |
a function in the package named postgres, in your module or the one declaring the type |
infra/postgres.NewUserRepository |
the same, with as much of the import path as it takes to tell two such packages apart |
The provider's result only has to be assignable to the type, which is how an interface gets its implementation. On a
named field, with= also stores the value; on _, it only chooses, and a struct's fields then serve the container (see
Containers).
./server.go:9:2: error: package postgres has no function NewUserRepositry (field _)
did you mean NewUserRepository?
./server.go:9:2: error: postgres.NewOrderRepository returns *postgres.OrderRepository, which does not implement user.Repository (missing method Find) (field _)Containers
//kanna:container marks a struct, and every field of it is resolved unless its tag says otherwise:
| Field | Named | _ |
|---|---|---|
| no tag | resolve the field's type and store the value | a struct with exported fields: resolve it, store nothing; else error |
di:"with=<ref>" |
resolve it with ref, which serves the type in the whole container | choose ref for the type, store nothing |
di:"arg" |
take it as a parameter, named after its type, and store it | take it as a parameter only |
di:"arg=<name>" |
the same, with the parameter's name spelled out | the same, with the parameter's name |
di:"-" |
leave the field alone | error |
A named or embedded field without a tag whose type is a struct with no fields, not a pointer to one, is left at its zero
value, the only value it has: a gRPC server embedding todov1.UnimplementedTodoServiceServer needs no provider for it.
A parameter keeps its name, and an import the generated file needs under that name is renamed instead; a name spelled out that the package declares at the top level, or that Go does, is an error.
What the container takes or chooses but nothing needs draws a warning:
./server.go:12:2: warning: nothing in server needs the context.Context it takes with di:"arg"
./server.go:9:2: warning: nothing in server needs user.Repository, so its with= chooses nothingAn embedded struct, or pointer to one, is resolved — or, tagged di:"arg", taken as a parameter — and stored like any
field, and its exported fields, promoted ones included, serve the rest of the container; they cannot serve what the
struct itself is built from. A shallower field hides a deeper one of the same type, as in Go; two at the same depth are
an error when the type is needed.
//kanna:container
type App struct {
infra.Deps `di:"arg"` // its DB and Cache serve the providers of Users
Users *handler.User
}A struct, or pointer to one, in a _ field serves the container the same way without being stored, so the container
gets neither the struct nor the fields and methods embedding would promote onto it. It is resolved when it, or one of
its fields, is needed: without a tag as any field is, or taken or chosen with di:"arg" or with=. A use case draws on
what the infrastructure bundles this way:
//kanna:container
type DeleteTodo struct {
_ *infra.Deps // infra.NewDeps provides it; its Transactor and Todos fill the fields below
transactor tx.Transactor
todos repository.Todo
}func NewDeleteTodo() (*DeleteTodo, func(), error) {
deps, cleanup, err := infra.NewDeps()
if err != nil {
return nil, nil, err
}
transactor := deps.Transactor
todos := deps.Todos
return &DeleteTodo{
transactor: transactor,
todos: todos,
}, cleanup, nil
}A container of the same package embedded without a tag — a struct marked //kanna:container itself — is not built by
its own constructor but here, field by field, as part of this one. What it takes as arguments, this constructor takes
too, unless it takes the type itself; its with= serve this container after this container's own. That is how a test
takes a server apart without restating it (see Tests). A container of another package is built by its
constructor instead, since its unexported fields cannot be set from here, and so is a named field of a container type.
The directive takes options:
| Directive | Effect |
|---|---|
//kanna:container name=<ident> |
name the constructor (default: New and the struct's name, or new and it for an unexported struct) |
//kanna:container returns=<type> |
declare what the constructor returns: the struct, to return it by value, or an interface it implements (default: a pointer) |
//kanna:container must |
also write MustNewServer (mustNewServer for newServer), which panics instead of returning an error |
A generic struct cannot be a container: its constructor would have no way to choose the type arguments. A struct with
di tags but no directive gets a warning, as does a misspelled directive.
Write the directive against the comment marker. Go only treats //kanna:… as a directive when the two are adjacent;
with a space, // kanna:container stays part of the doc comment and shows up in go doc and on pkg.go.dev. kanna-di
does not honor that form either, and points out the spelling it expected.
Cleanup
A provider that opens something returns how to close it, and the constructor returns one cleanup for all of them:
func newServer() (*server, func(), error) {
// ...
db, cleanup, err := openDB(cfg)
if err != nil {
return nil, nil, err
}
cache, cleanup2, err := openCache(cfg)
if err != nil {
cleanup()
return nil, nil, err
}
// ...
cleanupAll := func() {
cleanup2()
cleanup()
}
return &server{
// ...
}, cleanupAll, nil
}The cleanups run in the reverse of the order their providers ran in. When a provider fails, the cleanups of those that
ran before it are run before the error is returned. The must variant returns the cleanup as well. What the constructor
takes as an argument, or finds in a struct it embeds or declares as _, belongs to the caller and is never closed;
neither is anything for implementing io.Closer.
Tests
A container declared in a _test.go file is generated too, into di_gen_test.go next to it, in the package that file
declares — the package itself or its external _test package. This one, in the package itself, embeds the server, so
it is the server, built here with the repository and the sender taken as arguments instead of chosen with with=.
Nothing then needs *sql.DB or config.Config: openDB and config.Load are not called, and newTestServer returns
neither a cleanup nor an error.
//kanna:container
type testServer struct {
*server
_ user.Repository `di:"arg"`
_ mail.Sender `di:"arg"`
}
func TestRegisterUser(t *testing.T) {
s := newTestServer(memory.NewUserRepository(), &fakeSender{})
srv := httptest.NewServer(s.routes())
defer srv.Close()
// ...
}The test calls newTestServer before the first run has written it. kanna-di type-checks the packages it loads once
more with what it is about to write in place, so a call to a constructor that is missing still, or about to take or
return something else, does not stop it. What is wrong then is reported, and nothing is written: a call the constructor
as written does not match, or a mistake the missing constructor hid, such as a method that what it returns does not
have.
The functions of a test container's package include those of its _test.go files. A provider declared in a _test.go
file is compiled into that package's tests and nowhere else, so it serves the containers of that package's tests,
in-package and external, and no other. One directory can hold test containers in either of its two test packages, not
both: they would share di_gen_test.go.
Flags
kanna-di [flags] [packages]| Flag | Meaning |
|---|---|
-tags <list> |
comma-separated build tags |
-check |
verify the output is up to date instead of writing it, including that no output is left over |
-v |
list the files written, removed, or left unchanged |
-version |
print the version |
The packages say where the containers are, not where providers come from, and default to the one in the current
directory, so //go:generate go tool kanna-di needs no argument. A run that succeeds prints nothing but its warnings.
A package whose structs have all lost //kanna:container has its di_gen.go and di_gen_test.go removed, unless code
in the packages the run loads still calls their constructors; a file of either name without the generated-code header
is never touched. The go generate that removes one ends with open ...: no such file or directory, from go generate
itself, which lists a package's files before it runs the generators; the file is gone as it should be
(golang/go#36068).
Example
example wires a small shop in the layers above — domain, use cases, infrastructure, handlers — finding its
providers through bindings to interfaces, an argument, and an embedded container, with cleanups, a must variant, and
a test container that swaps the repository for one in memory. CI regenerates it and fails if the output would change.
Development
make test # unit tests
make lint # golangci-lint
make example # regenerate the example, then vet, test, and run it