DEV Community

Aditya
Aditya

Posted on

Mastering SOLID Principles in Go: Writing Clean and Maintainable Code

In software development, building maintainable, scalable, and robust code is the ultimate goal. The SOLID principles, coined by Robert C. Martin (Uncle Bob), provide a foundation for achieving this in object-oriented programming. But how do these principles apply to Go (Golang), a language known for its simplicity and pragmatism? Let’s explore how Go’s idiomatic style aligns with SOLID principles to produce clean, efficient software.


Single Responsibility Principle (SRP)

"A class should have only one reason to change."

In Go, SRP translates to designing functions, structs, and packages with a single responsibility. This ensures code is easier to understand, test, and maintain.

Example

Violating SRP:

func (us *UserService) RegisterUser(username, password string) error {
  // Save user to database
  // Send confirmation email
  // Log registration event
  return nil
}
Enter fullscreen mode Exit fullscreen mode

This function handles multiple responsibilities: saving a user, sending an email, and logging events. Changes in any of these areas would require modifying this function.

Following SRP:

type UserService struct {
  db Database
  email EmailService
  logger Logger
}

func (us *UserService) RegisterUser(username, password string) error {
  if err := us.db.SaveUser(username, password); err != nil {
    return err
  }
  if err := us.email.SendConfirmation(username); err != nil {
    return err
  }
  us.logger.Log("User registered: " + username)
  return nil
}
Enter fullscreen mode Exit fullscreen mode

Here, each responsibility is delegated to a specific component, making the code modular and testable.


Open/Closed Principle (OCP)

"Software entities should be open for extension but closed for modification."

Go achieves OCP through interfaces and composition, allowing behavior to be extended without altering existing code.

Example

Violating OCP:

func (p *PaymentProcessor) ProcessPayment(method string) {
  if method == "credit_card" {
    fmt.Println("Processing credit card payment")
  } else if method == "paypal" {
    fmt.Println("Processing PayPal payment")
  }
}
Enter fullscreen mode Exit fullscreen mode

Adding a new payment method requires modifying the ProcessPayment function, which violates OCP.

Following OCP:

type PaymentMethod interface {
  Process()
}

type CreditCard struct {}
func (cc CreditCard) Process() { fmt.Println("Processing credit card payment") }

type PayPal struct {}
func (pp PayPal) Process() { fmt.Println("Processing PayPal payment") }

func (p PaymentProcessor) ProcessPayment(method PaymentMethod) {
  method.Process()
}
Enter fullscreen mode Exit fullscreen mode

Now, adding a new payment method only requires implementing the PaymentMethod interface, leaving existing code untouched.


Liskov Substitution Principle (LSP)

"Subtypes must be substitutable for their base types."

In Go, LSP is achieved by designing interfaces that focus on behavior rather than structure.

Example

Violating LSP:

type Rectangle struct {
  Width, Height float64
}

type Square struct {
  Side float64
}

func SetDimensions(shape *Rectangle, width, height float64) {
  shape.Width = width
  shape.Height = height
}
Enter fullscreen mode Exit fullscreen mode

Passing a Square to this function would break its constraints, as a square must have equal width and height.

Following LSP:

type Shape interface {
  Area() float64
}

type Rectangle struct {
  Width, Height float64
}
func (r Rectangle) Area() float64 { return r.Width * r.Height }

type Square struct {
  Side float64
}
func (s Square) Area() float64 { return s.Side * s.Side }

func PrintArea(shape Shape) {
  fmt.Printf("Area: %.2f\n", shape.Area())
}
Enter fullscreen mode Exit fullscreen mode

Both Rectangle and Square can implement Shape without violating their constraints, ensuring substitutability.


Interface Segregation Principle (ISP)

"Clients should not be forced to depend on interfaces they do not use."

Go’s lightweight interfaces naturally align with ISP by encouraging small, focused interfaces.

Example

Violating ISP:

type Worker interface {
  Work()
  Eat()
  Sleep()
}
Enter fullscreen mode Exit fullscreen mode

Robots implementing this interface would have unused methods like Eat and Sleep.

Following ISP:

type Worker interface { Work() }
type Eater interface { Eat() }
type Sleeper interface { Sleep() }
Enter fullscreen mode Exit fullscreen mode

Each type implements only the interfaces it needs, avoiding unnecessary dependencies.


Dependency Inversion Principle (DIP)

"High-level modules should depend on abstractions, not on details."

Go’s interfaces make it easy to decouple high-level logic from low-level implementations.

Example

Violating DIP:

type NotificationService struct {
  emailSender EmailSender
}

func (ns *NotificationService) NotifyUser(message string) {
  ns.emailSender.SendEmail(message)
}
Enter fullscreen mode Exit fullscreen mode

Here, NotificationService is tightly coupled to EmailSender.

Following DIP:

type Notifier interface {
  Notify(message string)
}

type NotificationService struct {
  notifier Notifier
}

func (ns *NotificationService) NotifyUser(message string) {
  ns.notifier.Notify(message)
}
Enter fullscreen mode Exit fullscreen mode

This allows swapping EmailSender with other implementations like SMSSender without modifying NotificationService.


By embracing SOLID principles, Go developers can write clean, maintainable, and scalable code. Start small, refactor often, and let Go’s simplicity guide you toward better software design.

Top comments (6)

Collapse
 
dionyosh profile image
Dion Yosh

This is great, I only learnt oop I thought c was hard that I didn't think of trying it but now I'm trying to learn procedural programing by build a go api backend system using only standard libraries and maybe in the future do a system using c

Collapse
 
adi73 profile image
Aditya

Thanks @dionyosh, I have been working on an extensive go library which can be used to build backend systems. Do check it out - github.com/nandlabs/golly

Collapse
 
deividas_strole profile image
Deividas Strole

Thank you for very detailed and informative article! Great job!

Collapse
 
adi73 profile image
Aditya
Collapse
 
citronbrick profile image
CitronBrick

Tangential to your article, but I really love the assignment + test if operator in GoLang.
This post reminded me of it.

Collapse
 
adi73 profile image
Aditya

Yeah, Golang has a lot to love about it!