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:

  • Datastruct
  • 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 protected equivalent

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.


Go for OOP Developers — Documentation for developers transitioning from OOP to Go.

This site uses Just the Docs, a documentation theme for Jekyll.