DEV Community

Cover image for Dependency Injection in Go using Fx
Preslav Mihaylov
Preslav Mihaylov

Posted on • Edited on • Originally published at pmihaylov.com

Dependency Injection in Go using Fx

When you initially start a Go project, your main function typically has a bunch of wiring code - initialising your routes, plugging in middlewares, initialising your template engines, loggers, etc.

This is one of the great things about Go - you don't have any magic happening behind the scenes. The code is all there and you can read it and debug it.

But as your software grows, you start feeling the growing pains - your main function starts becoming more and more convoluted. You start having all sorts of small bits and pieces plugged in here and there - healthchecks, database setup code, metrics, tracers, external API connections, etc etc.

And what if your application grows into a microservice architecture? What do you do when you have five different microservices demanding the same bunch of setup code, specific to your environment?

In this article, I will introduce you to Fx. It's a Go framework which solves both problems outlined above using dependency injection.

Let's jump in.

All the code from this article is available on github.com/preslavmihaylov/fxappexample.

Creating a Simple App Using Manual Wiring

What I described in the introduction - having your setup code visible in your codebase (often times the main function) is called manual wiring.

Let's see an example to understand how it looks like in practice.

If you already know what manual wiring is then simply skip to the next section. But make sure to review the code at the end of the section to have enough context for what we're building.

The typical way you start a new web app is via simple hello world server:

And here is the main function:

Nothing fancy so far.

At some point, you also need a logger. Adding it to your main using the logger from uber-go/zap:

That logger is then assigned to a field in the http handler. That part of the code I will omit for brevity.

But look at that hardcoded address. Let's export it into a configuration using go-yaml/yaml.

Our configuration will look like this in config/base.yaml:

In order to parse it, we'll have to create some more structs:

And later use them to un-marshal the yaml and pass the address to our server:

Let's stop here. I think you get the idea. We construct each structure ourselves and manually wire the dependency to the component who needs it.

All the code from this section is available on branch v1-manual-wiring.

Limitations of The Current Approach

To be honest, this approach works quite well for small to medium apps. The benefit is that the code is easy to trace, understand, debug. There is no hidden magic.

The problem, however, is that as the application grows, there will be tons of other components created which you will need to manually wire in your main function as we've been doing so far.

As time passes, the manual wiring will become more and more complex, easy to get wrong and hard to manage.

This is what dependency injection frameworks are for - they allow you to not bother with this wiring logic as they do it for you.

This allows your application to scale gracefully as more and more components are added into it.

The second big problem with the approach so far is that reuse of common components is harder.

In case you are working on a microservice architecture, there will probably be a lot of infrastructure-related components which stay the same between services. They are not directly related to the service's domain.

Examples are emitting metrics, logging, standard configurations, health checks, etc.

If you rely on manual wiring, you will have to either copy-paste this boilerplate across services each time you bootstrap one or, best-case scenario, export the common components into separate packages to reuse.

But in the latter case, you will still have to deal with instance initialisation and wiring yourself.

So if you've grown to a scale where you are facing these problems, what options do you have?

Scaling the Application using Fx

Fx is a dependency injection application framework.

That's a mouthful. What does it mean?

The "dependency injection" part means that it can take care of the component wiring code for you.

The "application framework" part means that this is not simply a library you plug in and use where needed. Instead, it is a framework, which manages the entire lifecycle of your application - imagine ASP.NET for C# and Spring for Java.

Those are application frameworks.

But although I am comparing Fx to those behemoths, it is far simpler than those frameworks as you'll see.

But What is a Dependency Injection Framework?

First, let's learn what a dependency injection framework does for a living. At its core it simply connects "providers" to "receivers".

A provider is some component which says "Hey, here is my instance. Feel free to give it to anyone who needs it."

A receiver is some component which says "Hey, in order to work properly, I need instances of components X, Y and Z".

Those X, Y, and Zs are "dependencies" the receiver needs in order to work.

Note that receivers can also be providers.

So what the DI framework does for you is it automatically connects providers to receivers. In case some receiver cannot get one of his dependencies, the framework throws an error.

That's all you need to know to get started. Now let's refactor our application to use the Fx framework.

Converting our Service into an Fx Application

The first step is to install the library via go get go.uber.org/fx. Next up, let's boot it up in our main:

If you keep everything as-is and run your application now, you will have a fully-functioning empty Fx application.

Now, let's start refactoring the rest of our code as well.

To keep things simple, all following changes for this section will be done in main.go.

Extracting the basic providers

We'll start with the configuration:

What we just did is create a provider function for our configuration. We now have to declare to Fx that this provider is available for use:

Let's now do the same for the logger:

Here, we did the same refactor as the one for the configuration. The only difference with the original implementation is that we removed defer logger.Sync().

We'll add that later in an application shutdown hook as it is meant to be executed when the application exits.

One additional dependency we have to provide is the standard http serve mux. It is a dependency to our http handler:

You could wrap that also in a provider function. This would allow you to plug in any additional standard options into the serve mux, which your application might need.

At the moment, we don't need that so I've provided the function from the http package directly.

Extracting the http handler - our first receiver

Finally, let's provide our http handler's New function and delete the rest of our main function:

This last addition is an example of both a provider and a receiver. Why? Let's recall the http handler New function again:

Notice the parameters to the function. All other functions we added so far didn't have any parameters. That meant that they only provided some instances, without having any additional dependencies - they were only providers.

This function is now both a provider and a receiver. It is a provider because it returns a new instance of our http handler.

But more interestingly, it is a receiver also as it expects a logger and a http.ServeMux to work properly.

Where do those parameters come from then? They come from the rest of the provider functions which we created earlier - ProvideLogger and http.NewServeMux

Fx does the heavy lifting of correctly routing the instances to where they are needed.

Registering the lifecycle hooks

One final touch we have to add is adding an OnStart hook. We need that in order to finally boot up our server after everything else is setup.
We'll also need an OnStop hook in order to flush the logger's buffer on program exit:

Notice that this is also a receiver function as it depends on fx.Lifecycle, *zap.SugaredLogger, *Config and *httphandler.Handler.

And there you have it! We are now running a full-fledged Fx application.

Take a look at the entire code from this section on branch v2-fx-example.

Conclusion

Some might look at the initial version of the code and think that it is better as it's much simpler and there is no magic going on.

And that is definitely true. This is why I suggest using Fx only if you really need it. For small to medium size Go applications, sticking to manual wiring makes sense.

After all, every tool we use should be able pull its weight - it's benefits should outweigh the costs. In this case, the cost is in terms of complexity and readability.

Additionally, since this tool relies on reflection, your IDE won't be as helpful in finding out where a dependency comes from. Hence, your code will be harder to trace & debug.

However, as I stated in the beginning of the article, your manual wiring code will start becoming more and more complex. That is the point when considering a framework like this one makes sense.

And additionally, if you're maintaining several services which rely on common components & configuration, Fx can then help as well by allowing you to separate your application into reusable modules.

That is something we are going to explore in the follow-up article as this one is already becoming too large.

Stay tuned.

Top comments (2)

Collapse
 
theodesp profile image
Theofanis Despoudis

How does this compare with wire?

Collapse
 
pmihaylov profile image
Preslav Mihaylov

The difference is that wire uses code generation to handle DI, while Fx relies on reflection.

Both approaches have their pros and cons - code generations spits out a lot of boilerplate code which isn't so interesting, reflection makes "magic behind the scenes" making code debugging & indexing usages harder.

What I like most about Fx, however, is how it allows you to modularize your codebase - separating your code into independent modules. This is especially useful in a large codebase where there is a lot of infra going on - monitoring, tracing, logging, etc. With fx, you can encapsulate all this code & configuration into a common module which can be reused across services.

The result is that in your main function, you simply provide your module and have a lot of infra already setup out of the box.