SOLID Design in Go
Grace Collins
Solutions Engineer · Leapcell

In software development, building maintainable, scalable, and robust code is the ultimate goal. The SOLID principles, proposed by Robert C. Martin (also known as Uncle Bob), provide a foundation for achieving this goal. How can these principles be applied to the Go programming language? Known for its simplicity and pragmatism, Go allows us to explore how its idiomatic style aligns with SOLID principles to produce clean and efficient software.
Single Responsibility Principle (SRP)
“A class should have only one reason to change.”
In Go, SRP translates into designing functions, structs, and packages with a single responsibility. This ensures that the 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 }
This function handles multiple responsibilities: saving a user, sending an email, and logging an event. Any changes in these domains 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 }
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 implements OCP through interfaces and composition, allowing behavior to be extended without changing 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") } }
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() }
Now, adding a new payment method only requires implementing the PaymentMethod
interface, with no changes to existing code.
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 }
Passing a Square
to this function would break its constraints, since a square’s width and height must always be equal.
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()) }
Both Rectangle
and Square
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, encouraging small and focused interfaces.
Example:
Violating ISP:
type Worker interface { Work() Eat() Sleep() }
A robot implementing this interface would end up with unused methods such as Eat
and Sleep
.
Following ISP:
type Worker interface { Work() } type Eater interface { Eat() } type Sleeper interface { Sleep() }
Each type only implements 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) }
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) }
This allows replacing EmailSender
with other implementations (such as SMSSender
) without modifying NotificationService
.
Conclusion
By embracing the SOLID principles, Go developers can write clean, maintainable, and extensible code. Start small, refactor frequently, and let Go’s simplicity guide you toward better software design.
We are Leapcell, your top choice for hosting Go projects.
Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:
Multi-Language Support
- Develop with Node.js, Python, Go, or Rust.
Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the Documentation!
Follow us on X: @LeapcellHQ