DEV Community

Dzung Nguyen
Dzung Nguyen

Posted on

Golang Dependency Injection - Just in 5 Minutes!

When building large applications, managing dependencies effectively is critical for keeping your code flexible, testable, and maintainable. Dependency Injection (DI) is a method that helps you decouple components, making it easier to change dependencies without breaking your application. In this post, you’ll learn how to use DI in Go through a simple example.

Image description

Why Dependency Injection Matters: A Real-Life Example

Imagine you're running an online store. You have a main service (the OrderService) that handles customer orders. Each time an order is placed, the system needs to send a notification (like an email or SMS) to the customer. However, the notification method could change based on user preferences — sometimes they want an email, other times they might prefer an SMS.

Without Dependency Injection, the OrderService would be tightly coupled to one specific notification method, making it difficult to adapt if you want to introduce a new way of notifying customers (e.g., push notifications or in-app messages).

Image description

Here’s where Dependency Injection (DI) shines: it allows your OrderService to be independent of the actual notification method used. Instead of being locked into a specific type of notification, DI allows you to inject the notification dependency (like an EmailNotifier or SMSNotifier) into the OrderService, making the application much more flexible and easier to maintain.

Core Idea

Dependency Injection means letting your application decide which tool (like an email or SMS service) to use at runtime, instead of hardcoding this logic into the core service (like OrderService). This enables easy swapping of notification methods without changing the business logic of order placement.

Dependency Injection in Go: A Code Example

Let’s build an example where an OrderService sends notifications to users. Instead of being tightly coupled to an EmailService, we’ll use Dependency Injection to make it flexible and testable.

Step 1: Define a Notifier Interface
Create an interface that defines a general contract for sending notifications.

type Notifier interface {
    Notify(recipient string, message string)
}
Enter fullscreen mode Exit fullscreen mode

This abstraction lets us use any implementation of Notifier (like email or SMS) without changing the code that uses it.

Step 2: Implement an EmailNotifier

type EmailNotifier struct {}

func (e *EmailNotifier) Notify(recipient string, message string) {
    fmt.Printf("Sending email to %s: %s\n", recipient, message)
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Use Dependency Injection in OrderService

type OrderService struct {
    notifier Notifier
}

func NewOrderService(notifier Notifier) *OrderService {
    return &OrderService{notifier: notifier}
}

func (o *OrderService) PlaceOrder(orderID string, customerEmail string) {
    fmt.Printf("Placing order %s\n", orderID)
    o.notifier.Notify(customerEmail, "Your order "+orderID+" has been placed!")
}
Enter fullscreen mode Exit fullscreen mode

Notice that OrderService depends on the Notifier interface, not a specific implementation. We pass the implementation (like EmailNotifier) when creating OrderService.

Step 4: Main Function with Dependency Injection

func main() {
    emailNotifier := &EmailNotifier{}                        // Injecting EmailNotifier
    orderService := NewOrderService(emailNotifier)            // Dependency Injection
    orderService.PlaceOrder("12345", "customer@example.com")  // Using injected dependency
}
Enter fullscreen mode Exit fullscreen mode

Benefits of Dependency Injection

Flexibility: You can switch to an SMSNotifier without changing OrderService:

type SMSNotifier struct {}

func (s *SMSNotifier) Notify(recipient string, message string) {
    fmt.Printf("Sending SMS to %s: %s\n", recipient, message)
}

Enter fullscreen mode Exit fullscreen mode

Just by injecting it:

smsNotifier := &SMSNotifier{}
orderService := NewOrderService(smsNotifier)
Enter fullscreen mode Exit fullscreen mode

Testability: Create a mock Notifier for testing:

type MockNotifier struct {
    messages []string
}

func (m *MockNotifier) Notify(recipient string, message string) {
    m.messages = append(m.messages, fmt.Sprintf("To: %s, Message: %s", recipient, message))
}

Enter fullscreen mode Exit fullscreen mode

Maintainability: Following the Single Responsibility Principle, OrderService only handles order logic, and notification logic is handled elsewhere.

You can check out the complete code example repository here on Github here.

🏁 Conclusion

Dependency Injection lets you build flexible, testable, and maintainable Go applications by decoupling your services from their dependencies. Just like a barista using different coffee machines without changing their process, your services can use different implementations without being rewritten.

Start using DI in your Go projects today to unlock its powerful benefits!

Follow me to stay updated with my future posts:

Top comments (0)