SOLID is an acronym for five object-oriented design principles that help developers write maintainable, scalable, and robust code. While Go doesn’t have traditional classes like object-oriented languages, these principles are still highly applicable and valuable. In this guide, we’ll explore each SOLID principle with practical Go examples.
SOLID stands for:
These principles have become fundamental guidelines for writing clean, maintainable code.
graph LR
A[SOLID Principles] --> B[Single Responsibility]
A --> C[Open/Closed]
A --> D[Liskov Substitution]
A --> E[Interface Segregation]
A --> F[Dependency Inversion]
B --> B1[One class/type<br/>one reason to change]
C --> C1[Open for extension<br/>Closed for modification]
D --> D1[Subtypes must be<br/>substitutable]
E --> E1[Many specific interfaces<br/>better than one general]
F --> F1[Depend on abstractions<br/>not concretions]
style A fill:#e1f5ff
style B fill:#b6d7a8
style C fill:#b6d7a8
style D fill:#b6d7a8
style E fill:#b6d7a8
style F fill:#b6d7a8
Definition: A type, function, or package should have only one reason to change. It should have only one job or responsibility.
SRP can apply at the package level as well as the struct/function level. For example, having a package do too many unrelated things is unidiomatic. Dave Cheney1 notes that packages like utils or common often become dumping grounds for miscellaneous functionality and thus violate SRP by having many reasons to change.
Here’s an example that violates SRP:
package main
import (
"encoding/json"
"fmt"
"os"
)
// User represents a user in the system
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// UserService violates SRP - it handles multiple responsibilities
type UserService struct {
users []User
}
// SaveToFile handles persistence (one responsibility)
func (us *UserService) SaveToFile(filename string) error {
data, err := json.Marshal(us.users)
if err != nil {
return err
}
return os.WriteFile(filename, data, 0644)
}
// ValidateEmail handles validation (another responsibility)
func (us *UserService) ValidateEmail(email string) bool {
// Simple email validation
return len(email) > 0 && contains(email, "@")
}
// SendEmail handles notification (yet another responsibility)
func (us *UserService) SendEmail(user User, message string) error {
fmt.Printf("Sending email to %s: %s\n", user.Email, message)
return nil
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
(len(s) > len(substr) && (s[:len(substr)] == substr ||
contains(s[1:], substr))))
}
func main() {
service := &UserService{
users: []User{
{ID: 1, Name: "John Doe", Email: "[email protected]"},
{ID: 2, Name: "Jane Smith", Email: "[email protected]"},
},
}
// Multiple responsibilities mixed together
service.ValidateEmail("[email protected]")
service.SaveToFile("users.json")
service.SendEmail(service.users[0], "Welcome!")
}
Problem: The UserService has three different responsibilities:
Let’s refactor to follow SRP:
package main
import (
"encoding/json"
"fmt"
"os"
)
// User represents a user in the system
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// UserRepository handles data persistence (single responsibility)
type UserRepository struct {
filename string
}
func NewUserRepository(filename string) *UserRepository {
return &UserRepository{filename: filename}
}
func (ur *UserRepository) Save(users []User) error {
data, err := json.Marshal(users)
if err != nil {
return err
}
return os.WriteFile(ur.filename, data, 0644)
}
func (ur *UserRepository) Load() ([]User, error) {
data, err := os.ReadFile(ur.filename)
if err != nil {
return nil, err
}
var users []User
err = json.Unmarshal(data, &users)
return users, err
}
// EmailValidator handles email validation (single responsibility)
type EmailValidator struct{}
func (ev *EmailValidator) Validate(email string) bool {
return len(email) > 0 && contains(email, "@")
}
// EmailService handles email communication (single responsibility)
type EmailService struct{}
func (es *EmailService) Send(user User, message string) error {
fmt.Printf("Sending email to %s: %s\n", user.Email, message)
return nil
}
// UserService now orchestrates but doesn't implement details
type UserService struct {
repo *UserRepository
validator *EmailValidator
emailer *EmailService
}
func NewUserService(repo *UserRepository, validator *EmailValidator, emailer *EmailService) *UserService {
return &UserService{
repo: repo,
validator: validator,
emailer: emailer,
}
}
func (us *UserService) RegisterUser(name, email string) error {
if !us.validator.Validate(email) {
return fmt.Errorf("invalid email: %s", email)
}
users, _ := us.repo.Load()
newUser := User{
ID: len(users) + 1,
Name: name,
Email: email,
}
users = append(users, newUser)
if err := us.repo.Save(users); err != nil {
return err
}
return us.emailer.Send(newUser, "Welcome to our service!")
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
(len(s) > len(substr) && (s[:len(substr)] == substr ||
contains(s[1:], substr))))
}
func main() {
repo := NewUserRepository("users.json")
validator := &EmailValidator{}
emailer := &EmailService{}
service := NewUserService(repo, validator, emailer)
err := service.RegisterUser("John Doe", "[email protected]")
if err != nil {
fmt.Printf("Error: %v\n", err)
}
}
Benefits:
Before:
graph LR
A[UserService<br/>Before] --> B[Persistence]
A --> C[Validation]
A --> D[Email]
style A fill:#ffcccc
After:
graph LR
A[UserService] --> B[UserRepository]
A --> C[EmailValidator]
A --> D[EmailService]
B --> B1[Persistence Only]
C --> C1[Validation Only]
D --> D1[Email Only]
style A fill:#ffcccc
style B fill:#e1f5ff
style C fill:#e1f5ff
style D fill:#e1f5ff
Definition: Software entities should be open for extension but closed for modification. You should be able to add new functionality without changing existing code.
package main
import "fmt"
// PaymentProcessor violates OCP - needs modification for each new payment type
type PaymentProcessor struct{}
func (pp *PaymentProcessor) ProcessPayment(amount float64, method string) error {
switch method {
case "credit_card":
fmt.Printf("Processing credit card payment: $%.2f\n", amount)
// Credit card processing logic
return nil
case "paypal":
fmt.Printf("Processing PayPal payment: $%.2f\n", amount)
// PayPal processing logic
return nil
case "bank_transfer":
fmt.Printf("Processing bank transfer: $%.2f\n", amount)
// Bank transfer logic
return nil
default:
return fmt.Errorf("unsupported payment method: %s", method)
}
}
func main() {
processor := &PaymentProcessor{}
processor.ProcessPayment(100.0, "credit_card")
processor.ProcessPayment(200.0, "paypal")
}
Problem: To add a new payment method (e.g., cryptocurrency), you must modify the ProcessPayment method, violating OCP.
package main
import "fmt"
// PaymentMethod defines the interface for payment processing
type PaymentMethod interface {
Process(amount float64) error
GetName() string
}
// CreditCardPayment implements credit card payments
type CreditCardPayment struct{}
func (cc *CreditCardPayment) Process(amount float64) error {
fmt.Printf("Processing credit card payment: $%.2f\n", amount)
// Credit card processing logic
return nil
}
func (cc *CreditCardPayment) GetName() string {
return "credit_card"
}
// PayPalPayment implements PayPal payments
type PayPalPayment struct{}
func (pp *PayPalPayment) Process(amount float64) error {
fmt.Printf("Processing PayPal payment: $%.2f\n", amount)
// PayPal processing logic
return nil
}
func (pp *PayPalPayment) GetName() string {
return "paypal"
}
// BankTransferPayment implements bank transfer payments
type BankTransferPayment struct{}
func (bt *BankTransferPayment) Process(amount float64) error {
fmt.Printf("Processing bank transfer: $%.2f\n", amount)
// Bank transfer logic
return nil
}
func (bt *BankTransferPayment) GetName() string {
return "bank_transfer"
}
// CryptocurrencyPayment - NEW payment method added without modifying existing code
type CryptocurrencyPayment struct {
WalletAddress string
}
func (cp *CryptocurrencyPayment) Process(amount float64) error {
fmt.Printf("Processing cryptocurrency payment: $%.2f to %s\n", amount, cp.WalletAddress)
// Cryptocurrency processing logic
return nil
}
func (cp *CryptocurrencyPayment) GetName() string {
return "cryptocurrency"
}
// PaymentProcessor is now closed for modification but open for extension
type PaymentProcessor struct{}
func (pp *PaymentProcessor) ProcessPayment(amount float64, method PaymentMethod) error {
return method.Process(amount)
}
func main() {
processor := &PaymentProcessor{}
// Existing payment methods
processor.ProcessPayment(100.0, &CreditCardPayment{})
processor.ProcessPayment(200.0, &PayPalPayment{})
processor.ProcessPayment(300.0, &BankTransferPayment{})
// New payment method added without modifying PaymentProcessor
processor.ProcessPayment(400.0, &CryptocurrencyPayment{
WalletAddress: "0x1234567890abcdef",
})
}
Benefits:
PaymentProcessor remains stable and doesn’t need changesBefore:
classDiagram
class PaymentProcessor {
+ProcessPayment(amount: float64, method: PaymentMethod) error
}
After:
classDiagram
class PaymentMethod {
<<interface>>
+Process(amount float64) error
+GetName() string
}
class CreditCardPayment {
+Process(amount float64) error
+GetName() string
}
class PayPalPayment {
+Process(amount float64) error
+GetName() string
}
class BankTransferPayment {
+Process(amount float64) error
+GetName() string
}
class CryptocurrencyPayment {
-WalletAddress string
+Process(amount float64) error
+GetName() string
}
class PaymentProcessor {
+ProcessPayment(amount float64, method PaymentMethod) error
}
PaymentMethod <|.. CreditCardPayment : implements
PaymentMethod <|.. PayPalPayment : implements
PaymentMethod <|.. BankTransferPayment : implements
PaymentMethod <|.. CryptocurrencyPayment : implements
PaymentProcessor --> PaymentMethod : uses
Definition: Objects of a supertype should be replaceable with objects of its subtypes without breaking the application. In Go, this means that types implementing an interface should be fully substitutable.
Who is Liskov? Barbara Liskov
package main
import "fmt"
// Bird represents a bird
type Bird interface {
Fly() error
Eat() error
}
// Sparrow implements Bird correctly
type Sparrow struct{}
func (s *Sparrow) Fly() error {
fmt.Println("Sparrow is flying")
return nil
}
func (s *Sparrow) Eat() error {
fmt.Println("Sparrow is eating")
return nil
}
// Penguin violates LSP - it's a bird but can't fly
type Penguin struct{}
func (p *Penguin) Fly() error {
return fmt.Errorf("penguins cannot fly")
}
func (p *Penguin) Eat() error {
fmt.Println("Penguin is eating")
return nil
}
// MakeBirdFly violates LSP - breaks when given a Penguin
func MakeBirdFly(bird Bird) error {
return bird.Fly() // This will fail for Penguin!
}
func main() {
sparrow := &Sparrow{}
penguin := &Penguin{}
// Works fine
MakeBirdFly(sparrow)
// Breaks the contract - penguin can't fly
if err := MakeBirdFly(penguin); err != nil {
fmt.Printf("Error: %v\n", err)
}
}
Problem: Penguin implements Bird but cannot fulfill the Fly() contract, breaking LSP.
package main
import "fmt"
// Animal represents a basic animal
type Animal interface {
Eat() error
}
// Flyer represents animals that can fly
type Flyer interface {
Animal
Fly() error
}
// Swimmer represents animals that can swim
type Swimmer interface {
Animal
Swim() error
}
// Sparrow can fly and eat
type Sparrow struct{}
func (s *Sparrow) Fly() error {
fmt.Println("Sparrow is flying")
return nil
}
func (s *Sparrow) Eat() error {
fmt.Println("Sparrow is eating")
return nil
}
// Penguin can swim and eat, but not fly
type Penguin struct{}
func (p *Penguin) Swim() error {
fmt.Println("Penguin is swimming")
return nil
}
func (p *Penguin) Eat() error {
fmt.Println("Penguin is eating")
return nil
}
// Duck can fly, swim, and eat
type Duck struct{}
func (d *Duck) Fly() error {
fmt.Println("Duck is flying")
return nil
}
func (d *Duck) Swim() error {
fmt.Println("Duck is swimming")
return nil
}
func (d *Duck) Eat() error {
fmt.Println("Duck is eating")
return nil
}
// MakeAnimalFly only accepts animals that can fly
func MakeAnimalFly(flyer Flyer) error {
return flyer.Fly()
}
// MakeAnimalSwim only accepts animals that can swim
func MakeAnimalSwim(swimmer Swimmer) error {
return swimmer.Swim()
}
func main() {
sparrow := &Sparrow{}
penguin := &Penguin{}
duck := &Duck{}
// All work correctly - each type fulfills its contract
MakeAnimalFly(sparrow)
MakeAnimalSwim(penguin)
MakeAnimalFly(duck)
MakeAnimalSwim(duck)
}
Benefits:
Before:
classDiagram
class Bird {
<<interface>>
+Fly() error
+Eat() error
}
class Sparrow {
+Fly() error
+Eat() error
}
class Penguin {
+Fly() error ⚠️ Returns error
+Eat() error
}
class MakeBirdFly {
<<function>>
+MakeBirdFly(bird Bird) error
}
Bird <|.. Sparrow : implements
Bird <|.. Penguin : implements ⚠️ LSP Violation
MakeBirdFly ..> Bird : uses
note for Penguin "LSP Violation: Penguin cannot fly,<br />breaking the expected behavior<br />of the Bird interface"
note for Bird "Interface assumes all birds can fly,<br />which is not true for all bird types"
After:
classDiagram
class Animal {
<<interface>>
+Eat() error
}
class Flyer {
<<interface>>
+Eat() error
+Fly() error
}
class Swimmer {
<<interface>>
+Eat() error
+Swim() error
}
class Sparrow {
+Fly() error
+Eat() error
}
class Duck {
+Fly() error
+Swim() error
+Eat() error
}
class Penguin {
+Swim() error
+Eat() error
}
class MakeAnimalFly {
<<function>>
+MakeAnimalFly(flyer Flyer) error
}
class MakeAnimalSwim {
<<function>>
+MakeAnimalSwim(swimmer Swimmer) error
}
MakeAnimalFly ..> Flyer : uses
MakeAnimalSwim ..> Swimmer : uses
Animal <|-- Flyer : extends
Animal <|-- Swimmer : extends
Flyer <|.. Sparrow : implements
Flyer <|.. Duck : implements
Swimmer <|.. Duck : implements
Swimmer <|.. Penguin : implements
Definition: Clients should not be forced to depend on interfaces they don’t use. It’s better to have many specific interfaces than one general-purpose interface.
package main
import "fmt"
// Worker interface is too large - violates ISP
type Worker interface {
Work() error
Eat() error
Sleep() error
Code() error
Design() error
Manage() error
}
// Developer is forced to implement all methods, even unused ones
type Developer struct{}
func (d *Developer) Work() error {
fmt.Println("Developer is working")
return nil
}
func (d *Developer) Eat() error {
fmt.Println("Developer is eating")
return nil
}
func (d *Developer) Sleep() error {
fmt.Println("Developer is sleeping")
return nil
}
func (d *Developer) Code() error {
fmt.Println("Developer is coding")
return nil
}
// Developer doesn't design or manage, but must implement these
func (d *Developer) Design() error {
return fmt.Errorf("developer cannot design")
}
func (d *Developer) Manage() error {
return fmt.Errorf("developer cannot manage")
}
// Designer also forced to implement unused methods
type Designer struct{}
func (des *Designer) Work() error {
fmt.Println("Designer is working")
return nil
}
func (des *Designer) Eat() error {
fmt.Println("Designer is eating")
return nil
}
func (des *Designer) Sleep() error {
fmt.Println("Designer is sleeping")
return nil
}
func (des *Designer) Design() error {
fmt.Println("Designer is designing")
return nil
}
// Designer doesn't code or manage
func (des *Designer) Code() error {
return fmt.Errorf("designer cannot code")
}
func (des *Designer) Manage() error {
return fmt.Errorf("designer cannot manage")
}
func main() {
dev := &Developer{}
des := &Designer{}
dev.Work()
dev.Code()
des.Work()
des.Design()
}
Problem: The Worker interface is too large. Types are forced to implement methods they don’t need or can’t use.
package main
import "fmt"
// Basic behaviors that everyone has
type Human interface {
Eat() error
Sleep() error
}
// Workable represents entities that can work
type Workable interface {
Work() error
}
// Codable represents entities that can code
type Codable interface {
Code() error
}
// Designable represents entities that can design
type Designable interface {
Design() error
}
// Manageable represents entities that can manage
type Manageable interface {
Manage() error
}
// Developer implements only relevant interfaces
type Developer struct{}
func (d *Developer) Eat() error {
fmt.Println("Developer is eating")
return nil
}
func (d *Developer) Sleep() error {
fmt.Println("Developer is sleeping")
return nil
}
func (d *Developer) Work() error {
fmt.Println("Developer is working")
return nil
}
func (d *Developer) Code() error {
fmt.Println("Developer is coding")
return nil
}
// Designer implements only relevant interfaces
type Designer struct{}
func (des *Designer) Eat() error {
fmt.Println("Designer is eating")
return nil
}
func (des *Designer) Sleep() error {
fmt.Println("Designer is sleeping")
return nil
}
func (des *Designer) Work() error {
fmt.Println("Designer is working")
return nil
}
func (des *Designer) Design() error {
fmt.Println("Designer is designing")
return nil
}
// Manager implements management capabilities
type Manager struct{}
func (m *Manager) Eat() error {
fmt.Println("Manager is eating")
return nil
}
func (m *Manager) Sleep() error {
fmt.Println("Manager is sleeping")
return nil
}
func (m *Manager) Work() error {
fmt.Println("Manager is working")
return nil
}
func (m *Manager) Manage() error {
fmt.Println("Manager is managing")
return nil
}
// FullStackDeveloper can do everything
type FullStackDeveloper struct{}
func (fs *FullStackDeveloper) Eat() error {
fmt.Println("FullStackDeveloper is eating")
return nil
}
func (fs *FullStackDeveloper) Sleep() error {
fmt.Println("FullStackDeveloper is sleeping")
return nil
}
func (fs *FullStackDeveloper) Work() error {
fmt.Println("FullStackDeveloper is working")
return nil
}
func (fs *FullStackDeveloper) Code() error {
fmt.Println("FullStackDeveloper is coding")
return nil
}
func (fs *FullStackDeveloper) Design() error {
fmt.Println("FullStackDeveloper is designing")
return nil
}
// Functions that work with specific capabilities
func MakeCode(coder Codable) error {
return coder.Code()
}
func MakeDesign(designer Designable) error {
return designer.Design()
}
func MakeManage(manager Manageable) error {
return manager.Manage()
}
func main() {
dev := &Developer{}
des := &Designer{}
mgr := &Manager{}
fs := &FullStackDeveloper{}
// Each type only implements what it needs
MakeCode(dev)
MakeDesign(des)
MakeManage(mgr)
// FullStackDeveloper can do multiple things
MakeCode(fs)
MakeDesign(fs)
}
Benefits:
Before:
classDiagram
class Worker {
<<interface>>
+Work() error
+Eat() error
+Sleep() error
+Code() error
+Design() error
+Manage() error
}
class Developer {
+Work() error
+Eat() error
+Sleep() error
+Code() error
+Design() error ⚠️ Returns error
+Manage() error ⚠️ Returns error
}
class Designer {
+Work() error
+Eat() error
+Sleep() error
+Code() error ⚠️ Returns error
+Design() error
+Manage() error ⚠️ Returns error
}
Worker <|.. Developer : implements ⚠️ ISP Violation
Worker <|.. Designer : implements ⚠️ ISP Violation
note for Worker "Fat Interface Problem:<br />Forces implementers to define<br />methods they don't support"
note for Developer "Forced to implement Design() and Manage()<br />even though developers don't perform<br />these functions"
note for Designer "Forced to implement Code() and Manage()<br />even though designers don't perform<br />these functions"
After:
classDiagram
class Human {
<<interface>>
+Eat() error
+Sleep() error
}
class Workable {
<<interface>>
+Work() error
}
class Codable {
<<interface>>
+Code() error
}
class Designable {
<<interface>>
+Design() error
}
class Manageable {
<<interface>>
+Manage() error
}
class Developer {
+Eat() error
+Sleep() error
+Work() error
+Code() error
}
class Designer {
+Eat() error
+Sleep() error
+Work() error
+Design() error
}
class Manager {
+Eat() error
+Sleep() error
+Work() error
+Manage() error
}
class FullStackDeveloper {
+Eat() error
+Sleep() error
+Work() error
+Code() error
+Design() error
}
class MakeCode {
<<function>>
+MakeCode(coder Codable) error
}
class MakeDesign {
<<function>>
+MakeDesign(designer Designable) error
}
class MakeManage {
<<function>>
+MakeManage(manager Manageable) error
}
Human <|.. Developer : implements
Human <|.. Designer : implements
Human <|.. Manager : implements
Human <|.. FullStackDeveloper : implements
Workable <|.. Developer : implements
Workable <|.. Designer : implements
Workable <|.. Manager : implements
Workable <|.. FullStackDeveloper : implements
Codable <|.. Developer : implements
Codable <|.. FullStackDeveloper : implements
Designable <|.. Designer : implements
Designable <|.. FullStackDeveloper : implements
Manageable <|.. Manager : implements
MakeCode ..> Codable : uses
MakeDesign ..> Designable : uses
MakeManage ..> Manageable : uses
Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces). In Go, this means depending on interfaces rather than concrete types.
package main
import (
"fmt"
"time"
)
// MySQLDatabase is a low-level module
type MySQLDatabase struct{}
func (db *MySQLDatabase) Save(data string) error {
fmt.Printf("Saving '%s' to MySQL database\n", data)
// MySQL-specific implementation
return nil
}
func (db *MySQLDatabase) Load(id string) (string, error) {
fmt.Printf("Loading '%s' from MySQL database\n", id)
// MySQL-specific implementation
return "data from MySQL", nil
}
// UserService is a high-level module that directly depends on MySQLDatabase
type UserService struct {
db *MySQLDatabase // Direct dependency on concrete type - violates DIP
}
func NewUserService() *UserService {
return &UserService{
db: &MySQLDatabase{}, // Tightly coupled to MySQL
}
}
func (us *UserService) CreateUser(name string) error {
return us.db.Save(fmt.Sprintf("user:%s", name))
}
func (us *UserService) GetUser(id string) (string, error) {
return us.db.Load(id)
}
func main() {
service := NewUserService()
service.CreateUser("John Doe")
service.GetUser("user-123")
}
Problem: UserService directly depends on MySQLDatabase. To switch to PostgreSQL or another database, you must modify UserService.
package main
import (
"fmt"
"time"
)
// Database defines the abstraction (interface)
type Database interface {
Save(data string) error
Load(id string) (string, error)
}
// MySQLDatabase is a low-level module implementing Database
type MySQLDatabase struct{}
func (db *MySQLDatabase) Save(data string) error {
fmt.Printf("Saving '%s' to MySQL database\n", data)
// MySQL-specific implementation
return nil
}
func (db *MySQLDatabase) Load(id string) (string, error) {
fmt.Printf("Loading '%s' from MySQL database\n", id)
return "data from MySQL", nil
}
// PostgreSQLDatabase is another low-level module implementing Database
type PostgreSQLDatabase struct{}
func (db *PostgreSQLDatabase) Save(data string) error {
fmt.Printf("Saving '%s' to PostgreSQL database\n", data)
// PostgreSQL-specific implementation
return nil
}
func (db *PostgreSQLDatabase) Load(id string) (string, error) {
fmt.Printf("Loading '%s' from PostgreSQL database\n", id)
return "data from PostgreSQL", nil
}
// InMemoryDatabase is another implementation (useful for testing)
type InMemoryDatabase struct {
data map[string]string
}
func NewInMemoryDatabase() *InMemoryDatabase {
return &InMemoryDatabase{
data: make(map[string]string),
}
}
func (db *InMemoryDatabase) Save(data string) error {
fmt.Printf("Saving '%s' to in-memory database\n", data)
db.data[data] = data
return nil
}
func (db *InMemoryDatabase) Load(id string) (string, error) {
fmt.Printf("Loading '%s' from in-memory database\n", id)
if data, ok := db.data[id]; ok {
return data, nil
}
return "", fmt.Errorf("not found: %s", id)
}
// UserService is a high-level module that depends on Database interface
type UserService struct {
db Database // Depends on abstraction, not concrete type
}
func NewUserService(db Database) *UserService {
return &UserService{
db: db, // Accept any Database implementation
}
}
func (us *UserService) CreateUser(name string) error {
return us.db.Save(fmt.Sprintf("user:%s", name))
}
func (us *UserService) GetUser(id string) (string, error) {
return us.db.Load(id)
}
func main() {
// Can easily switch between different database implementations
mysqlDB := &MySQLDatabase{}
postgresDB := &PostgreSQLDatabase{}
memoryDB := NewInMemoryDatabase()
// Same service works with any database
service1 := NewUserService(mysqlDB)
service1.CreateUser("John Doe")
service1.GetUser("user-123")
service2 := NewUserService(postgresDB)
service2.CreateUser("Jane Smith")
service2.GetUser("user-456")
// In-memory database is great for testing
service3 := NewUserService(memoryDB)
service3.CreateUser("Test User")
service3.GetUser("user:Test User")
}
Benefits:
Before:
classDiagram
class MySQLDatabase {
+Save(data string) error
+Load(id string) (string, error)
}
class UserService {
-db *MySQLDatabase
+CreateUser(name string) error
+GetUser(id string) (string, error)
}
class NewUserService {
<<function>>
+NewUserService() *UserService
}
UserService --> MySQLDatabase : depends on<br />⚠️ DIP Violation
NewUserService ..> UserService : creates
NewUserService ..> MySQLDatabase : instantiates
After:
classDiagram
class Database {
<<interface>>
+Save(data string) error
+Load(id string) (string, error)
}
class MySQLDatabase {
+Save(data string) error
+Load(id string) (string, error)
}
class PostgreSQLDatabase {
+Save(data string) error
+Load(id string) (string, error)
}
class InMemoryDatabase {
-data map[string]string
+Save(data string) error
+Load(id string) (string, error)
}
class UserService {
-db Database
+CreateUser(name string) error
+GetUser(id string) (string, error)
}
class NewUserService {
<<function>>
+NewUserService(db Database) *UserService
}
class NewInMemoryDatabase {
<<function>>
+NewInMemoryDatabase() *InMemoryDatabase
}
Database <|.. MySQLDatabase : implements
Database <|.. PostgreSQLDatabase : implements
Database <|.. InMemoryDatabase : implements
UserService --> Database : depends on abstraction ✓
NewUserService ..> UserService : creates
NewUserService ..> Database : accepts
NewInMemoryDatabase ..> InMemoryDatabase : creates
SOLID principles provide a foundation for writing maintainable, scalable, and robust Go code:
graph LR
A[SOLID Principles] --> B[Better Code Quality]
B --> C[Maintainability]
B --> D[Testability]
B --> E[Flexibility]
B --> F[Scalability]
style A fill:#e1f5ff
style B fill:#90ee90
By following SOLID principles, you’ll write code that is easier to understand, test, and maintain, making your Go applications more robust and adaptable to change.
This post is written or assisted by AI. Read more about it here.