Practical SOLID in Golang: Open/Closed Principle

The SOLID principles form the cornerstone of software design, ensuring code remains maintainable, scalable and easy to comprehend. One principle known as OCP states that software entities should be open for extension but closed for modification, meaning we should be able to extend a module's functionality without changing its existing code base.
In this article, we will investigate the Open/Closed Principle in Golang and examine its significance before applying it practically to build robust applications with minimal code changes. By adhering to this principle, applications will become scalable without incurring unnecessary changes.
Understanding the Open/Closed Principle
Bertrand Meyer introduced the Open/Closed Principle in 1988 to emphasize that any class, function, or module should allow new behavior to be added without altering existing source code; this approach increases code maintainability while decreasing bugs and increasing scalability.
Benefits of the Open/Closed Principle
Enhances Maintainability: Prevents unnecessary modifications to existing code.
Promotes Scalability: Allows adding new functionality without breaking current features.
Encourages Reusability: Supports modular code that can be extended easily.
Minimizes Bugs: Reduces risks associated with modifying working code.
Implementing the Open/Closed Principle in Golang
Golang, being a statically typed language, does not support traditional object-oriented inheritance; however, we can implement the Open/Closed Principle through interfaces and composition. These allow adding new functionality without altering existing code.
Example: Violating the Open/Closed Principle
Let’s consider a scenario where we need to calculate the area of different shapes:
package main
import (
"fmt"
)
type Shape struct {
Type string
Width float64
Height float64
Radius float64
}
func CalculateArea(s Shape) float64 {
switch s.Type {
case "rectangle":
return s.Width * s.Height
case "circle":
return 3.14 s.Radius s.Radius
default:
return 0
}
}
func main() {
rectangle := Shape{Type: "rectangle", Width: 10, Height: 5}
circle := Shape{Type: "circle", Radius: 7}
fmt.Println("Rectangle Area:", CalculateArea(rectangle))
fmt.Println("Circle Area:", CalculateArea(circle))
}
Why This Code Violates OCP?
Modifying the function for new shapes: If we need to add a new shape (e.g., a triangle), we must modify the CalculateArea function.
High coupling: The CalculateArea function depends on multiple shape types.
Not scalable: As the application grows, modifications become complex.
Applying the Open/Closed Principle
We can refactor the above implementation using interfaces, making it extensible without modifying existing code.
package main
import (
"fmt"
)
// Step 1: Define an interface
type Shape interface {
Area() float64
}
// Step 2: Implement Rectangle struct
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Step 3: Implement Circle struct
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14 c.Radius c.Radius
}
// Step 4: Generic function to calculate area
func PrintArea(s Shape) {
fmt.Println("Area:", s.Area())
}
func main() {
rectangle := Rectangle{Width: 10, Height: 5}
circle := Circle{Radius: 7}
PrintArea(rectangle)
PrintArea(circle)
}
How This Follows the Open/Closed Principle?
Open for extension– We can add new shapes like Triangle without modifying existing code.
Closed for modification– The PrintArea function does not need to change when a new shape is added.
Encapsulated behavior– Each shape has its own Area() method, reducing dependencies.
Adding New Functionality Without Modification
Let’s extend our system by adding a Triangle without modifying existing programming courses code:
// Step 5: Implement Triangle struct
type Triangle struct {
Base, Height float64
}
func (t Triangle) Area() float64 {
return 0.5 t.Base t.Height
}
func main() {
rectangle := Rectangle{Width: 10, Height: 5}
circle := Circle{Radius: 7}
triangle := Triangle{Base: 8, Height: 6}
PrintArea(rectangle)
PrintArea(circle)
PrintArea(triangle) // New shape added without modifying PrintArea function
}
Best Practices for Using the Open/Closed Principle in Golang
Use Interfaces Effectively: Define behavior in interfaces and implement them in structs.
Leverage Composition: Avoid modifying existing structures; instead, compose new functionalities.
Follow Dependency Inversion: Depend on abstractions (Shape interface) rather than concrete implementations.
Keep Functions Generic: Functions like PrintArea should operate on interfaces rather than specific structs.
Conclusion
Golang's Open/Closed Principle ensures our code remains scalable, maintainable and extensible. By using interfaces and composition to extend functionality without making changes to existing code bases, this reduces risks while eliminating bugs for clean modular design.
Implementing SOLID principles into Golang training will enable you to build applications with robust features that adapt easily to changing requirements without breaking existing functionality. Doing so will also allow for cleaner, more efficient code - something SOLID principles in Go are perfect for.
Subscribe to my newsletter
Read articles from Kelly Gloria directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Kelly Gloria
Kelly Gloria
Hey, I'm Kelly. I am a full-stack developer. I have keen interest in innovate new easy methodologies to solve the problems. I have around 7 years of experience in developing field. Apart from this, I love to play volleyball. I believe in that physical exercise is also important along with the mental exercise.