🇫🇷 🇬🇧 LinkedIn GitHub

Mastering Dependency Injection in Go with Uber FX

Saturday, September 13, 2025
Uber FX Logo

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.

Overview

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

How FX Works

Step 1: fx.Provide

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.

Step 2: fx.Invoke

 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.

Step 3: fx.Decorate

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

Step 4: fx.Lifecycle

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

Going Further

Tagged Services

There are two ways to tag services:

For each of these tag types, you have the possibility to:

Let’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

fx.In and fx.Out

It is also possible to not explicitly declare all this information by using:

 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.

The Power of fx.Decorate in an Enterprise Context

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:

Sprint 1 - Database Access

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{}
}

Sprint 2 - Let’s Add Cache

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()

Sprint 3 - Let’s Add Business Metrics

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()

Testing our DI

FX comes with the fxtest package which allows you to test the DI. Some methods will be particularly useful:

package 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()
}

Bonus

Enable / Disable / Adapt FX Logging

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()

Visualize your Dependencies with fx.DotGraph

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
}

  1. Symfony Dependency Injection Components ↩︎

  2. Uber FX ↩︎


← Retour aux billets techniques