Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
go run ./explain
go run ./optional
go run ./observe
go run ./factory
- name: test
run: go test -race -count=1 -coverprofile=coverage.out ./...
- name: what only the hand-written tests reach
Expand Down
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,81 @@ not in the picture. A complete service with a worker, a health endpoint,
request scopes and graceful shutdown is in
[`examples/app`](examples/app/main.go).

#### Factories

There is no transient lifetime. A value made fresh on every use is a
function, registered like any other service and asked for like one:

[embedmd]:# (examples/factory/main.go go)
```go
// Factories: a value made fresh on every use is a function, and one that
// also needs a lifecycle is a scope.
package main

import (
"context"
"fmt"

"github.com/floatdrop/di"
)

type Config struct{ Prefix string }

// The function's type is the key, so it is named: two factories with the
// same signature would otherwise collide, and the name is what Explain shows
// and what a wired constructor asks for.
type NewID func(n int) string

type Job struct{ id string }

func NewIDs(cfg Config) NewID {
return func(n int) string { return fmt.Sprintf("%s-%d", cfg.Prefix, n) }
}
func NewJob(newID NewID) *Job { return &Job{id: newID(3)} }
func release(_ context.Context, j *Job) error { fmt.Println("released", j.id); return nil }

func main() {
app := di.New()
app.Value(Config{Prefix: "job"})
app.Wire[NewID](NewIDs)
// A value with a lifecycle of its own is Scoped, resolved from a child
// scope opened for one unit of work and stopped with it.
app.Wire[*Job](NewJob).Scoped().OnStop(release)

// The container built the factory once and never sees what it makes;
// the values are the caller's.
newID := app.Get[NewID]()
fmt.Println(newID(1), newID(2))

work := app.Child("work")
fmt.Println("running", work.Get[*Job]().id)
_ = work.Stop(context.Background())
}
```

```
job-1 job-2
running job-3
released job-3
```

The function's type is the key, so it is named. Two factories with the same
signature would otherwise collide, and `NewID` is what `Explain` shows and
what `NewJob` asks for. The container builds the factory once, with
dependencies and hooks of its own, and never sees what it makes: those values
are the caller's to release, as a `*sql.Conn` from `sql.DB.Conn` is, or the
factory keeps what it made and its `OnStop` releases it, which is what a pool
is.

A value that needs a lifecycle of its own is a scope's, not a factory's.
Mark it `Scoped()`, resolve it from a child opened for one unit of work, and
stop the child when the work ends. The container then drains it, stops it in
order and shows it in `Explain`. The cost is a scope per unit of work rather
than a call, so it is for values worth tracking. samber/do has
`ProvideTransient`, whose instances the container does not track either, so a
function here is the same thing said plainly; uber/fx has no transient at all
and uses the same function.

#### Groups

`Group()` makes a registration one member of the group for its type instead
Expand Down
43 changes: 43 additions & 0 deletions examples/factory/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Factories: a value made fresh on every use is a function, and one that
// also needs a lifecycle is a scope.
package main

import (
"context"
"fmt"

"github.com/floatdrop/di"
)

type Config struct{ Prefix string }

// The function's type is the key, so it is named: two factories with the
// same signature would otherwise collide, and the name is what Explain shows
// and what a wired constructor asks for.
type NewID func(n int) string

type Job struct{ id string }

func NewIDs(cfg Config) NewID {
return func(n int) string { return fmt.Sprintf("%s-%d", cfg.Prefix, n) }
}
func NewJob(newID NewID) *Job { return &Job{id: newID(3)} }
func release(_ context.Context, j *Job) error { fmt.Println("released", j.id); return nil }

func main() {
app := di.New()
app.Value(Config{Prefix: "job"})
app.Wire[NewID](NewIDs)
// A value with a lifecycle of its own is Scoped, resolved from a child
// scope opened for one unit of work and stopped with it.
app.Wire[*Job](NewJob).Scoped().OnStop(release)

// The container built the factory once and never sees what it makes;
// the values are the caller's.
newID := app.Get[NewID]()
fmt.Println(newID(1), newID(2))

work := app.Child("work")
fmt.Println("running", work.Get[*Job]().id)
_ = work.Stop(context.Background())
}
Loading