Dependency injection might sound complex, but at its core it’s about making code more flexible, reusable, and easier to test. By the end of this article you’ll understand what DI is and how to use it effectively in Go with Uber Fx.
What is Dependency Injection?
Instead of a struct constructing its own dependencies, they are passed in from the outside:
// Without DI — hard to test, tightly coupled
type UserService struct {
db *sql.DB
}
func NewUserService() *UserService {
db, _ := sql.Open("postgres", os.Getenv("DB_URL"))
return &UserService{db: db}
}
// With DI — testable, loosely coupled
type UserService struct {
db Database
}
func NewUserService(db Database) *UserService {
return &UserService{db: db}
}
Benefits of Dependency Injection
- Loose Coupling — components don’t know how their dependencies are created
- Testability — inject fakes/mocks in tests without changing production code
- Reusability — swap implementations without touching business logic
- Single Responsibility — construction logic lives in one place
Introducing Uber Fx
Fx is a dependency injection framework for Go that uses reflection to wire up your application automatically.
go get go.uber.org/fx
Wiring a Simple App
package main
import (
"context"
"go.uber.org/fx"
)
func NewDatabase() *Database {
return &Database{conn: connectPostgres()}
}
func NewUserRepository(db *Database) *UserRepository {
return &UserRepository{db: db}
}
func NewUserService(repo *UserRepository) *UserService {
return &UserService{repo: repo}
}
func NewHTTPServer(svc *UserService) *http.Server {
mux := http.NewServeMux()
mux.HandleFunc("/users", svc.HandleGetUsers)
return &http.Server{Addr: ":8080", Handler: mux}
}
func main() {
fx.New(
fx.Provide(
NewDatabase,
NewUserRepository,
NewUserService,
NewHTTPServer,
),
fx.Invoke(func(s *http.Server, lc fx.Lifecycle) {
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
go s.ListenAndServe()
return nil
},
OnStop: func(ctx context.Context) error {
return s.Shutdown(ctx)
},
})
}),
).Run()
}
Lifecycle Hooks
Fx gives you OnStart and OnStop hooks for graceful startup and shutdown — critical in production services.
Fx Modules
Group related providers into reusable modules:
var DatabaseModule = fx.Module("database",
fx.Provide(NewDatabase, NewMigrator),
)
var UserModule = fx.Module("user",
fx.Provide(NewUserRepository, NewUserService),
)
func main() {
fx.New(
DatabaseModule,
UserModule,
fx.Provide(NewHTTPServer),
fx.Invoke(startServer),
).Run()
}
When to Use Uber Fx
Fx shines in large applications with many services and dependencies. For small CLIs or simple services, manual wiring is often clearer. The break-even point is roughly when you have 5+ interconnected services.