Introduction: Why SOLID is Important in Go?
SOLID is a set of five fundamental principles of object-oriented programming that help create flexible, maintainable, and extensible code. Although Go is not a classic OOP language (it has no classes or inheritance), these principles apply perfectly thanks to interfaces, composition, and strong typing. In this article, we will break down each SOLID principle with examples in Go, so you can write higher quality and more scalable code.
1. Single Responsibility Principle (SRP)
SRP states: "A module should have only one reason to change". In Go, this means that a function, struct, or package should be responsible for one clearly defined task.
Bad example (SRP violation):
type Report struct {
Title string
Body string
}
func (r *Report) Generate() string {
return fmt.Sprintf("<h1>%s</h1><p>%s</p>", r.Title, r.Body)
}
func (r *Report) SaveToFile(filename string) error {
return os.WriteFile(filename, []byte(r.Generate()), 0644)
}
func (r *Report) SendEmail(addr string) error {
// email sending logic
return nil
}
Here the Report struct is responsible for generation, saving, and sending — three different reasons to change.
Good example (SRP compliance):
type Report struct {
Title string
Body string
}
func (r *Report) Generate() string {
return fmt.Sprintf("<h1>%s</h1><p>%s</p>", r.Title, r.Body)
}
type ReportSaver struct{}
func (rs *ReportSaver) SaveToFile(report *Report, filename string) error {
return os.WriteFile(filename, []byte(report.Generate()), 0644)
}
type EmailSender struct{}
func (es *EmailSender) Send(report *Report, addr string) error {
// email sending logic
return nil
}
Now each entity has a single responsibility: generation, saving, and sending.
2. Open/Closed Principle (OCP)
OCP states: "Software entities should be open for extension, but closed for modification". In Go, this is achieved through interfaces and composition.
Bad example (OCP violation):
type PaymentProcessor struct{}
func (p *PaymentProcessor) Process(paymentType string, amount float64) {
if paymentType == "credit_card" {
// credit card processing
} else if paymentType == "paypal" {
// PayPal processing
} else if paymentType == "crypto" {
// cryptocurrency processing
}
}
When adding a new payment type, you would have to modify the Process method.
Good example (OCP compliance):
type PaymentMethod interface {
Pay(amount float64) error
}
type CreditCard struct{}
func (c *CreditCard) Pay(amount float64) error {
// card payment logic
return nil
}
type PayPal struct{}
func (p *PayPal) Pay(amount float64) error {
// PayPal payment logic
return nil
}
type PaymentProcessor struct{}
func (pp *PaymentProcessor) Process(method PaymentMethod, amount float64) error {
return method.Pay(amount)
}
Now to add a new payment method, you simply implement the PaymentMethod interface without touching existing code.
3. Liskov Substitution Principle (LSP)
LSP requires: "Objects in a program should be replaceable with instances of their subtypes without altering the correctness of the program". In Go, this concerns interface implementation.
Bad example (LSP violation):
type Bird interface {
Fly() error
}
type Penguin struct{}
func (p *Penguin) Fly() error {
return errors.New("penguins cannot fly")
}
func LetBirdFly(b Bird) {