SOLID Principles in Go: Advanced Guide with Examples

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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

Blogs

Book Recommendations