Render Your Go Code Clean: Introduction to Dependency Injection with Fx

A presentation at Render ATL Conference in July 2024 in Atlanta, GA, USA by Kemet Dugue

Slide 1

Slide 1

Render Your Go Code Clean: Introduction to Dependency Injection with Fx RenderATL 2024 Workshop

Slide 2

Slide 2

Agenda 01 Introduction 02 Dependency Injection 03 Fx Framework (5 minutes) (20 minutes) (25 minutes)

      • Break: 10 minutes - - - 04 Project Overview 05 Hands on time! 06 Wrap up + feedback (5 minutes) Render Your Go Code Clean: Introduction to Dependency Injection with Fx (5 minutes) 2

Slide 3

Slide 3

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

Slide 4

Slide 4

What to expect in this workshop Render Your Go Code Clean: Introduction to Dependency Injection with Fx 4

Slide 5

Slide 5

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

Slide 6

Slide 6

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

Slide 7

Slide 7

Dependency Injection Render Your Go Code Clean: Introduction to Dependency Injection with Fx 7

Slide 8

Slide 8

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

Slide 9

Slide 9

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

Slide 10

Slide 10

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

Slide 11

Slide 11

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

Slide 12

Slide 12

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

Slide 13

Slide 13

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

Slide 14

Slide 14

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

Slide 15

Slide 15

DI: Examples & Live Demo (code examples) https://t.uber.com/render-demos Render Your Go Code Clean: Introduction to Dependency Injection with Fx 15

Slide 16

Slide 16

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

Slide 17

Slide 17

Demo (v1) Render Your Go Code Clean: Introduction to Dependency Injection with Fx 17

Slide 18

Slide 18

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

Slide 19

Slide 19

Demo (v2) Render Your Go Code Clean: Introduction to Dependency Injection with Fx 19

Slide 20

Slide 20

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

Slide 21

Slide 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 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

Slide 22

Slide 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 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

Slide 23

Slide 23

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

Slide 24

Slide 24

Fx github.com/uber-go/fx Render Your Go Code Clean: Introduction to Dependency Injection with Fx 24

Slide 25

Slide 25

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

Slide 26

Slide 26

Demo (v3) Render Your Go Code Clean: Introduction to Dependency Injection with Fx 26

Slide 27

Slide 27

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

Slide 28

Slide 28

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

Slide 29

Slide 29

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

Slide 30

Slide 30

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

Slide 31

Slide 31

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

Slide 32

Slide 32

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

Slide 33

Slide 33

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

  • } LogLevel Name *zapcore.Level string optional:"true" func NewLogger(p Params) (Result, error) { … Render Your Go Code Clean: Introduction to Dependency Injection with Fx 33

Slide 34

Slide 34

Fx | 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

Slide 35

Slide 35

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

Slide 36

Slide 36

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

Slide 37

Slide 37

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

Slide 38

Slide 38

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

Slide 39

Slide 39

Q&A Render Your Go Code Clean: Introduction to Dependency Injection with Fx 39

Slide 40

Slide 40

Thank you! Dorian Perkins Render Your Go Code Clean: Introduction to Dependency Injection with Fx Kemet Dugue Paul Murage 40