Uber Fx - Invoke

Viewed 37

How do I get the following working?

To the lifecycle function, I need to pass 2 different implementations of Foo.

package main

import (
    "context"
    "fmt"
    "go.uber.org/fx"
)

type Foo interface {
    Print()
}

type Bar struct{}

func (b *Bar) Print() {
    fmt.Println("I'm bar")
}

type Baz struct{}

func (b *Baz) Print() {
    fmt.Println("I'm baz")
}

func foo() Foo {
    return &Bar{}
}

func anotherFoo() Foo {
    return &Baz{}
}

func main() {
    workingStart() //This works
    //nonWorkingStart() //This does not
}

func nonWorkingStart() {

    app := fx.New(
        fx.Provide(fx.Annotate(foo, fx.ResultTags(`name:"bar"`))),
        fx.Provide(fx.Annotate(anotherFoo, fx.ResultTags(`name:"baz"`))),
        //How to configure nonWorkingRun having 2 different foo's in its arguments?
        fx.Invoke(nonWorkingRun),
    )

    if app.Err() != nil {
        panic(fmt.Errorf("unable to bootstrap app: %w", app.Err()))
    }

    app.Run()
}

func workingStart() {

    app := fx.New(
        fx.Provide(foo),
        fx.Invoke(workingRun),
    )

    if app.Err() != nil {
        panic(fmt.Errorf("unable to bootstrap app: %w", app.Err()))
    }

    app.Run()
}

func nonWorkingRun(lifecycle fx.Lifecycle, foo Foo, anotherFoo Foo) {
    lifecycle.Append(
        fx.Hook{
            OnStart: func(context.Context) error {
                foo.Print()
                anotherFoo.Print()
                return nil
            },
        },
    )
}

func workingRun(lifecycle fx.Lifecycle, foo Foo) {
    lifecycle.Append(
        fx.Hook{
            OnStart: func(context.Context) error {
                foo.Print()
                return nil
            },
        },
    )
}

I got it working implementing it the following way. Not sure if there are other ways other than using a struct holding a slice of Foo's and building that struct using fx.ParamTags

type FooSlices struct {
    fooSlices []Foo
}

func fooSlices(foo, anotherFoo Foo) FooSlices {
    return FooSlices{fooSlices: []Foo{foo, anotherFoo}}
}

func main() {
    //workingStart()
    nonWorkingStart()
}

func nonWorkingStart() {

    app := fx.New(
        fx.Provide(fx.Annotate(foo, fx.ResultTags(`name:"bar"`))),
        fx.Provide(fx.Annotate(anotherFoo, fx.ResultTags(`name:"baz"`))),
        fx.Provide(fx.Annotate(fooSlices, fx.ParamTags(`name:"bar"`, `name:"baz"`))),
        fx.Invoke(nonWorkingRun),
    )

    if app.Err() != nil {
        panic(fmt.Errorf("unable to bootstrap app: %w", app.Err()))
    }

    app.Run()
}


func nonWorkingRun(lifecycle fx.Lifecycle, fooSlices FooSlices) {
    lifecycle.Append(
        fx.Hook{
            OnStart: func(context.Context) error {
                for _, foo := range fooSlices.fooSlices {
                    foo.Print()
                }
                return nil
            },
        },
    )
}
1 Answers

Collecting a slice of inputs is fairly hard to reason about, also not very extensible and goes against the spirit of what fx is "meant" to do (hide boilerplate code).

Just a small amount of refactors first

  • fx.Provide can take in a range of inputs and just needs to be called once
  • n implementations of foo requires only n-1 annotations, as the first one can be the raw type, and the remaining being the annotated types
  • avoid wrapping the panic and let it happen naturally if it happens; this would imply a syntax issue which can be mitigated prior to pushing the code
fx.New(
    fx.Provide(
        foo,
        fx.Annotate(foo, fx.ResultTags(`name:"baz"`)),
    ),
    fx.Invoke(toBeInvoked),
).Run()

type Params struct {
    fx.In

    Lifecycle fx.Lifecycle
    Bar foo
    Baz foo `name:"baz"`
}

func toBeInvoked(p Params) {
    p.Lifecycle.Append(
        fx.Hook{
            OnStart: func(_ context.Context) error {
                p.Bar.Print()
                p.Baz.Print()
                return nil
            },
        },
    )
}

There are other ways to do this as well in fx, this one is a fairly common pattern.

Related