OOP vs Go: Conceptual Shifts
This document explains the conceptual differences between classic Object-Oriented Programming (Java-style) and Go’s design philosophy.
The goal is not to turn you into a Go expert, but to help you adjust your mental model when moving from Java to Go.
1. Think in structs + functions, not classes
Why this change exists
Go was designed to reduce conceptual overhead.
Instead of centering everything around classes, Go separates:
- Data →
struct - Behavior → functions or methods with receivers
This avoids deep hierarchies and keeps behavior explicit.
Java (OOP)
- Class is the main abstraction.
- Data and behavior are tightly coupled.
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Go
- No classes.
- Structs model data.
- Methods are functions attached to a type via receivers.
type Person struct {
Name string
}
func (p *Person) GetName() string {
return p.Name
}
Gains
- Simpler mental model
- Less boilerplate
- Easier to reason about behavior
Trade-offs
- No class-level abstraction
- Less familiar for traditional OOP developers
2. Use composition, not inheritance
Why this change exists
Inheritance creates tight coupling and rigid hierarchies. Go avoids this by design to favor flexibility and clarity.
Instead of asking “what is this?”, Go encourages “what does this contain?”.
Java (OOP)
- Inheritance is a core mechanism.
- Can lead to deep class trees.
class Employee extends Person {
private String role;
}
Go
- Composition via struct embedding.
- Behavior is reused without hierarchy.
type Logger struct {
// logging behavior
}
type Service struct {
Logger // embedded
}
Gains
- Flexible reuse
- Fewer hidden dependencies
- Easier refactoring
Trade-offs
- No polymorphism via inheritance
- Requires a mindset shift
3. Interfaces are small and implicit
Why this change exists
Go treats interfaces as contracts of behavior, not type hierarchies. They are meant to be small, focused, and local.
Java (OOP)
- Explicit
implements - Often large, centralized interfaces
interface Speaker {
void speak();
}
class Person implements Speaker {
public void speak() {
System.out.println("Hello!");
}
}
Go
- Implicit implementation
- A type satisfies an interface automatically
type Speaker interface {
Speak()
}
type Person struct {}
func (p Person) Speak() {
fmt.Println("Hello!")
}
Gains
- Decoupled code
- Easier testing and mocking
- Encourages clean boundaries
Trade-offs
- Less explicit relationships
- Harder to discover implementations via IDE
4. Access modifiers are simplified
Why this change exists
Go prioritizes readability over configurability. Instead of keywords, visibility is inferred from naming.
Java (OOP)
public,private,protected
Go
- Capitalized = exported
- Lowercase = unexported
type Person struct {
Name string // exported
age int // unexported
}
Gains
- Extremely simple rules
- Easier API reading
Trade-offs
- Less granular access control
- No
protectedequivalent
5. Constructors are just functions
Why this change exists
Go avoids special language constructs when functions are enough. Initialization logic is explicit and testable.
Java (OOP)
- Constructors tied to class name
class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
Go
- Factory functions
- Convention-based naming
type Person struct {
Name string
}
func NewPerson(name string) *Person {
return &Person{
Name: name,
}
}
Gains
- Flexible initialization
- Clear intent
- Easier dependency injection
Trade-offs
- No enforced constructor pattern
6. Explicit behavior over hidden magic
Why this change exists
Go intentionally avoids heavy reflection and annotation-driven behavior. This reduces surprise and debugging complexity.
Java (OOP)
- Heavy framework usage
- Behavior often hidden via annotations
@Entity
class Person {
@Id
private int id;
}
Go
- Explicit wiring
- Minimal reflection (mostly via struct tags)
type Person struct {
ID int `json:"id"`
}
Gains
- Predictable execution
- Easier debugging
- Lower cognitive load
Trade-offs
- More manual setup
- Less automation
7. Error handling is explicit
Why this change exists
Exceptions hide control flow. Go treats errors as normal return values.
Java (OOP)
try {
int result = divide(10, 0);
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero");
}
Go
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("cannot divide by zero")
}
return a / b, nil
}
result, err := divide(10, 0)
if err != nil {
fmt.Println(err)
}
Gains
- Clear control flow
- No hidden jumps
- Easier reasoning
Trade-offs
- More repetitive checks
- Verbose code
8. Libraries over frameworks
Why this change exists
Frameworks tend to invert control and hide execution paths. Go prefers small libraries and explicit composition.
Java (OOP)
- Spring, Hibernate, etc.
Go
- Lightweight libraries
- Application owns the flow
r := gin.Default()
r.GET("/users", getUsersHandler)
Gains
- Full control of execution
- Easier performance tuning
- Less magic
Trade-offs
- More decisions for the developer
- Less “out of the box” behavior
9. Generics are optional, not dominant
Why this change exists
Go prefers concrete types for readability. Generics exist to remove duplication, not to model everything.
Java (OOP)
List<String> names = new ArrayList<>();
Go
func MapSlice[T any](items []T, fn func(T) T) []T {
var result []T
for _, item := range items {
result = append(result, fn(item))
}
return result
}
Gains
- Simpler APIs
- Less abstract code
Trade-offs
- Fewer generic-heavy patterns
- Less expressive type systems
10. Dependency Injection is manual
Why this change exists
Explicit dependencies improve testability and clarity. Go avoids container-managed lifecycles.
Java (OOP)
- Framework-managed DI
Go
- Constructor-based DI
type Service struct {
Repo Repository
}
func NewService(repo Repository) *Service {
return &Service{Repo: repo}
}
Gains
- Transparent dependencies
- Easier testing
- No runtime magic
Trade-offs
- More wiring code
- No automatic lifecycle management
11. Concurrency is a first-class concept
Why this change exists
Go was designed for concurrent systems. Concurrency is part of the language, not a library concern.
Java (OOP)
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() ->
System.out.println("Running in parallel")
);
Go
go func() {
fmt.Println("Running in parallel")
}()
Gains
- Simple concurrency model
- Lightweight goroutines
- Clear communication via channels
Trade-offs
- Requires discipline
- Easier to create race conditions if misused
Final mindset shift
Java asks: “What class does this belong to?”
Go asks: “What data do I have, and what behavior do I need?”
Go trades abstraction depth for clarity, simplicity, and explicitness.