SOLID Principles in Go: Theory and Practice from A to Z
\n\nSOLID is an acronym for five fundamental principles of object-oriented programming that help create flexible, maintainable, and scalable code. Although Go is not a classic OOP language (it has no classes or inheritance in the traditional sense), these principles are applicable and extremely useful when developing in Go. In this article, we will break down each principle with concrete examples, show how to apply them in Go, and explain why this is important.
\n\n1. Single Responsibility Principle (SRP)
\n\nEssence: Each module, class, or function should have only one reason to change. In other words, one entity should be responsible for one part of the functionality.
\n\nIn Go, this is often violated when a struct performs too many tasks: working with a database, sending emails, generating reports.
\n\nBad example (SRP violation):
\ntype User struct {\n Name string\n Email string\n}\n\nfunc (u *User) Save() error {\n // database saving logic\n return nil\n}\n\nfunc (u *User) SendWelcomeEmail() error {\n // email sending logic\n return nil\n}\n\nfunc (u *User) GenerateReport() string {\n // report generation logic\n return "report"\n}\n\n\nGood example (SRP compliance):
\ntype User struct {\n Name string\n Email string\n}\n\ntype UserRepository struct{}\n\nfunc (r *UserRepository) Save(u *User) error {\n // only saving\n return nil\n}\n\ntype EmailService struct{}\n\nfunc (s *EmailService) SendWelcome(u *User) error {\n // only sending email\n return nil\n}\n\ntype ReportGenerator struct{}\n\nfunc (g *ReportGenerator) Generate(u *User) string {\n // only generating report\n return "report"\n}\n\n\nConclusion: Each struct is responsible for its own task. Changing the saving logic will not affect email sending.
\n\n2. Open-Closed Principle (OCP)
\n\nEssence: Software entities (classes, modules, functions) should be open for extension but closed for modification. That is, you should be able to add new functionality without changing existing code.
\n\nIn Go, this is perfectly implemented through interfaces.
\n\nBad example (OCP violation):
\ntype PaymentProcessor struct{}\n\nfunc (p *PaymentProcessor) Process(paymentType string, amount float64) {\n if paymentType == "credit_card" {\n // logic for credit card\n } else if paymentType == "paypal" {\n // logic for PayPal\n }\n // when adding a new type, this method will have to be changed\n}\n\n\nGood example (OCP compliance):
\ntype PaymentMethod interface {\n Pay(amount float64) error\n}\n\ntype CreditCard struct{}\n\nfunc (c *CreditCard) Pay(amount float64) error {\n // logic for credit card\n return nil\n}\n\ntype PayPal struct{}\n\nfunc (p *PayPal) Pay(amount float64) error {\n // logic for PayPal\n return nil\n}\n\nfunc ProcessPayment(method PaymentMethod, amount float64) error {\n return method.Pay(amount)\n}\n\n\nConclusion: Now, to add a new payment method (e.g., ApplePay), you just create a new struct with a Pay() method without touching the existing code.
3. Liskov Substitution Principle (LSP)
\n\nEssence: Objects in a program should be replaceable with instances of their subtypes without altering the correctness of the program. In Go, this means that if a function accepts an interface, any implementation of that interface should work correctly without breaking expectations.
\n\nBad example (LSP violation):
\ntype Bird interface {\n Fly() error\n}\n\nt