How to Use uber/fx for Dependency Injection in Go

uber/fx is a dependency injection container for Go that uses function signatures to define dependencies and provides a declarative way to wire your application together.

uber/fx is a dependency injection container for Go that uses function signatures to define dependencies and provides a declarative way to wire your application together. You define modules that provide specific values and consume them in other modules, letting fx handle the lifecycle and wiring automatically.

Here is a practical example of setting up a simple HTTP server with a database client:

package main

import (
	"context"
	"log"
	"net/http"

	"go.uber.org/fx"
)

// Define the dependency
type DBClient struct{}

func NewDBClient() *DBClient {
	log.Println("Initializing DB connection")
	return &DBClient{}
}

// Define the consumer
type Server struct {
	DB *DBClient
}

func NewServer(db *DBClient) *Server {
	return &Server{DB: db}
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello from fx-wired server"))
}

func main() {
	app := fx.New(
		fx.Provide(NewDBClient),
		fx.Provide(NewServer),
		fx.Invoke(func(s *Server) {
			log.Println("Starting server...")
			http.ListenAndServe(":8080", s)
		}),
	)

	if err := app.Err(); err != nil {
		log.Fatal(err)
	}

	// In a real app, you would use app.Run() which handles graceful shutdown
	// app.Run()
}

In this setup, fx.Provide registers the constructor functions. When fx.Invoke calls the anonymous function requiring a *Server, fx automatically resolves that Server needs a *DBClient, calls NewDBClient, and injects the result.

For more complex applications, you can split logic into separate modules to keep things organized. You can also use fx.Options to group configurations:

func main() {
	app := fx.New(
		fx.Module("db",
			fx.Provide(NewDBClient),
		),
		fx.Module("api",
			fx.Provide(NewServer),
			fx.Invoke(func(s *Server) {
				http.ListenAndServe(":8080", s)
			}),
		),
	)
	
	// Run the app
	if err := app.Err(); err != nil {
		log.Fatal(err)
	}
	
	// app.Run() handles context cancellation and graceful shutdown
}

Key things to remember:

  1. Function Signatures: fx relies entirely on Go's type system. If a function needs a dependency, it must be a parameter.
  2. Lifecycle: Use fx.Invoke for startup logic and fx.Hook if you need specific pre-start or post-stop actions.
  3. Testing: You can easily swap implementations in tests by providing mock constructors to fx.New instead of the real ones.

This approach eliminates manual wiring boilerplate and makes your application's dependency graph explicit and testable.