🇫🇷 🇬🇧 LinkedIn GitHub

Maîtriser l'injection de dépendances en Go avec Uber FX

samedi 13 septembre 2025
Uber FX Logo

Changer d’écosystème n’est jamais évident. Alors imaginez une entreprise comme M6 Web, avec plus de 15 ans d’expérience en PHP, à qui l’on propose du jour au lendemain d’abandonner Symfony pour passer à Go.

Ayant moi-même travaillé de longues années avec Symfony 1, je dois avouer que je suis devenu accro à son système d’injection de dépendances : il est simple à prendre en main, parfaitement documenté, et grâce à l’autowiring, son utilisation devient presque transparente. C’est tellement pratique qu’on en oublie tout le temps gagné au quotidien.

C’est pourquoi, lorsque j’ai commencé à réfléchir à ce changement de langage avec mon équipe, nous sommes partis d’un constat clair : peu importe la direction que nous prendrions, il nous fallait d’abord choisir une solution d’injection de dépendances. Tout le reste viendrait naturellement se greffer dessus.

C’est dans cette optique que nous avons décidé d’explorer FX 2, une solution d’injection de dépendances développée par Uber.

Présentation

FX est une librairie très riche en fonctionnalités mais pour résumer, elle possède deux principaux composants :

graph TD
  subgraph dep [Gestion des dépendances]
    A[fx.Provide] -->|Construit| B[Graph de dépendances]
    B -->|Peut être modifié| C[fx.Decorate]
    B -->|Résout et injecte| D[fx.Invoke]
  end

  subgraph cycle [Gestion du cycle de vie]
    E[fx.Lifecycle] -->|Manage| F[fx.Hook]
    F -->|OnStart| G[Démarrage des services]
    F -->|OnStop| H[Arrêt propre des services]
  end

  D -->|Démarre via| E

Le fonctionnement de FX

Étape 1 : fx.Provide

Partons d’une simple interface Doer.

 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}

Ici NewDoerImpl est enregistrée et disponible dans le graphe de dépendances.

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() n’est toutefois pas affiché car la structure est bien instanciée mais n’est jamais invoquée.

Étape 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}

Ici DoerFunc consomme la dépendance DoerImpl fournie par 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() s’affiche donc cette fois-ci.

Étape 3 : fx.Decorate

Nous allons maintenant décorer notre première implémentation de Doer.

 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 remplace la valeur de Config par une version modifiée.

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

Étape 4 : fx.Lifecycle

Ici nous allons tester les hooks OnStart et OnStop et pour celà, nous allons légèrement modifier notre programme car Run() attends un signal d’arrêt (SIGTERM ou SIGINT). C’est pourquoi nous allons utiliser la méthode Start(ctx context.Context) qui ne va pas attendre quoique ce soit pour arrêter le programme une fois fx.Invoke lancé.

Notre fonction DoerFunc() vient d’apprendre à compter jusqu’à 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}

Ici, nous sommes allés au bout des choses en ajoutant les hooks OnStart et OnStop sur l’ensemble des implémentations de Doer.

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

Pour aller plus loin

Les services taggés

Il existe deux façons de tagger des services :

Pour chacun de ces types de tag, vous avez la possibilité de :

Prenons l’exemple suivant :

 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 / Ici, on injecte les deux services nommés 
41// `name:"doer.first"` et `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 / Ici, on injecte `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			// déclaration de service 
60			fx.Annotate(
61				NewDoerImpl,
62				fx.ResultTags(`name:"doer.first"`),
63			),
64			// déclaration de service 
65			fx.Annotate(
66				NewDoerImpl,
67				fx.ResultTags(`group:"doer"`),
68			),
69		),
70		fx.Provide(
71			// déclaration de service 
72			fx.Annotate(
73				NewDecorationForDoerImpl,
74				fx.ResultTags(`name:"doer.second"`),
75			),
76			// déclaration de service 
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 et fx.Out

Il est également possible de ne pas déclarer explicitement toute ces informations en utilisant :

 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}

Il n’y a aucune différence entre les deux exemples.

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

Vous obtenez donc les mêmes résultats que précédemment. Néanmoins, il est vivement recommandé d’utiliser cette méthode avec FX afin de réduire le couplage entre les composants.

La puissance de fx.Decorate dans un contexte entreprise

En écrivant cette partie, je pense très fort à un ami qui se reconnaîtra car j’ai la fâcheuse tendance à adorer “décorer” tout mon code et à utiliser ce pattern constamment. Néanmoins, je pense que c’est probablement l’arme la plus puissante du développeur utilisant une DI.

Prenons un exemple simple avec cette interface :

type Repository interface {
    GetContent(id string) (string, error)
}

À partir d’une même interface, on va pouvoir par exemple sur plusieurs sprints :

Sprint 1 - L’accès à la base de données

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 - Ajoutons du 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
}

Assemblage

app := fx.New(
    fx.Provide(NewDBRepository),
    fx.Decorate(NewCacheRepository),
    fx.Invoke(func(repo Repository) {
        repo.GetContent("123")
        repo.GetContent("123")
    }),
)
app.Run()

Sprint 3 - Ajoutons des métriques “métier”

Plutôt que de se limiter à des métriques techniques, suivons des indicateurs métiers.

Exemple : suivi des contenus consultés par 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
}

Assemblage

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

Testons notre DI

FX arrive avec le package txtest qui permet de tester la DI. Quelques méthodes vont être particulièrement utile :

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

Activer / Désactiver / Adapter le logging de FX

Vous pouvez choisir de désactiver les logs de FX en ajoutant 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()

Visualisez vos dépendances avec fx.DotGraph

FX fournis une structure fx.DotGraph qui va vous permettre de debug votre graph de dépendances.

// 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