204 lines
5.9 KiB
Markdown
204 lines
5.9 KiB
Markdown
|
|
# goose: Compile-Time Dependency Injection for Go
|
|||
|
|
|
|||
|
|
goose is a compile-time [dependency injection][] framework for Go, inspired by
|
|||
|
|
[Dagger][]. It works by using Go code to specify dependencies, then
|
|||
|
|
generating code to create those structures, mimicking the code that a user
|
|||
|
|
might have hand-written.
|
|||
|
|
|
|||
|
|
[dependency injection]: https://en.wikipedia.org/wiki/Dependency_injection
|
|||
|
|
[Dagger]: https://google.github.io/dagger/
|
|||
|
|
|
|||
|
|
## Usage Guide
|
|||
|
|
|
|||
|
|
### Defining Providers
|
|||
|
|
|
|||
|
|
The primary mechanism in goose is the **provider**: a function that can
|
|||
|
|
produce a value, annotated with the special `goose:provide` directive. These
|
|||
|
|
functions are ordinary Go code and live in packages.
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package module
|
|||
|
|
|
|||
|
|
type Foo int
|
|||
|
|
|
|||
|
|
// goose:provide
|
|||
|
|
|
|||
|
|
// ProvideFoo returns a Foo.
|
|||
|
|
func ProvideFoo() Foo {
|
|||
|
|
return 42
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Providers are always part of a **module**: if there is no module name specified
|
|||
|
|
on the `//goose:provide` line, then `Module` is used.
|
|||
|
|
|
|||
|
|
Providers can specify dependencies with parameters:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package module
|
|||
|
|
|
|||
|
|
// goose:provide SuperModule
|
|||
|
|
|
|||
|
|
type Bar int
|
|||
|
|
|
|||
|
|
// ProvideBar returns a Bar: a negative Foo.
|
|||
|
|
func ProvideBar(foo Foo) Bar {
|
|||
|
|
return Bar(-foo)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Providers can also return errors:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package module
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"errors"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type Baz int
|
|||
|
|
|
|||
|
|
// goose:provide SuperModule
|
|||
|
|
|
|||
|
|
// ProvideBaz returns a value if Bar is not zero.
|
|||
|
|
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
|
|||
|
|
if bar == 0 {
|
|||
|
|
return 0, errors.New("cannot provide baz when bar is zero")
|
|||
|
|
}
|
|||
|
|
return Baz(bar), nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Modules can import other modules. To import `Module` in `SuperModule`:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// goose:import SuperModule Module
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Injectors
|
|||
|
|
|
|||
|
|
An application can use these providers by declaring an **injector**: a
|
|||
|
|
generated function that calls providers in dependency order.
|
|||
|
|
|
|||
|
|
An injector is declared by writing a function declaration without a body in a
|
|||
|
|
file guarded by a `gooseinject` build tag. Let's say that the above providers
|
|||
|
|
were defined in a package called `example.com/module`. The following would
|
|||
|
|
declare an injector to obtain a `Baz`:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
//+build gooseinject
|
|||
|
|
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
|
|||
|
|
"example.com/module"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// goose:use module.SuperModule
|
|||
|
|
|
|||
|
|
func initializeApp(ctx context.Context) (module.Baz, error)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Like providers, injectors can be parameterized on inputs (which then get sent to
|
|||
|
|
providers) and can return errors. The `goose:use` directive specifies the
|
|||
|
|
modules to use in the injection. Both `goose:use` and `goose:import` use the
|
|||
|
|
same syntax for referencing modules: an optional import qualifier (either a
|
|||
|
|
package name or a quoted import path) with a dot, followed by the module name.
|
|||
|
|
For example: `SamePackageModule`, `foo.Bar`, or `"example.com/foo".Bar`.
|
|||
|
|
|
|||
|
|
You can generate the injector using goose:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
goose
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Or you can add the line `//go:generate goose` to another file in your package to
|
|||
|
|
use [`go generate`]:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
go generate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
(Adding the line to the injection declaration file will be silently ignored by
|
|||
|
|
`go generate`.)
|
|||
|
|
|
|||
|
|
goose will produce an implementation of the injector that looks something like
|
|||
|
|
this:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// Code generated by goose. DO NOT EDIT.
|
|||
|
|
|
|||
|
|
//+build !gooseinject
|
|||
|
|
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"example.com/module"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func initializeApp(ctx context.Context) (module.Baz, error) {
|
|||
|
|
foo := module.ProvideFoo()
|
|||
|
|
bar := module.ProvideBar(foo)
|
|||
|
|
baz, err := module.ProvideBaz(ctx, bar)
|
|||
|
|
if err != nil {
|
|||
|
|
return 0, err
|
|||
|
|
}
|
|||
|
|
return baz, nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
As you can see, the output is very close to what a developer would write
|
|||
|
|
themselves. Further, there is no dependency on goose at runtime: all of the
|
|||
|
|
written code is just normal Go code, and can be used without goose.
|
|||
|
|
|
|||
|
|
[`go generate`]: https://blog.golang.org/generate
|
|||
|
|
|
|||
|
|
## Best Practices
|
|||
|
|
|
|||
|
|
goose is still not mature yet, but guidance that applies to Dagger generally
|
|||
|
|
applies to goose as well. In particular, when thinking about how to group a
|
|||
|
|
package of providers, follow the same [guidance](https://google.github.io/dagger/testing.html#organize-modules-for-testability) as Dagger:
|
|||
|
|
|
|||
|
|
> Some [...] bindings will have reasonable alternatives, especially for
|
|||
|
|
> testing, and others will not. For example, there are likely to be
|
|||
|
|
> alternative bindings for a type like `AuthManager`: one for testing, others
|
|||
|
|
> for different authentication/authorization protocols.
|
|||
|
|
>
|
|||
|
|
> But on the other hand, if the `AuthManager` interface has a method that
|
|||
|
|
> returns the currently logged-in user, you might want to [export a provider of
|
|||
|
|
> `User` that simply calls `CurrentUser()`] on the `AuthManager`. That
|
|||
|
|
> published binding is unlikely to ever need an alternative.
|
|||
|
|
>
|
|||
|
|
> Once you’ve classified your bindings into [...] bindings with reasonable
|
|||
|
|
> alternatives [and] bindings without reasonable alternatives, consider
|
|||
|
|
> arranging them into packages like this:
|
|||
|
|
>
|
|||
|
|
> - One [package] for each [...] binding with a reasonable alternative. (If
|
|||
|
|
> you are also writing the alternatives, each one gets its own [package].) That
|
|||
|
|
> [package] contains exactly one provider.
|
|||
|
|
> - All [...] bindings with no reasonable alternatives go into [packages]
|
|||
|
|
> organized along functional lines.
|
|||
|
|
> - The [packages] should each include the no-reasonable-alternative [packages] that
|
|||
|
|
> require the [...] bindings each provides.
|
|||
|
|
|
|||
|
|
One goose-specific practice though: create one-off types where in Java you
|
|||
|
|
would use a binding annotation.
|
|||
|
|
|
|||
|
|
## Future Work
|
|||
|
|
|
|||
|
|
- The names of imports and provider results in the generated code are not
|
|||
|
|
actually as nice as shown above. I'd like to make them nicer in the
|
|||
|
|
common cases while ensuring uniqueness.
|
|||
|
|
- I'd like to support optional and multiple bindings.
|
|||
|
|
- At the moment, the entire transitive closure of all dependencies are read
|
|||
|
|
for providers. It might be better to have provider imports be opt-in, but
|
|||
|
|
that seems like too many levels of magic.
|
|||
|
|
- Currently, all dependency satisfaction is done using identity. I'd like to
|
|||
|
|
use a limited form of assignability for interface types, but I'm unsure
|
|||
|
|
how well this implicit satisfaction will work in practice.
|
|||
|
|
- Errors emitted by goose are not very good, but it has all the information
|
|||
|
|
it needs to emit better ones.
|