Introduction: The Eternal Debate in the Go World
\n\nGo is a language that broke the mold. It doesn't tolerate unnecessary complexity, yet gives the developer incredible freedom. Everyone who starts writing in Go (or switches from Python or Java) eventually hits a wall: "What is the right way? Write like in Java with classes and inheritance, or like in Haskell with functions without side effects?"
\n\nSpoiler: Go is neither a purely object-oriented nor a purely functional language. It is a pragmatist. But that doesn't mean you can't (and shouldn't) use both paradigms. In this article, we'll break down how to combine functional and object-oriented approaches in Go to make your code readable, testable, and fast. We won't lecture you, but will look at real examples and antipatterns.
\n\nIf you're looking for Go best practices and want to understand how to write clean code in this language, you've come to the right place.
\n\n1. Object-Oriented Approach in Go: Without Classes, But with Soul
\n\nMany believe that since Go doesn't have class, it doesn't have OOP. This is a myth. OOP in Go is built on two pillars: structs and interfaces. There is no inheritance, but there is composition, which the language creators consider more flexible.
Structs and Methods
\nYou can define methods for any type. This is the foundation of object-oriented code.
\n\npackage main\n\nimport "fmt"\n\n// Define an interface (contract)\ntype Notifier interface {\n Send(message string) error\n}\n\n// Struct implementing the interface\ntype EmailNotifier struct {\n Email string\n}\n\n// Method for EmailNotifier\nfunc (e EmailNotifier) Send(message string) error {\n fmt.Printf("Sending email to %s: %s\\n", e.Email, message)\n return nil\n}\n\n// Another struct\ntype SMSNotifier struct {\n Phone string\n}\n\nfunc (s SMSNotifier) Send(message string) error {\n fmt.Printf("Sending SMS to %s: %s\\n", s.Phone, message)\n return nil\n}\n\n// Function that accepts any Notifier (polymorphism)\nfunc AlertAll(notifiers []Notifier, msg string) {\n for _, n := range notifiers {\n n.Send(msg)\n }\n}\n\nfunc main() {\n notifiers := []Notifier{\n EmailNotifier{Email: "user@example.com"},\n SMSNotifier{Phone: "+1234567890"},\n }\n AlertAll(notifiers, "Server is down!")\n}\n\nPros of OOP in Go:
\n- \n
- Polymorphism through interfaces. You write the
AlertAllfunction once, and it works with any type that implementsNotifier. This is loose coupling. \n - State. Structs are ideal for data models (User, Order, Config). \n
- Encapsulation. Fields starting with a lowercase letter are private, with uppercase are public. Simple and effective. \n
Cons:
\n- \n
- It's easy to slip into a "Java-style": creating deep hierarchies, even though Go doesn't support them. \n
- Mutability (changing state) complicates testing and debugging. \n
2. Functional Programming in Go: Purity for Speed
\n\nThe functional style in Go is not lambda calculus, but rather a set of principles: minimal side effects, immutability, functions as arguments. With the advent of generics (Go 1.18+), this style has become more powerful.
\n\nPure Functions and Immutability
\nThe main rule of functional programming: a function takes data, returns new data, but doesn't change anything outside.
\n\npackage main\n\nimport "fmt"\n\n// Bad: mutating the input slice (side effect)\nfunc BadAddPrefix(users []string, prefix string) {\n for i := range users {\n users[i] = prefix + users[i]\n }\n}\n\n// Good: returning a new slice (pure function)