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.
Example,
type Car struct {
Model string
Year int
}
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")
}
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:
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.
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.
It is a design pattern that helps decouple components and improve testability. In Go, it’s often implemented using interfaces.
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.
First, we define an interface for our notifier. This interface will specify the method for sending notifications.
type Notifier interface {
Send(message string) error
}
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
}
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)
}
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!")
}
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.Notifier
interface for unit testing of the NotificationService
.Notifier
interface without changing the NotificationService
code.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.
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 ConsultationGet 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