Golang: Struct, Interface And Dependency Injection(DI)

Write Clean, Flexible, and Testable Go Code with Structs and Interfaces!
Jan 9 2025 · 4 min read

Background

In this blog, we will explore when to use structs versus interfaces in Go. We will also look at how to leverage both for Dependency Injection (DI). 

To make these concepts easier to grasp, we’ll use a simple analogy of a Toy Box.

Understanding with a real-world example: Toy Box

Structs

  • Think of a struct as a specific toy in a toy box, like a car. 
  • The car has specific features, like its color, size, and type (e.g., sports car). 
  • In programming, a struct holds data about an object.

Interfaces

  • An interface is like a toy box that can hold any type of toy. 
  • It defines what toys can do, like roll, make noise, or light up. Any toy that can perform these actions can fit in the toy box. 
  • In programming, an interface defines a set of methods that different types(struct) can implement.

Dependency Injection 

  • Imagine a child who plays with toys. Instead of the child only being able to play with one specific toy, you let them choose any toy from the toy box whenever they want. 
  • This is like dependency injection, where you provide a function or class with the tools (or dependencies) it needs to work, allowing for flexibility.

Understanding the Basics

Structs

  • Definition: A struct is a way to define a new type with specific fields.
  • Purpose: Useful for modeling data structures and encapsulating data and behavior within a single unit.

Example,

type Car struct {
    Model string
    Year  int
}

Interfaces

  • Definition: An interface defines a set of methods that a type must implement.
  • Purpose: Crucial for polymorphism and decoupling components, enabling generic programming.

Example,


type CarInterface interface {
    Start()
    Stop()
}

Implement CarInterface using Car struct,

func (c *Car) Start() {
    fmt.Println("Car started")
}

func (c *Car) Stop() {
    fmt.Println("Car stopped")
}

When to Use Which?

Use Structs When

  • You need to model a specific data structure with defined fields.
  • You want to encapsulate data and behavior within a single unit.

Use Interfaces When

  • You want to define a contract that multiple types can implement.
  • You need to decouple components and make your code more flexible and testable.
  • You want to leverage polymorphism to write generic code.

Balancing Flexibility and Performance

While interfaces provide flexibility, dynamic method calls can introduce overhead. 

Structs, on the other hand, offer performance advantages due to static type checking and direct method calls. Below are the ways to strike the balance:

Interface Composition

Combine multiple interfaces to create more specific interfaces. For example, consider a file system interface:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

Now, we can create a more specific interface ReadWrite, by composing Reader and Writer:

type ReadWrite interface {
    Reader
    Writer
}

Benefit: This approach promotes modularity, reusability, and flexibility in your code.

Interface Embedding

Embed interfaces within structs to inherit their methods. For example, consider a logging interface:

type Logger interface {
    Log(message string)
}

Now, we can create a more specific interface, ErrorLogger, which embeds the Logger interface:

type ErrorLogger interface {
    Logger
    LogError(err error)
}

Any type that implements the ErrorLogger interface must also implement the Log method inherited from the embedded Logger interface.

type ConsoleLogger struct{}

func (cl *ConsoleLogger) Log(message string) {
    fmt.Println(message)
}

func (cl *ConsoleLogger) LogError(err error) {
    fmt.Println("Error:", err)
}

Benefit: This can be used to create hierarchical relationships between interfaces, making your code more concise and expressive.

Dependency Injection

It is a design pattern that helps decouple components and improve testability. In Go, it’s often implemented using interfaces.

Example: Notification System

In this example, we will define a notification service that can send messages through different channels. We will use DI to allow the service to work with any notification method.

Step 1: Define the Notifier Interface

First, we define an interface for our notifier. This interface will specify the method for sending notifications.

type Notifier interface {
    Send(message string) error
}

Step 2: Implement Different Notifiers

Next, we create two implementations of the Notifier interface: one for sending email notifications and another for sending SMS notifications.

Email Notifier Implementation:

type EmailNotifier struct {
    EmailAddress string
}

func (e *EmailNotifier) Send(message string) error {
    // Simulate sending an email
    fmt.Printf("Sending email to %s: %s\n", e.EmailAddress, message)
    return nil
}

SMS Notifier Implementation:

type SMSNotifier struct {
    PhoneNumber string
}

func (s *SMSNotifier) Send(message string) error {
    // Simulate sending an SMS
    fmt.Printf("Sending SMS to %s: %s\n", s.PhoneNumber, message)
    return nil
}

Step 3: Create the Notification Service

Now, we create a NotificationService that will use the Notifier interface. This service will be responsible for sending notifications.

type NotificationService struct {
    notifier Notifier
}

func NewNotificationService(n Notifier) *NotificationService {
    return &NotificationService{notifier: n}
}

func (ns *NotificationService) Notify(message string) error {
    return ns.notifier.Send(message)
}

Step 4: Use Dependency Injection in the Main Function

In the main function, we will create instances of the notifiers and inject them into the NotificationService.

func main() {
    // Create an email notifier
    emailNotifier := &EmailNotifier{EmailAddress: "john@example.com"}
    emailService := NewNotificationService(emailNotifier)
    emailService.Notify("Hello via Email!")

    // Create an SMS notifier
    smsNotifier := &SMSNotifier{PhoneNumber: "123-456-7890"}
    smsService := NewNotificationService(smsNotifier)
    smsService.Notify("Hello via SMS!")
}

Benefits of This Approach

  • Decoupling: The NotificationService does not depend on specific implementations of notifiers. It only relies on the Notifier interface, making it easy to add new notification methods in the future.
  • Testability: You can easily create mock implementations of the Notifier interface for unit testing of the NotificationService.
  • Flexibility: If you want to add a new notification method (like push notifications), you can create a new struct that implements the Notifier interface without changing the NotificationService code.

Conclusion

In conclusion, understanding when to use structs versus interfaces is essential for writing clean, maintainable, and testable code in Go. 

By leveraging both concepts along with Dependency Injection, we can create flexible and robust applications.

Similar Articles


Code, Build, Repeat.
Stay updated with the latest trends and tutorials in Android, iOS, and web development.
nidhi-d image
Nidhi Davra
Web developer@canopas | Gravitated towards Web | Eager to assist
nidhi-d image
Nidhi Davra
Web developer@canopas | Gravitated towards Web | Eager to assist
canopas-logo
We build products that customers can't help but love!
Get in touch
background-image

Get started today

Let's build the next
big thing!

Let's improve your business's digital strategy and implement robust mobile apps to achieve your business objectives. Schedule Your Free Consultation Now.

Get Free Consultation
footer
Follow us on
2025 Canopas Software LLP. All rights reserved.