
Changing ecosystems is never easy. So imagine a company like M6 Web, with more than 15 years of PHP experience, being offered to abandon Symfony overnight and switch to Go.
Having worked with Symfony 1 for many years myself, I must admit I became addicted to its dependency injection system: it’s simple to get started with, perfectly documented, and thanks to autowiring, its usage becomes almost transparent. It’s so convenient that we forget all the time saved daily.
That’s why, when I started thinking about this language change with my team, we started from a clear observation: regardless of the direction we would take, we first needed to choose a dependency injection solution. Everything else would naturally graft onto it.
It was with this in mind that we decided to explore FX 2, a dependency injection solution developed by Uber.
FX is a library very rich in features, but to summarize, it has two main components:
graph TD
subgraph dep [Dependency Management]
A[fx.Provide] -->|Builds| B[Dependency Graph]
B -->|Can be modified| C[fx.Decorate]
B -->|Resolves and injects| D[fx.Invoke]
end
subgraph cycle [Lifecycle Management]
E[fx.Lifecycle] -->|Manage| F[fx.Hook]
F -->|OnStart| G[Service Startup]
F -->|OnStop| H[Clean Service Shutdown]
end
D -->|Starts via| E
Let’s start with a simple Doer interface.
1package main
2
3import (
4 "fmt"
5
6 "go.uber.org/fx"
7)
8
9type Doer interface {
10 Do()
11}
12
13var _ Doer = (*DoerImpl)(nil)
14
15type DoerImpl struct{}
16
17func NewDoerImpl() Doer {
18 return &DoerImpl{}
19}
20
21func (d DoerImpl) Do() {
22 fmt.Println(">>>>> DoerImpl.Do()")
23}
24
25func main() {
26 fx.New(
27 fx.Provide(NewDoerImpl),
28 ).Run()
29}
Here NewDoerImpl is registered and available in the dependency graph.
go run ./main.go
[Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1()
[Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
[Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
[Fx] PROVIDE *main.DoerImpl <= main.NewDoerImpl()
[Fx] RUNNING
>>>>> DoerImpl.Do() is not displayed however because the structure is indeed instantiated but never invoked.
1package main
2
3import (
4 "fmt"
5
6 "go.uber.org/fx"
7)
8
9type Doer interface {
10 Do()
11}
12
13var _ Doer = (*DoerImpl)(nil)
14
15type DoerImpl struct{}
16
17func NewDoerImpl() Doer {
18 return &DoerImpl{}
19}
20
21func (d DoerImpl) Do() {
22 fmt.Println(">>>>> DoerImpl.Do()")
23}
24
25func DoerFunc(doer Doer) {
26 doer.Do()
27}
28
29func main() {
30 fx.New(
31 fx.Provide(NewDoerImpl),
32 fx.Invoke(DoerFunc),
33 ).Run()
34}
Here DoerFunc consumes the DoerImpl dependency provided by fx.Provide.
go run ./main.go
[Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1()
[Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
[Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
[Fx] PROVIDE main.Doer <= main.NewDoerImpl()
[Fx] INVOKE main.DoerFunc()
[Fx] BEFORE RUN provide: main.NewDoerImpl()
[Fx] RUN provide: main.NewDoerImpl() in 58.708µs
>>>>> DoerImpl.Do()
[Fx] RUNNING
>>>>> DoerImpl.Do() is displayed this time.
We will now decorate our first Doer implementation.
1package main
2
3import (
4 "fmt"
5
6 "go.uber.org/fx"
7)
8
9type Doer interface {
10 Do()
11}
12
13var _ Doer = (*DoerImpl)(nil)
14
15type DoerImpl struct{}
16
17func NewDoerImpl() Doer {
18 return &DoerImpl{}
19}
20
21func (d DoerImpl) Do() {
22 fmt.Println(">>>>> DoerImpl.Do()")
23}
24
25var _ Doer = (*DecorationForDoerImpl)(nil)
26
27type DecorationForDoerImpl struct {
28 inner Doer
29}
30
31func NewDecorationForDoerImpl(inner Doer) Doer {
32 return &DecorationForDoerImpl{inner: inner}
33}
34
35func (d DecorationForDoerImpl) Do() {
36 fmt.Println(">>>>> DecorationForDoerImpl.Do()")
37 d.inner.Do()
38}
39
40func DoerFunc(doer Doer) {
41 doer.Do()
42}
43
44func main() {
45 fx.New(
46 fx.Provide(NewDoerImpl),
47 fx.Decorate(NewDecorationForDoerImpl),
48 fx.Invoke(DoerFunc),
49 ).Run()
50}
fx.Decorate replaces the Config value with a modified version.
go run ./main.go
[Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1()
[Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
[Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
[Fx] PROVIDE main.Doer <= main.NewDoerImpl()
[Fx] DECORATE main.Doer <= main.NewDecorationForDoerImpl()
[Fx] INVOKE main.DoerFunc()
[Fx] BEFORE RUN provide: main.NewDoerImpl()
[Fx] RUN provide: main.NewDoerImpl() in 168.167µs
[Fx] BEFORE RUN decorate: main.NewDecorationForDoerImpl()
[Fx] RUN decorate: main.NewDecorationForDoerImpl() in 3.292µs
>>>>> DecorationForDoerImpl.Do()
>>>>> DoerImpl.Do()
[Fx] RUNNING
Here we’re going to test the OnStart and OnStop hooks and for that, we’re going to slightly modify our program because Run() waits for a stop signal (SIGTERM or SIGINT).
That’s why we’re going to use the Start(ctx context.Context) method which won’t wait for anything to stop the program once fx.Invoke is launched.
Our DoerFunc() function just learned to count to 3.
1package main
2
3import (
4 "context"
5 "fmt"
6 "time"
7
8 "go.uber.org/fx"
9)
10
11type Doer interface {
12 Do()
13}
14
15var _ Doer = (*DoerImpl)(nil)
16
17type DoerImpl struct{}
18
19func NewDoerImpl(lc fx.Lifecycle) Doer {
20 doer := &DoerImpl{}
21 lc.Append(fx.Hook{
22 OnStart: func(ctx context.Context) error {
23 fmt.Println(">>>>> DoerImpl.OnStart()")
24 return nil
25 },
26 OnStop: func(ctx context.Context) error {
27 fmt.Println(">>>>> DoerImpl.OnStop()")
28 return nil
29 },
30 })
31 return doer
32}
33
34func (d DoerImpl) Do() {
35 fmt.Println(">>>>> DoerImpl.Do()")
36}
37
38var _ Doer = (*DecorationForDoerImpl)(nil)
39
40type DecorationForDoerImpl struct {
41 inner Doer
42}
43
44func NewDecorationForDoerImpl(inner Doer, lc fx.Lifecycle) Doer {
45 doer := &DecorationForDoerImpl{inner: inner}
46 lc.Append(fx.Hook{
47 OnStart: func(ctx context.Context) error {
48 fmt.Println(">>>>> DecorationForDoerImpl.OnStart()")
49 return nil
50 },
51 OnStop: func(ctx context.Context) error {
52 fmt.Println(">>>>> DecorationForDoerImpl.OnStop()")
53 return nil
54 },
55 })
56 return doer
57}
58
59func (d DecorationForDoerImpl) Do() {
60 fmt.Println(">>>>> DecorationForDoerImpl.Do()")
61 d.inner.Do()
62}
63
64func DoerFunc(doer Doer) {
65 for i := 0; i < 3; i++ {
66 doer.Do()
67 fmt.Println(">>>>> Sleeping 1 second")
68 time.Sleep(time.Second * 1)
69 }
70}
71
72func main() {
73 err := fx.New(
74 fx.Provide(NewDoerImpl),
75 fx.Decorate(NewDecorationForDoerImpl),
76 fx.Invoke(DoerFunc),
77 ).Start(context.Background())
78 if err != nil {
79 return
80 }
81}
Here, we went all the way by adding the OnStart and OnStop hooks on all Doer implementations.
go run ./main.go
[Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1()
[Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
[Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
[Fx] PROVIDE main.Doer <= main.NewDoerImpl()
[Fx] DECORATE main.Doer <= main.NewDecorationForDoerImpl()
[Fx] INVOKE main.DoerFunc()
[Fx] BEFORE RUN provide: go.uber.org/fx.New.func1()
[Fx] RUN provide: go.uber.org/fx.New.func1() in 41.042µs
[Fx] BEFORE RUN provide: main.NewDoerImpl()
[Fx] RUN provide: main.NewDoerImpl() in 96.834µs
[Fx] BEFORE RUN decorate: main.NewDecorationForDoerImpl()
[Fx] RUN decorate: main.NewDecorationForDoerImpl() in 15.625µs
>>>>> DecorationForDoerImpl.Do()
>>>>> DoerImpl.Do()
>>>>> Sleeping 1 second
>>>>> DecorationForDoerImpl.Do()
>>>>> DoerImpl.Do()
>>>>> Sleeping 1 second
>>>>> DecorationForDoerImpl.Do()
>>>>> DoerImpl.Do()
>>>>> Sleeping 1 second
[Fx] HOOK OnStart main.NewDoerImpl.func1() executing (caller: main.NewDoerImpl)
>>>>> DoerImpl.OnStart()
[Fx] HOOK OnStart main.NewDoerImpl.func1() called by main.NewDoerImpl ran successfully in 8.208µs
[Fx] HOOK OnStart main.NewDecorationForDoerImpl.func1() executing (caller: main.NewDecorationForDoerImpl)
>>>>> DecorationForDoerImpl.OnStart()
[Fx] HOOK OnStart main.NewDecorationForDoerImpl.func1() called by main.NewDecorationForDoerImpl ran successfully in 2.125µs
[Fx] RUNNING
There are two ways to tag services:
name tags that will allow you to name services and inject them by name. This is mandatory if for example you want to differentiate two services having the same interfacegroup tags that will allow you to inject a collection of services named identically into a collectionFor each of these tag types, you have the possibility to:
fx.ResultTags functionfx.ParamTags functionLet’s take the following example:
1package main
2
3import (
4 "context"
5 "fmt"
6
7 "go.uber.org/fx"
8)
9
10type Doer interface {
11 Do()
12}
13
14var _ Doer = (*DoerImpl)(nil)
15
16type DoerImpl struct{}
17
18func NewDoerImpl(lc fx.Lifecycle) Doer {
19 return &DoerImpl{}
20}
21
22func (d DoerImpl) Do() {
23 fmt.Println(">>>>> DoerImpl.Do()")
24}
25
26var _ Doer = (*DecorationForDoerImpl)(nil)
27
28type DecorationForDoerImpl struct {
29}
30
31func NewDecorationForDoerImpl() Doer {
32 return &DecorationForDoerImpl{}
33}
34
35func (d DecorationForDoerImpl) Do() {
36 fmt.Println(">>>>> DecorationForDoerImpl.Do()")
37}
38
39
40// Injection 1 / Here, we inject the two named services
41// `name:"doer.first"` and `name:"doer.second"`
42func DoerFunc(first Doer, second Doer) {
43 fmt.Println(">>>>> CALL EACH SERVICE")
44 first.Do()
45 second.Do()
46}
47
48// Injection 2 / Here, we inject `group:"doer"`
49func DoerAllFunc(all []Doer) {
50 fmt.Println(">>>>> CALL SLICE OF SERVICES")
51 for _, doer := range all {
52 doer.Do()
53 }
54}
55
56func main() {
57 fx.New(
58 fx.Provide(
59 // service declaration
60 fx.Annotate(
61 NewDoerImpl,
62 fx.ResultTags(`name:"doer.first"`),
63 ),
64 // service declaration
65 fx.Annotate(
66 NewDoerImpl,
67 fx.ResultTags(`group:"doer"`),
68 ),
69 ),
70 fx.Provide(
71 // service declaration
72 fx.Annotate(
73 NewDecorationForDoerImpl,
74 fx.ResultTags(`name:"doer.second"`),
75 ),
76 // service declaration
77 fx.Annotate(
78 NewDecorationForDoerImpl,
79 fx.ResultTags(`group:"doer"`),
80 ),
81 ),
82 fx.Invoke(
83 // Injection 1
84 fx.Annotate(
85 DoerFunc,
86 fx.ParamTags(`name:"doer.first"`, `name:"doer.second"`),
87 ),
88 // Injection 2
89 fx.Annotate(
90 DoerAllFunc,
91 fx.ParamTags(`group:"doer"`),
92 ),
93 ),
94 ).Run()
95}
go run ./cmd/test/
[Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1()
[Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
[Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
[Fx] PROVIDE main.Doer[name = "doer.first"] <= fx.Annotate(main.NewDoerImpl(), fx.ResultTags(["name:\"doer.first\""])
[Fx] PROVIDE main.Doer[group = "doer"] <= fx.Annotate(main.NewDoerImpl(), fx.ResultTags(["group:\"doer\""])
[Fx] PROVIDE main.Doer[name = "doer.second"] <= fx.Annotate(main.NewDecorationForDoerImpl(), fx.ResultTags(["name:\"doer.second\""])
[Fx] PROVIDE main.Doer[group = "doer"] <= fx.Annotate(main.NewDecorationForDoerImpl(), fx.ResultTags(["group:\"doer\""])
[Fx] INVOKE fx.Annotate(main.DoerFunc(), fx.ParamTags(["name:\"doer.first\"" "name:\"doer.second\""])
[Fx] BEFORE RUN provide: go.uber.org/fx.New.func1()
[Fx] RUN provide: go.uber.org/fx.New.func1() in 26.083µs
[Fx] BEFORE RUN provide: fx.Annotate(main.NewDoerImpl(), fx.ResultTags(["name:\"doer.first\""])
[Fx] RUN provide: fx.Annotate(main.NewDoerImpl(), fx.ResultTags(["name:\"doer.first\""]) in 100.917µs
[Fx] BEFORE RUN provide: fx.Annotate(main.NewDecorationForDoerImpl(), fx.ResultTags(["name:\"doer.second\""])
[Fx] RUN provide: fx.Annotate(main.NewDecorationForDoerImpl(), fx.ResultTags(["name:\"doer.second\""]) in 18.291µs
>>>>> CALL EACH SERVICE
>>>>> DoerImpl.Do()
>>>>> DecorationForDoerImpl.Do()
[Fx] INVOKE fx.Annotate(main.DoerAllFunc(), fx.ParamTags(["group:\"doer\""])
[Fx] BEFORE RUN provide: fx.Annotate(main.NewDoerImpl(), fx.ResultTags(["group:\"doer\""])
[Fx] RUN provide: fx.Annotate(main.NewDoerImpl(), fx.ResultTags(["group:\"doer\""]) in 14.792µs
[Fx] BEFORE RUN provide: fx.Annotate(main.NewDecorationForDoerImpl(), fx.ResultTags(["group:\"doer\""])
[Fx] RUN provide: fx.Annotate(main.NewDecorationForDoerImpl(), fx.ResultTags(["group:\"doer\""]) in 15.542µs
>>>>> CALL SLICE OF SERVICES
>>>>> DoerImpl.Do()
>>>>> DecorationForDoerImpl.Do()
[Fx] HOOK OnStart main.NewDoerImpl.func1() executing (caller: main.NewDoerImpl)
>>>>> DoerImpl.OnStart()
[Fx] HOOK OnStart main.NewDoerImpl.func1() called by main.NewDoerImpl ran successfully in 1.166µs
[Fx] HOOK OnStart main.NewDecorationForDoerImpl.func1() executing (caller: main.NewDecorationForDoerImpl)
>>>>> DecorationForDoerImpl.OnStart()
[Fx] HOOK OnStart main.NewDecorationForDoerImpl.func1() called by main.NewDecorationForDoerImpl ran successfully in 1.292µs
[Fx] HOOK OnStart main.NewDoerImpl.func1() executing (caller: main.NewDoerImpl)
>>>>> DoerImpl.OnStart()
[Fx] HOOK OnStart main.NewDoerImpl.func1() called by main.NewDoerImpl ran successfully in 959ns
[Fx] HOOK OnStart main.NewDecorationForDoerImpl.func1() executing (caller: main.NewDecorationForDoerImpl)
>>>>> DecorationForDoerImpl.OnStart()
[Fx] HOOK OnStart main.NewDecorationForDoerImpl.func1() called by main.NewDecorationForDoerImpl ran successfully in 916ns
[Fx] RUNNING
It is also possible to not explicitly declare all this information by using:
fx.ResultTags use fx.Outfx.ParamTags use fx.In 1package main
2
3import (
4 "context"
5 "fmt"
6
7 "go.uber.org/fx"
8)
9
10type Doer interface {
11 Do()
12}
13
14var _ Doer = (*DoerImpl)(nil)
15
16type DoerImpl struct{}
17
18type DoerFirstResults struct {
19 fx.Out
20
21 First Doer `name:"doer.first"`
22 Group Doer `group:"doer"`
23}
24
25func NewDoerImpl() DoerFirstResults {
26 doer := &DoerImpl{}
27 return DoerFirstResults{
28 First: doer,
29 Group: doer,
30 }
31}
32
33func (d DoerImpl) Do() {
34 fmt.Println(">>>>> DoerImpl.Do()")
35}
36
37var _ Doer = (*DecorationForDoerImpl)(nil)
38
39type DecorationForDoerImpl struct {
40}
41
42type DoerSecondResults struct {
43 fx.Out
44
45 Second Doer `name:"doer.second"`
46 Group Doer `group:"doer"`
47}
48
49func NewDecorationForDoerImpl() DoerSecondResults {
50 doer := &DecorationForDoerImpl{}
51 return DoerSecondResults{
52 Second: doer,
53 Group: doer,
54 }
55}
56
57func (d DecorationForDoerImpl) Do() {
58 fmt.Println(">>>>> DecorationForDoerImpl.Do()")
59}
60
61type DoerParams struct {
62 fx.In
63
64 First Doer `name:"doer.first"`
65 Second Doer `name:"doer.second"`
66}
67
68func DoerFunc(params DoerParams) {
69 fmt.Println(">>>>> CALL EACH SERVICE")
70 params.First.Do()
71 params.Second.Do()
72}
73
74type DoersParams struct {
75 fx.In
76
77 Doers []Doer `group:"doer"`
78}
79
80func DoerAllFunc(params DoersParams) {
81 fmt.Println(">>>>> CALL SLICE OF SERVICES")
82 for _, doer := range params.Doers {
83 doer.Do()
84 }
85}
86
87func main() {
88 fx.New(
89 fx.Provide(
90 NewDoerImpl,
91 NewDecorationForDoerImpl,
92 ),
93 fx.Invoke(
94 DoerFunc,
95 DoerAllFunc,
96 ),
97 ).Run()
98}
There is no difference between the two examples.
go run ./cmd/test/
[Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1()
[Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
[Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
[Fx] PROVIDE main.Doer[name = "doer.first"] <= main.NewDoerImpl()
[Fx] PROVIDE main.Doer[group = "doer"] <= main.NewDoerImpl()
[Fx] PROVIDE main.Doer[name = "doer.second"] <= main.NewDecorationForDoerImpl()
[Fx] PROVIDE main.Doer[group = "doer"] <= main.NewDecorationForDoerImpl()
[Fx] INVOKE main.DoerFunc()
[Fx] BEFORE RUN provide: go.uber.org/fx.New.func1()
[Fx] RUN provide: go.uber.org/fx.New.func1() in 37.417µs
[Fx] BEFORE RUN provide: main.NewDoerImpl()
[Fx] RUN provide: main.NewDoerImpl() in 121.75µs
[Fx] BEFORE RUN provide: main.NewDecorationForDoerImpl()
[Fx] RUN provide: main.NewDecorationForDoerImpl() in 14.625µs
>>>>> CALL EACH SERVICE
>>>>> DoerImpl.Do()
>>>>> DecorationForDoerImpl.Do()
[Fx] INVOKE main.DoerAllFunc()
>>>>> CALL SLICE OF SERVICES
>>>>> DoerImpl.Do()
>>>>> DecorationForDoerImpl.Do()
[Fx] HOOK OnStart main.NewDoerImpl.func1() executing (caller: main.NewDoerImpl)
>>>>> DoerImpl.OnStart()
[Fx] HOOK OnStart main.NewDoerImpl.func1() called by main.NewDoerImpl ran successfully in 1.083µs
[Fx] HOOK OnStart main.NewDecorationForDoerImpl.func1() executing (caller: main.NewDecorationForDoerImpl)
>>>>> DecorationForDoerImpl.OnStart()
[Fx] HOOK OnStart main.NewDecorationForDoerImpl.func1() called by main.NewDecorationForDoerImpl ran successfully in 959ns
[Fx] RUNNING
You therefore get the same results as previously. Nevertheless, it is strongly recommended to use this method with FX in order to reduce coupling between components.
When writing this section, I’m thinking very much of a friend who will recognize himself because I have the unfortunate tendency to love “decorating” all my code and to use this pattern constantly. Nevertheless, I think it’s probably the developer’s most powerful weapon when using a DI.
Let’s take a simple example with this interface:
type Repository interface {
GetContent(id string) (string, error)
}
From the same interface, we can for example over several sprints:
type Repository interface {
GetContent(id string) (string, error)
}
type DBRepository struct{}
func (r *DBRepository) GetContent(id string) (string, error) {
log.Println("Fetching from database")
return "data-from-db", nil
}
func NewDBRepository() Repository {
return &DBRepository{}
}
type CacheRepository struct {
next Repository
cache map[string]string
}
func NewCacheRepository(next Repository) Repository {
return &CacheRepository{next: next, cache: make(map[string]string)}
}
func (r *CacheRepository) GetContent(id string) (string, error) {
if val, ok := r.cache[id]; ok {
log.Println("Fetching from cache")
return val, nil
}
log.Println("Cache miss -> fallback DB")
val, err := r.next.GetContent(id)
if err == nil {
r.cache[id] = val
}
return val, err
}
Assembly
app := fx.New(
fx.Provide(NewDBRepository),
fx.Decorate(NewCacheRepository),
fx.Invoke(func(repo Repository) {
repo.GetContent("123")
repo.GetContent("123")
}),
)
app.Run()
Rather than limiting ourselves to technical metrics, let’s follow business indicators.
Example: tracking content consulted by type
type Content struct {
ID string
Type string // "article", "video", "image"
Data string
}
type Repository interface {
GetContent(id string) (Content, error)
}
type MetricsRepository struct {
next Repository
byType *prometheus.CounterVec
}
func NewMetricsRepository(next Repository) Repository {
counter := promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "repository_content_requests_total",
Help: "Number of content requests by type (business metric)",
},
[]string{"type"},
)
return &MetricsRepository{next: next, byType: counter}
}
func (r *MetricsRepository) GetContent(id string) (Content, error) {
content, err := r.next.GetContent(id)
if err == nil {
r.byType.WithLabelValues(content.Type).Inc()
log.Printf("[METRICS] Content of type %s requested", content.Type)
}
return content, err
}
Assembly
app := fx.New(
fx.Provide(NewDBRepository),
fx.Decorate(NewCacheRepository),
fx.Decorate(NewMetricsRepository),
fx.Invoke(func(repo Repository) {
repo.GetContent("article-123")
repo.GetContent("video-456")
}),
)
app.Run()
FX comes with the fxtest package which allows you to test the DI. Some methods will be particularly useful:
fx.Populate which will allow you to retrieve a structure to test it specifically in a DI usage contextfx.Replace which will allow you to replace a service with a mockapp.RequireStart() will trigger all the OnStart in order to be able to test the behaviorsapp.RequireStop() will do the same with the OnStoppackage main
import (
"testing"
"go.uber.org/fx"
"go.uber.org/fx/fxtest"
)
func TestFXDoer(t *testing.T) {
var first Doer
var second Doer
app := fxtest.New(
t,
fx.Provide(
NewDoerImpl,
NewDecorationForDoerImpl,
),
fx.Populate(
fx.Annotate(
&first,
fx.ParamTags(`name:"doer.first"`),
),
fx.Annotate(
&second,
fx.ParamTags(`name:"doer.second"`),
),
),
)
defer app.RequireStart().RequireStop()
first.Do()
second.Do()
}
You can choose to disable FX logs by adding fx.NopLogger
1fx.New(
2 fx.Provide(
3 NewDoerImpl,
4 NewDecorationForDoerImpl,
5 ),
6 fx.Invoke(
7 DoerFunc,
8 DoerAllFunc,
9 ),
10 fx.NopLogger,
11).Run()
go run ./cmd/test
>>>>> CALL EACH SERVICE
>>>>> DoerImpl.Do()
>>>>> DecorationForDoerImpl.Do()
>>>>> CALL SLICE OF SERVICES
>>>>> DoerImpl.Do()
>>>>> DecorationForDoerImpl.Do()
>>>>> DoerImpl.OnStart()
>>>>> DecorationForDoerImpl.OnStart()
FX provides a fx.DotGraph structure that will allow you to debug your dependency graph.
// DotGraphHandler is a router decorator that adds a /graph endpoint to display the dependency graph
type DotGraphHandler struct {
dot fx.DotGraph
}
// NewDotGraphHandler creates a new dot graph handler instance
func NewDotGraphHandler(dot fx.DotGraph) *DotGraphHandler {
return &DotGraphHandler{dot: dot}
}
// Apply applies the router decorator to the router
func (d *DotGraphHandler) Apply(r chi.Router) error {
r.Get("/graph", func (w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/vnd.graphviz")
_, _ = w.Write([]byte(d.dot))
})
return nil
}