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:
// Good: Separate responsibilities into two functions type EmailSender struct{} func (e *EmailSender) SendEmail(to, subject, body string) { // logic for sending email } func formatEmailBody(template string, data map[string]string) string { // logic for formatting the email body return "" } // Avoid: Single function doing multiple jobs func sendFormattedEmail(to, subject, template string, data map[string]string) { body := formatEmailBody(template, data) // email sending logic mixed with formatting }
-
Open/Closed Principle (OCP)
- Description: Software entities (classes, modules, functions) should be open for extension but closed for modification.
Example:
// Good: Extend functionality via interfaces type PaymentProcessor interface { ProcessPayment(amount float64) error } type PayPalProcessor struct{} func (p PayPalProcessor) ProcessPayment(amount float64) error { // PayPal payment processing logic return nil } type CreditCardProcessor struct{} func (c CreditCardProcessor) ProcessPayment(amount float64) error { // Credit card processing logic return nil } // Avoid: Modifying existing functions to add new payment methods func processPayment(method string, amount float64) error { if method == "paypal" { // PayPal logic } else if method == "creditcard" { // Credit card logic } return nil }
-
Liskov Substitution Principle (LSP)
- Description: Objects should be replaceable by their subtypes without altering the correctness of the program.
Example:
// Good: Subtypes maintain expected behavior type Animal interface { Speak() string } type Dog struct{} func (d Dog) Speak() string { return "Woof!" } type Cat struct{} func (c Cat) Speak() string { return "Meow!" } func makeAnimalSpeak(a Animal) { fmt.Println(a.Speak()) } // Avoid: Subtypes behaving unpredictably type SilentDog struct{} func (s SilentDog) Speak() string { return "" // Violates expectations of the Speak method }
-
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:
// Good: Segregated interfaces type Reader interface { Read() string } type Writer interface { Write(data string) error } // Avoid: Fat interface forcing implementations to use unused methods type ReadWriteCloser interface { Read() string Write(data string) error Close() error } // 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:
// Good: High-level module depends on abstraction type NotificationService struct { sender EmailSender } func NewNotificationService(sender EmailSender) *NotificationService { return &NotificationService{sender: sender} } func (n *NotificationService) Notify(message string) { n.sender.SendEmail("[email protected]", "Notification", message) } // Avoid: High-level module directly depending on a low-level module func sendNotification() { sender := &EmailSender{} // tight coupling with EmailSender sender.SendEmail("[email protected]", "Notification", "Hello!") }
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:
// Higher-order function that accepts another function as a parameter func applyDiscount(price float64, discountFunc func(float64) float64) float64 { return discountFunc(price) } func tenPercentDiscount(price float64) float64 { return price * 0.9 } func main() { price := 100.0 discountedPrice := applyDiscount(price, tenPercentDiscount) fmt.Println(discountedPrice) // 90.0 }
-
Pure Functions
- Description: A pure function’s output is determined only by its input values, without observable side effects.
Example:
// Good: Pure function (no side effects) func add(a, b int) int { return a + b } // Avoid: Impure function (with side effects) var counter int func addWithSideEffect(a, b int) int { counter++ // changes external state return a + b }
-
Immutability
- Description: Data should not be modified after it is created. Instead, create a new copy of the data when changes are needed.
Example:
// Good: Immutability by creating a new slice func addToSlice(slice []int, value int) []int { newSlice := append(slice, value) return newSlice } // Avoid: Modifying the original slice func modifySlice(slice []int, value int) { slice = append(slice, value) }
-
Recursion Over Loops
- Description: Functional programming often favors recursion over iterative loops, especially when solving problems like list processing.
Example:
// Good: Recursion func factorial(n int) int { if n == 0 { return 1 } return n * factorial(n-1) } // Avoid: Iterative approach (loops) func iterativeFactorial(n int) int { result := 1 for i := n; i > 0; i-- { result *= i } return result }
-
Function Composition
- Description: Combine simple functions to build more complex ones, allowing for clean and reusable logic.
Example:
func double(x int) int { return x * 2 } func square(x int) int { return x * x } func compose(f, g func(int) int) func(int) int { return func(x int) int { return f(g(x)) // Apply g, then f } } func main() { doubleSquare := compose(double, square) fmt.Println(doubleSquare(3)) // Output: 18 (3^2 * 2) }
These principles can help keep your code clean, modular, and testable while using both object-oriented and functional paradigms.
comments powered by Disqus