SOLID Principles in Go: Theory and Practice from A to Z
SOLID is an acronym for five fundamental principles of object-oriented design formulated by Robert Martin (Uncle Bob). Despite the fact that Go is not a classic object-oriented language (it has no classes or inheritance in the traditional sense), these principles are applicable and critically important for creating high-quality, maintainable, and scalable code. In this article, we will break down each SOLID principle with examples from real Go practice.
1. Single Responsibility Principle (SRP)
Essence: Each module, structure, or function should have only one reason to change. In other words, they should be responsible for one clearly defined task.
Problem: Developers often place all logic into a single structure: validation, database saving, email sending, response formatting. This leads to "God Objects" that are difficult to test and modify.
Solution in Go: Separate responsibilities across different structures and packages.
// ❌ Bad: One structure does everythingtype UserService struct { db *sql.DB mailer *Mailer}
func (s *UserService) CreateUser(name, email string) error { // 1. Validation if !strings.Contains(email, "@") { return errors.New("invalid email") } // 2. Database save _, err := s.db.Exec("INSERT INTO users (name, email) VALUES ($1, $2)", name, email) if err != nil { return err } // 3. Send welcome email return s.mailer.Send(email, "Welcome!", "Thank you for registering")}
// ✅ Good: Separation of responsibilitiestype UserValidator struct{}func (v *UserValidator) Validate(email string) error { if !strings.Contains(email, "@") { return errors.New("invalid email") } return nil}
type UserRepository struct { db *sql.DB}func (r *UserRepository) Save(name, email string) error { _, err := r.db.Exec("INSERT INTO users (name, email) VALUES ($1, $2)", name, email) return err}
type WelcomeMailer struct { mailer *Mailer}func (m *WelcomeMailer) Send(email string) error { return m.mailer.Send(email, "Welcome!", "Thank you for registering")}
type UserService struct { validator *UserValidator repo *UserRepository mailer *WelcomeMailer}
func (s *UserService) CreateUser(name, email string) error { if err := s.validator.Validate(email); err != nil { return err } if err := s.repo.Save(name, email); err != nil { return err } return s.mailer.Send(email)}Practical tip: If you cannot briefly describe what your function or method does without using the conjunction "and", SRP is likely violated.
2. Open-Closed Principle (OCP)
Essence: Software entities (structures, functions, packages) should be open for extension but closed for modification. This means you should be able to add new behavior without changing existing code.
Solution in Go: Use interfaces to abstract behavior.
// ❌ Bad: Adding a new payment type requires modifying the ProcessPayment functiontype PaymentProcessor struct{}
func (p *PaymentProcessor) Process(paymentType string, amount float64) { if paymentType == "credit_card" { // logic for credit card } else if paymentType == "paypal" { // logic for PayPal } // When adding "crypto" — need to add a new else if}
// ✅ Good: Interface allows extending functionality without changing codetype PaymentMethod interface { Pay(amount float64) error}
type CreditCard struct{}func