Render Your Go Code Clean: Introduction to Dependency Injection with Fx RenderATL 2024 Workshop
A presentation at Render ATL Conference in July 2024 in Atlanta, GA, USA by Kemet Dugue
Render Your Go Code Clean: Introduction to Dependency Injection with Fx RenderATL 2024 Workshop
Agenda 01 Introduction 02 Dependency Injection 03 Fx Framework (5 minutes) (20 minutes) (25 minutes)
Meet your team! Dorian Perkins Kemet Dugue Paul Murage Staff Software Engineer Software Networking team Software Engineer Driver Onboarding team Software Engineer Configuration Platform team Render Your Go Code Clean: Introduction to Dependency Injection with Fx 3
What to expect in this workshop Render Your Go Code Clean: Introduction to Dependency Injection with Fx 4
Introduction | What you’ll need … Laptop Go IDE You’ll be coding in this workshop, so make sure you have your trusty laptop handy. Ensure you have Go downloaded and installed locally before the workshop. Get your favorite code editor ready to go and ensure it is set up to work with Go. (prior experience in Go is not required). [https://go.dev/doc/install] Render Your Go Code Clean: Introduction to Dependency Injection with Fx 5
Introduction | What you’ll get … Understanding of dependency injection Render Your Go Code Clean: Introduction to Dependency Injection with Fx Introduction to the Fx application framework Hands-on experience writing a Go application with Fx 6
Dependency Injection Render Your Go Code Clean: Introduction to Dependency Injection with Fx 7
Dependency Injection | What is a dependency? Code that is relied on by other code to function correctly External dependencies ● Pre-written code created by a third-party (i.e., libraries or frameworks) Internal dependencies ● Connections between different parts of your own code Render Your Go Code Clean: Introduction to Dependency Injection with Fx import ( // External dependency “go.uber.org/zap” ) import ( // Internal dependency “mycompany.com/my-project/my-dependency” ) 8
Dependency Injection | What is dependency injection (DI)? Supplying an object with its dependencies rather than creating them itself ❌ Example 1 (global state) var Logger = zap.NewExample() func MyFunction() { // Uses global state Logger.Info(“Hello!”) } Render Your Go Code Clean: Introduction to Dependency Injection with Fx ❌ Example 2 (create deps) func MyFunction() { // Creates dependency itself logger := zap.NewExample() logger.Info(“Hello!”) } 9
Dependency Injection | What is dependency injection (DI)? Supplying an object with its dependencies rather than creating them itself ✔ Example 3 (DI, concrete type) func MyFunction(logger *zap.Logger) { // Injects a concrete type logger.Info(“Hello!”) } ✔ Example 4 (DI, interface) type Logger interface { Info(v …any) } func MyFunction(logger Logger) { // Injects an interface logger.Info(“Hello!”) } Render Your Go Code Clean: Introduction to Dependency Injection with Fx 10
Dependency Injection | Benefits ● Loose coupling ○ Objects are less reliant on specific implementations type Logger interface { Info(v …any) } func MyFunction(logger Logger) { // Injects an interface logger.Info(“Hello!”) } Render Your Go Code Clean: Introduction to Dependency Injection with Fx 11
Dependency Injection | Benefits ● Loose coupling ● Promotes modularity ○ Separates concerns of dependency creation and usage func main() { logger := zap.NewExample() MyFunction(logger) } func MyFunction(logger Logger) { // Injects an interface logger.Info(“Hello!”) } Render Your Go Code Clean: Introduction to Dependency Injection with Fx 12
Dependency Injection | Benefits ● Loose coupling ● Promotes modularity ● Increased maintainability ○ Easier to swap out implementations without code changes func main() { logger := zap.NewExample() + logger := fancylogger.New() MyFunction(logger) } func MyFunction(logger Logger) { // Injects an interface logger.Info(“Hello!”) } Render Your Go Code Clean: Introduction to Dependency Injection with Fx 13
Dependency Injection | Benefits ● ● ● ● Loose coupling Promotes modularity Increased maintainability Improved testability ○ Dependencies can be easily mocked or stubbed type Logger interface { Info(v …any) } Render Your Go Code Clean: Introduction to Dependency Injection with Fx func Test_MyFunction(t *testing.T) { logger := zaptest.NewLogger(t) MyFunction(logger) … } func Test_MyFunction(t *testing.T) { logger := mocks.NewMockLogger() MyFunction(logger) … } func MyFunction(logger Logger) { // Injects an interface logger.Info(“Hello!”) } func MyFunction(logger Logger) { // Injects an interface logger.Info(“Hello!”) } 14
DI: Examples & Live Demo (code examples) https://t.uber.com/render-demos Render Your Go Code Clean: Introduction to Dependency Injection with Fx 15
Dependency Injection | v1 – No dependency injection func main() { // Create logger logger := zap.NewExample() // Create handler handler := http.HandlerFunc(func(w http.ResponseWriter, r *”http.Request) { logger.Info(“[v1] Handler received request”) if _, err := io.Copy(w, r.Body); err != nil { logger.Warn(“Failed to handle request”, zap.Error(err)) } }) // Register handler logger.Info(“Registering handler”) http.Handle(“/echo”, handler) // Start server logger.Info(“Starting server”) http.ListenAndServe(“:8080”, nil) } Render Your Go Code Clean: Introduction to Dependency Injection with Fx 16
Demo (v1) Render Your Go Code Clean: Introduction to Dependency Injection with Fx 17
Dependency Injection | v2 – Manual dependency injection func main() { logger := NewLogger() handler := NewHandler(logger) RegisterHandler(logger, handler) StartServer(logger) } func NewLogger() *zap.Logger { return zap.NewExample() } func NewHandler(logger *zap.Logger) http.Handler { return http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { logger.Info(“[v2] - Handler received request”) if _, err := io.Copy(w, r.Body); err != nil { logger.Warn(“Failed to handle request”, zap.Error(err)) } }, ) } func StartServer(logger *zap.Logger) { logger.Info(“Starting server”) http.ListenAndServe(“:8080”, nil) } func RegisterHandler(logger *zap.Logger, h http.Handler) { logger.Info(“Registering handler”) http.Handle(“/echo”, h) } Render Your Go Code Clean: Introduction to Dependency Injection with Fx 18
Demo (v2) Render Your Go Code Clean: Introduction to Dependency Injection with Fx 19
Dependency Injection | Cost of manual dependency injection ● Requires writing boilerplate in every service ○ Repetitive and time consuming App A App B func main() { logger := NewLogger() handler := NewHandler(logger) RegisterHandler(logger, handler) StartServer(logger) } func main() { logger := NewLogger() handler := NewHandler(logger) RegisterHandler(logger, handler) StartServer(logger) } Render Your Go Code Clean: Introduction to Dependency Injection with Fx App C func main() { logger := NewLogger() handler := NewHandler(logger) RegisterHandler(logger, handler) StartServer(logger) } 20
Dependency Injection | Cost of manual dependency injection ● Requires writing boilerplate in every service ○ Repetitive and time consuming ● Long-term maintenance burden as application evolves ○ Some adopt quickly, others fall behind; usages diverge over time App A (adopts change) func main() { logger := NewLogger() + logger := NewLogger(zapcore.InfoLevel) handler := NewHandler(logger) RegisterHandler(logger, handler) StartServer(logger) } Render Your Go Code Clean: Introduction to Dependency Injection with Fx App B (falls behind) func main() { logger := NewLogger() handler := NewHandler(logger) RegisterHandler(logger, handler) StartServer(logger) } 21
Dependency Injection | Cost of manual dependency injection ● Requires writing boilerplate in every service ○ Repetitive and time consuming ● Long-term maintenance burden as application evolves ○ Some adopt quickly, others fall behind; usages diverge over time ● Can lead to creation of global state ○ Less effort to maintain (singleton); complicates testing var Logger = zap.NewExample() func MyFunction() { // Uses global state Logger.Info(“Log message”) } Render Your Go Code Clean: Introduction to Dependency Injection with Fx func Test_MyFunction(t *testing.T) { globalLogger := Logger // Override global logger Logger := zaptest.New(t) defer func() { // Revert global logger override Logger = globalLogger }() MyFunction() } 22
Dependency Injection | Cost of manual dependency injection ● Requires writing boilerplate in every service ○ Repetitive and time consuming ● Long-term maintenance burden as application evolves ○ Some adopt quickly, others fall behind; usages diverge over time ● Can lead to creation of global state ○ Less effort to maintain (singleton); complicates testing ● Cost multiplies at scale ○ Uber’s hypergrowth demanded smarter, more-efficient solution Render Your Go Code Clean: Introduction to Dependency Injection with Fx 23
Fx github.com/uber-go/fx Render Your Go Code Clean: Introduction to Dependency Injection with Fx 24
Fx | What is Fx? ● A dependency injection framework for Go, built and battle-tested at Uber. ● Provides dependency injection without the manual wiring. v2 – Manual DI func main() { // Create app with manual dependency // injection. logger := NewLogger() handler := NewHandler(logger) RegisterHandler(logger, handler) StartServer(logger) } Render Your Go Code Clean: Introduction to Dependency Injection with Fx v3 – Fx func main() { // Create Fx app. fx.New( fx.Provide(NewLogger), fx.Provide(NewHandler), fx.Invoke(RegisterHandler), fx.Invoke(StartServer), ).Run() } 25
Demo (v3) Render Your Go Code Clean: Introduction to Dependency Injection with Fx 26
Fx | The magic Connecting providers to receivers Providers Receivers “Here’s an instance of component X” “I need an instance of component X” // NewLogger returns a logger. func NewLogger() *log.Logger { return log.New(os.Stdout, “”, 0) } Render Your Go Code Clean: Introduction to Dependency Injection with Fx // NewHandler receives a logger as a dependency. func NewHandler(logger *log.Logger) http.Handler { logger.Print(“Log message”) } 27
Fx | Provide & Invoke Core building blocks Provide Invoke “Registers a function with Fx lifecycle” “Executes a function during Fx lifecycle” func main() { fx.New( fx.Provide(NewLogger) … ).Run() } func main() { fx.New( fx.Invoke(NewLogger) … ).Run() } ● Provides are only executed as necessary (i.e., if they have a receiver). ● Invokes are always executed. Render Your Go Code Clean: Introduction to Dependency Injection with Fx 28
Fx | Provide & Invoke Fx’s core building blocks Revisiting example v3 No return values → No receivers Why Provide vs Invoke? func main() { // Create Fx app. fx.New( fx.Provide(NewLogger), fx.Provide(NewHandler), fx.Invoke(RegisterHandler), fx.Invoke(StartServer), ).Run() } func RegisterHandler(h http.Handler) { http.Handle(“/echo”, h) } func StartServer() { http.ListenAndServe(“:8080”, nil) } ● Core business logic is typically invoked. Render Your Go Code Clean: Introduction to Dependency Injection with Fx 29
Fx | Fx Lifecycle Two high-level phases: initialization and execution. Initialization: register constructors and decorators, run invoked functions Execution: run all startup hooks, wait for stop signal , run all shutdown hooks Render Your Go Code Clean: Introduction to Dependency Injection with Fx 30
Fx | Lifecycle Hooks Lifecycle hooks provide the ability to schedule work to be executed by Fx when the application starts up or shuts down. Fx allows two kinds of hooks: ● OnStart hooks, run in the order they were appended at Start ○ Example: Start HTTP server ● OnStop hooks, run in the reverse order they were appended at Stop ○ Example: Shutdown HTTP server Render Your Go Code Clean: Introduction to Dependency Injection with Fx 31
Fx | Modules Sharable bundles of one or more components Logger as a Module Using Logger Module Library module names should end in -fx Replace fx.Provide with Fx module package loggerfx var Module = fx.Options( fx.Provide(NewLogger), ) // NewLogger returns a logger. func NewLogger() *log.Logger { return log.New(os.Stdout, “”, 0) } Render Your Go Code Clean: Introduction to Dependency Injection with Fx func main() { // Create Fx app. fx.New( + loggerfx.Module, fx.Provide(NewLogger), … ).Run() } 32
Fx | Parameter objects Functions exposed by a module should not accept dependencies directly as parameters. Instead, they should use a parameter object. This allows new optional dependencies to be added in a backwards-compatible manner. type Params struct { fx.In
optional:"true"
func NewLogger(p Params) (Result, error) { …
Render Your Go Code Clean: Introduction to Dependency Injection with Fx
33Fx | Result objects Functions exposed by a module should not declare their results as regular return values. Instead, they should use a result object. This allows new results to be added in a backwards-compatible manner. type Result struct { fx.Out Logger *log.Logger … } func NewLogger(p Params) (Result, error) { … Render Your Go Code Clean: Introduction to Dependency Injection with Fx 34
Fx | Modules Sharable bundles of one or more components Module bundle Using Module bundle* Provide…all the things! Complex scaffolding made easy package uberfx var Module = fx.Options( loggerfx.Module, metricsfx.Module, rpcfx.Module, serverfx.Module, storagefx.Module, … ) * func main() { // Create Fx app. fx.New( uberfx.Module, … ).Run() } Useful for adding/deprecating shared libraries without modifying main. Render Your Go Code Clean: Introduction to Dependency Injection with Fx 35
Fx |
Value Groups Fx does not allow two instances of the same type to be present in the container. A value group is… ● a collection of values of the same type. ● defined using the “group” annotation. ○ Must be used on both the input parameter slice and output result. type Params struct { fx.In
type Result struct { fx.Out
Route []Route group:"routes"
}
Render Your Go Code Clean: Introduction to Dependency Injection with Fx
Route Route group:"routes"
}
36
Fx | Value Groups ● Any number of constructors can feed values into a value group. ● Any number of consumers can read from a value group. Render Your Go Code Clean: Introduction to Dependency Injection with Fx 37
Fx | Pros ● ● ● ● Eliminate globals ○ Helps remove globally shared state Increase efficiency ○ Less boilerplate code → Less repetitive work No manual wiring ○ Eliminates need to manually wire up dependencies Code reuse ○ Build loosely coupled, well-integrated sharable modules Render Your Go Code Clean: Introduction to Dependency Injection with Fx Cons ● ● ● Steeper learning curve ○ Introduces complexity harder to grasp for new developers Loss of control flow ○ Framework controls order of execution Harder to debug ○ Missing dependencies become runtime errors 38
Q&A Render Your Go Code Clean: Introduction to Dependency Injection with Fx 39
Thank you! Dorian Perkins Render Your Go Code Clean: Introduction to Dependency Injection with Fx Kemet Dugue Paul Murage 40