Dependency Injection
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 45 of 107.
Dependency Injection is a technique where a struct receives its dependencies from the outside rather than creating them internally. In Go, interfaces make this pattern natural and powerful.
Instead of hardcoding a specific implementation inside a struct, you accept an interface. This lets you swap implementations without changing the struct's code:
type Notifier interface {
Send(message string) string
}
type EmailNotifier struct{}
func (e EmailNotifier) Send(message string) string {
return "Email: " + message
}
type SMSNotifier struct{}
func (s SMSNotifier) Send(message string) string {
return "SMS: " + message
}
Now create a struct that depends on the interface, not a concrete type:
type OrderService struct {
notifier Notifier // dependency injected via interface
}
func NewOrderService(n Notifier) *OrderService {
return &OrderService{notifier: n}
}
func (o *OrderService) PlaceOrder(item string) string {
return o.notifier.Send("Order placed: " + item)
}
The OrderService doesn't know or care whether it's using email or SMS. You inject the dependency when creating the service:
func main() {
emailService := NewOrderService(EmailNotifier{})
fmt.Println(emailService.PlaceOrder("Book")) // Email: Order placed: Book
smsService := NewOrderService(SMSNotifier{})
fmt.Println(smsService.PlaceOrder("Phone")) // SMS: Order placed: Phone
}
This approach makes your code more flexible and testable. During testing, you can inject a mock notifier that doesn't actually send messages. In production, you inject the real implementation. The struct remains unchanged in both scenarios.
Challenge
EasyLet's build a logging system that demonstrates the power of dependency injection. You'll create a service that can write logs to different destinations—without the service knowing or caring where those logs actually go.
You'll organize your code across three files:
logger.go: Define aLoggerinterface that requires aLog(message string) stringmethod. Then create two different logger implementations:ConsoleLoggerwith aPrefixfield—itsLogmethod returns[CONSOLE] [Prefix]: [message]FileLoggerwith aFilenamefield—itsLogmethod returns[FILE:[Filename]] [message]
service.go: Create anAppServicestruct that depends on theLoggerinterface (not a concrete type). Include a constructor functionNewAppServicethat accepts aLoggerand returns a pointer toAppService. GiveAppServicea method calledDoWork(task string) stringthat uses the injected logger to log the messageProcessing: [task]and returns whatever the logger returns.main.go: Read the configuration from input, create both logger types, inject each one into separateAppServiceinstances, and callDoWorkon each service with the provided task. Print the result from each service call.
The following inputs will be provided:
- Line 1: Console logger prefix
- Line 2: File logger filename
- Line 3: Task to process
For example, given INFO, app.log, and user authentication, your output should be:
[CONSOLE] INFO: Processing: user authentication
[FILE:app.log] Processing: user authenticationNotice how AppService doesn't know whether it's logging to a console or a file—it simply calls Log on whatever logger was injected. This flexibility is the essence of dependency injection: the same service code works with completely different logging implementations.
Cheat sheet
Dependency Injection is a technique where a struct receives its dependencies from the outside rather than creating them internally. In Go, interfaces make this pattern natural and powerful.
Define an interface for the dependency:
type Notifier interface {
Send(message string) string
}
Create concrete implementations of the interface:
type EmailNotifier struct{}
func (e EmailNotifier) Send(message string) string {
return "Email: " + message
}
type SMSNotifier struct{}
func (s SMSNotifier) Send(message string) string {
return "SMS: " + message
}
Create a struct that depends on the interface, not a concrete type:
type OrderService struct {
notifier Notifier // dependency injected via interface
}
func NewOrderService(n Notifier) *OrderService {
return &OrderService{notifier: n}
}
func (o *OrderService) PlaceOrder(item string) string {
return o.notifier.Send("Order placed: " + item)
}
Inject the dependency when creating the service:
emailService := NewOrderService(EmailNotifier{})
fmt.Println(emailService.PlaceOrder("Book")) // Email: Order placed: Book
smsService := NewOrderService(SMSNotifier{})
fmt.Println(smsService.PlaceOrder("Phone")) // SMS: Order placed: Phone
This approach makes code more flexible and testable by allowing different implementations to be swapped without changing the struct's code.
Try it yourself
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Read console logger prefix
prefix, _ := reader.ReadString('\n')
prefix = prefix[:len(prefix)-1]
// Read file logger filename
filename, _ := reader.ReadString('\n')
filename = filename[:len(filename)-1]
// Read task to process
task, _ := reader.ReadString('\n')
if len(task) > 0 && task[len(task)-1] == '\n' {
task = task[:len(task)-1]
}
// TODO: Create a ConsoleLogger with the prefix
// TODO: Create a FileLogger with the filename
// TODO: Create an AppService with the ConsoleLogger injected
// TODO: Create another AppService with the FileLogger injected
// TODO: Call DoWork on each service with the task and print the results
fmt.Println("result1")
fmt.Println("result2")
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool