Let’s go through the SOLID principles and functional programming principles that can apply to your Go codebase. I’ll provide simple Go examples along with brief explanations to show how each principle can improve code quality.
SOLID Principles
-
Single Responsibility Principle (SRP)
- Description: A class or function should have only one reason to change, meaning it should only have one job or responsibility.
Example:
go1// Good: Separate responsibilities into two functions 2type EmailSender struct{} 3 4 5func (e *EmailSender) SendEmail(to, subject, body string) { 6 // logic for sending email 7} 8 9 10func formatEmailBody(template string, data map[string]string) string { 11 // logic for formatting the email body 12 return "" 13} 14 15 16// Avoid: Single function doing multiple jobs 17func sendFormattedEmail(to, subject, template string, data map[string]string) { 18 body := formatEmailBody(template, data) 19 // email sending logic mixed with formatting 20}
-
Open/Closed Principle (OCP)
- Description: Software entities (classes, modules, functions) should be open for extension but closed for modification.
Example:
go1// Good: Extend functionality via interfaces 2type PaymentProcessor interface { 3 ProcessPayment(amount float64) error 4} 5 6 7type PayPalProcessor struct{} 8 9 10func (p PayPalProcessor) ProcessPayment(amount float64) error { 11 // PayPal payment processing logic 12 return nil 13} 14 15 16type CreditCardProcessor struct{} 17 18 19func (c CreditCardProcessor) ProcessPayment(amount float64) error { 20 // Credit card processing logic 21 return nil 22} 23 24 25// Avoid: Modifying existing functions to add new payment methods 26func processPayment(method string, amount float64) error { 27 if method == "paypal" { 28 // PayPal logic 29 } else if method == "creditcard" { 30 // Credit card logic 31 } 32 return nil 33}
-
Liskov Substitution Principle (LSP)
- Description: Objects should be replaceable by their subtypes without altering the correctness of the program.
Example:
go1// Good: Subtypes maintain expected behavior 2type Animal interface { 3 Speak() string 4} 5 6 7type Dog struct{} 8 9 10func (d Dog) Speak() string { 11 return "Woof!" 12} 13 14 15type Cat struct{} 16 17 18func (c Cat) Speak() string { 19 return "Meow!" 20} 21 22 23func makeAnimalSpeak(a Animal) { 24 fmt.Println(a.Speak()) 25} 26 27 28// Avoid: Subtypes behaving unpredictably 29type SilentDog struct{} 30 31 32func (s SilentDog) Speak() string { 33 return "" // Violates expectations of the Speak method 34}
-
Interface Segregation Principle (ISP)
- Description: No client should be forced to depend on methods it doesn’t use. Create small, specific interfaces instead of one large interface.
Example:
go1// Good: Segregated interfaces 2type Reader interface { 3 Read() string 4} 5 6 7type Writer interface { 8 Write(data string) error 9} 10 11 12// Avoid: Fat interface forcing implementations to use unused methods 13type ReadWriteCloser interface { 14 Read() string 15 Write(data string) error 16 Close() error 17} 18 19 20// If a class only needs reading, it should not be forced to implement Write and Close.
-
Dependency Inversion Principle (DIP)
- Description: High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces).
Example:
go1// Good: High-level module depends on abstraction 2type NotificationService struct { 3 sender EmailSender 4} 5 6 7func NewNotificationService(sender EmailSender) *NotificationService { 8 return &NotificationService{sender: sender} 9} 10 11 12func (n *NotificationService) Notify(message string) { 13 n.sender.SendEmail("[email protected]", "Notification", message) 14} 15 16 17// Avoid: High-level module directly depending on a low-level module 18func sendNotification() { 19 sender := &EmailSender{} // tight coupling with EmailSender 20 sender.SendEmail("[email protected]", "Notification", "Hello!") 21}
Functional Programming Principles
-
First-Class and Higher-Order Functions
- Description: Functions are treated as first-class citizens and can be passed around as arguments, returned from other functions, or assigned to variables.
Example:
go1// Higher-order function that accepts another function as a parameter 2func applyDiscount(price float64, discountFunc func(float64) float64) float64 { 3 return discountFunc(price) 4} 5 6 7func tenPercentDiscount(price float64) float64 { 8 return price * 0.9 9} 10 11 12func main() { 13 price := 100.0 14 discountedPrice := applyDiscount(price, tenPercentDiscount) 15 fmt.Println(discountedPrice) // 90.0 16}
-
Pure Functions
- Description: A pure function’s output is determined only by its input values, without observable side effects.
Example:
go1// Good: Pure function (no side effects) 2func add(a, b int) int { 3 return a + b 4} 5 6 7// Avoid: Impure function (with side effects) 8var counter int 9 10 11func addWithSideEffect(a, b int) int { 12 counter++ // changes external state 13 return a + b 14}
-
Immutability
- Description: Data should not be modified after it is created. Instead, create a new copy of the data when changes are needed.
Example:
go1// Good: Immutability by creating a new slice 2func addToSlice(slice []int, value int) []int { 3 newSlice := append(slice, value) 4 return newSlice 5} 6 7 8// Avoid: Modifying the original slice 9func modifySlice(slice []int, value int) { 10 slice = append(slice, value) 11}
-
Recursion Over Loops
- Description: Functional programming often favors recursion over iterative loops, especially when solving problems like list processing.
Example:
go1// Good: Recursion 2func factorial(n int) int { 3 if n == 0 { 4 return 1 5 } 6 return n * factorial(n-1) 7} 8 9 10// Avoid: Iterative approach (loops) 11func iterativeFactorial(n int) int { 12 result := 1 13 for i := n; i > 0; i-- { 14 result *= i 15 } 16 return result 17}
-
Function Composition
- Description: Combine simple functions to build more complex ones, allowing for clean and reusable logic.
Example:
go1func double(x int) int { 2 return x * 2 3} 4 5 6func square(x int) int { 7 return x * x 8} 9 10 11func compose(f, g func(int) int) func(int) int { 12 return func(x int) int { 13 return f(g(x)) // Apply g, then f 14 } 15} 16 17 18func main() { 19 doubleSquare := compose(double, square) 20 fmt.Println(doubleSquare(3)) // Output: 18 (3^2 * 2) 21}
These principles can help keep your code clean, modular, and testable while using both object-oriented and functional paradigms.