/ golang

My thoughts on Uber FX

I rarely write posts on specific libraries, but this one in particular seems to have created some discussion within the Go community. So thought I'd throw my thoughts into the ring.

The main objections I've seen is that it's 'not very Go', it's not very idiomatic. But on the other hand I found it so useful, I couldn't resist but weight it up against some of the objections.

Unfortunately I still have to write lots of JavaScript day to day. A library I discovered recently which I loved was Awilix. Awilix is a service container. I've written about and used service containers fairly extensively in other languages. It's a pattern I love and encourage people to learn about.

The idiom in Go is to use interfaces wisely and pass in dependencies, you don't need to do anything clever in Go to recreate the dependency injection pattern. I stuck with that approach for a number of years and was happy enough. Then came Uber's FX library. I'd used a few other Uber libraries recently as well, namely Zap, and loved it. So I thought I'd give it a go!

What drove me to consider a DI library for Go, despite it being naturally very supportive of those good architectural patterns inherently. I found my main.go file, or my entrypoint file becomes an extensive series of 'plumbing' and set-up code. I found with bigger projects with lots of dependencies and set-up, this approach got a little bit tedious at times. So a tool that would neaten that up and potentially make things even more modular and extensible was an exciting prospect!

The good

First of all the API is really easy to use, you ensure your services/code has constructors with clearly defined depdencies, which are registered, and it just works! The overhead was minimal in terms of set-up.

Lifecycle management - Fx has a useful lifecycle system for dealing with application starts, stops etc. So you can manage what your depenencies do when your application starts etc. Which I found really useful.

Insert example here...


func RunApplication(
	lifecycle fx.Lifecycle,
	queueProc *queue.Queue,
	generator *generator.Generator,
) {
	lifecycle.Append(
		fx.Hook{
			OnStart: func(ctx context.Context) error {
				go queueProc.Listen()
                go generator.Process()
                return nil
            },
            OnStop: func(ctx context.Context) error {
                go queueProc.Stop()
                go generator.Stop()
                return nil
            }
        },
    },
}

Interfaces as argument types

Another gripe I came across was, accepting interfaces as arguments to constructors is often a good design choice, however Fx can't resolve an interface from the container. Potentially there could be any number of concrete types that satisfy an interface. Therefor you have to implicitly register a struct to an interface when registering your services. For example:

// main.go
app := fx.New(
		fx.Provide(

            // This is how we have to register a concrete type
            // implicitly to an interface within our container.
			func(a *queue.SQS) queue.Adapter { return a },
            queue.NewQueue,
        )
)

// queue.go
// Because of the conversion above, adapter will automatically
// become the above concrete type from the container.
func NewQueue(adapter Adapter) *Queue {
	return &Queue{
		messageChan: make(chan Message),
		errors:      make(chan error),
		adapter:     adapter,
	}
}

This is okay, just a little weird.

The not so good

I was always willing to accept the fact that this library is quite 'magical' and not particularly idiomatic. Although, the dependencies are type checked at run time, they aren't checked at compile time, of course. Which isn't the end of the world, they are checked at least.

Contructor functions are required. As far as I can see, you are required to create constructor functions for any struct which takes dependencies. This is a minor gripe, I tend to use constructor functions liberally anyway, so it wasn't a particularly onerous change, but you may not want to use this pattern for instantiating all of your structs, it's often overkill.

You can of course mitigate some of these negatives through good design, for example, in my uses, I tend to use this library purely for plubming in the top level dependencies. Once the set-up is done, the service container is almost forgotten from then onwards. In other words, I push the use of this library to the very top. You can use it more integrally by including it in your structs etc, creating modules etc. Which are useful usecases, but with all things, it depends how 'all in' you want to go. I'd suggest using any third party library with utmost caution and certainly resist creating a dependency between it, and your business logic.

In conclusion, it might not be the purist 'Go way', but it is really useful, so use it, but use it with caution!