
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.
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
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.
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.
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
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
Il existe deux façons de tagger des services :
name qui vont permettre de nommer des services et de les injecter depuis leur nom. C’est obligatoire si par exemple vous souhaitez différencier deux services ayant la même interfacegroup qui vont permettre d’injecter une collection de services nommés de manière identique dans une collectionPour chacun de ces types de tag, vous avez la possibilité de :
fx.ResultTagsfx.ParamTagsPrenons 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
Il est également possible de ne pas déclarer explicitement toute ces informations en utilisant :
fx.ResultTags utiliser fx.Outfx.ParamTags utiliser 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}
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.
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 :
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
}
Assemblage
app := fx.New(
fx.Provide(NewDBRepository),
fx.Decorate(NewCacheRepository),
fx.Invoke(func(repo Repository) {
repo.GetContent("123")
repo.GetContent("123")
}),
)
app.Run()
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()
FX arrive avec le package txtest qui permet de tester la DI. Quelques méthodes vont être particulièrement utile :
fx.Populate qui va permettre de récupérer une structure pour la tester spécifiquement dans un context d’utilisation de la DIfx.Replace qui va permettre de remplacer un service par un mockapp.RequireStart() va déclancher l’ensemble des OnStart afin de pouvoir tester les comportementsapp.RequireStop() va faire de même avec les 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()
}
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()
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
}