Menu
Coddy logo textTech

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 icon

Challenge

Easy

Let'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 a Logger interface that requires a Log(message string) string method. Then create two different logger implementations:
    • ConsoleLogger with a Prefix field—its Log method returns [CONSOLE] [Prefix]: [message]
    • FileLogger with a Filename field—its Log method returns [FILE:[Filename]] [message]
  • service.go: Create an AppService struct that depends on the Logger interface (not a concrete type). Include a constructor function NewAppService that accepts a Logger and returns a pointer to AppService. Give AppService a method called DoWork(task string) string that uses the injected logger to log the message Processing: [task] and returns whatever the logger returns.
  • main.go: Read the configuration from input, create both logger types, inject each one into separate AppService instances, and call DoWork on 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 authentication

Notice 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")
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming