SOLID Principles in Go: Advanced Level
SOLID is a set of five fundamental object-oriented design principles that help create flexible, maintainable, and scalable code. In the context of Go, a language with a unique philosophy of composition over inheritance, applying SOLID requires a special approach. In this article, we will break down each principle at an advanced level, with real-world examples and antipatterns characteristic of the Go community.
1. Single Responsibility Principle (SRP) in Go
The Single Responsibility Principle states: a class (struct) should have only one reason to change. In Go, this is often violated in utility packages and "god objects".
Antipattern: Struct with multiple responsibilities
type UserService struct { db *sql.DB cache *redis.Client logger *log.Logger}
func (s *UserService) CreateUser(ctx context.Context, u User) error { // 1. Validation // 2. Password hashing // 3. Saving to DB // 4. Sending email // 5. Logging return nil}Solution: separate responsibilities into individual services and use composition.
type UserValidator interface { Validate(u User) error}
type PasswordHasher interface { Hash(password string) (string, error)}
type UserRepository interface { Save(ctx context.Context, u User) error}
type EmailSender interface { SendWelcomeEmail(ctx context.Context, email string) error}
type UserService struct { validator UserValidator hasher PasswordHasher repo UserRepository emailSender EmailSender logger Logger}Each component now has one reason to change, simplifying testing and maintenance.
2. Open-Closed Principle (OCP) via Interfaces
The Open-Closed Principle: entities should be open for extension but closed for modification. In Go, this is achieved through interfaces and dependency injection.
Bad example: Tight coupling
type PaymentProcessor struct { paypalAPI *PayPalAPI}
func (p *PaymentProcessor) Process(amount float64) error { return p.paypalAPI.Charge(amount)}If you need to add Stripe — you have to change the code.
Correct approach: Interface + Strategy
type PaymentGateway interface { Charge(amount float64) error Refund(transactionID string) error}
type PayPalGateway struct{ /* ... */ }type StripeGateway struct{ /* ... */ }
func (p *PayPalGateway) Charge(amount float64) error { /* ... */ }func (s *StripeGateway) Charge(amount float64) error { /* ... */ }
type PaymentService struct { gateway PaymentGateway}
func NewPaymentService(gw PaymentGateway) *PaymentService { return &PaymentService{gateway: gw}}Now you can add new payment gateways without changing PaymentService.
3. Liskov Substitution Principle (LSP) in the Context of Go
The Liskov Substitution Principle: objects in a program should be replaceable with instances of their subtypes without altering the correctness of the program. In Go, where there is no classical inheritance, LSP manifests through interface contracts.
Typical violation: Interface with redundant methods
type Bird interface { Fly() error Swim() error}
type Penguin struct{}
func (p *Penguin) Fly() error { return errors.New("penguins can't fly")}
func (p *Penguin) Swim() error { return nil}Here, Penguin cannot correctly implement Fly(), which violates LSP.
Solution: Interface segregation
type Flyer interface { Fly() error}
type Swimmer interface { S