I’ve been diving deep into Go lately, and the transition from a traditional OOP/Spring background has been a total paradigm shift. Coming from a world of rigid “Classes” and deep “Inheritance” trees, Go’s approach felt alien at first—until the logic finally clicked.

The Shift: Modules over Hierarchies

In Go, the philosophy is simple: Go doesn’t have classes. Instead, it relies on two powerful pillars:

  1. Composition (Struct Embedding)
  2. Implicit Interfaces

Here’s the journey of how I built a “Vehicle System” from scratch and why this “horizontal” way of building software is winning me over. Go composition

1. The Hierarchy: Composition over Inheritance

In Go, we don’t say a Car is a Vehicle. We say a Car is composed of a Vehicle. Using Struct Embedding, I built a modular hierarchy:

  • Engine: Core data (Model/Year).
  • Vehicle: Shared logic (Speed/Type) embedding the Engine.
  • Aeroplane: A specialized wrapper embedding the Vehicle.

The “Wow” Moment: Method Promotion. Because Aeroplane embeds Vehicle, it can call .Start() automatically, but it keeps its unique behaviors like .Takeoff() separate.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type Engine struct {
    model    string
    makeyear string
}

type Vehicle struct {
    Engine
    speed   float64
    vehType string
}

type Aeroplane struct {
    Vehicle
}

2. Reducing Boilerplate with Factory Functions

Go doesn’t have constructors, but the idiomatic “New” pattern is a game changer. I implemented Factory Functions (like NewAeroplane) to encapsulate initialization. This keeps the main logic clean and ensures that every object is “born” in a valid state.

1
2
3
4
5
6
func NewVehicle(model string, speed float64, vehType string) *Vehicle {
	return &Vehicle{Engine: Engine{model, strconv.Itoa(2026)}, speed: speed, vehType: vehType}
}
func NewAeroplane(model string, speed float64) *Aeroplane {
	return &Aeroplane{Vehicle: *NewVehicle(model, speed, "aeroplane")}
}

3. The Power of “Implicit” Interfaces

This was the biggest bridge for me as a Spring developer. In Spring, you explicitly implement an interface. In Go, if your struct has the method, it is the interface.

I defined a Drivable interface:

1
2
3
type Drivable interface {
    Start()
}

Because my structs had a Start() method, I could create a Fleet (a slice of interfaces). I could iterate through Cars and Aeroplanes in one loop, triggering their specific logic without the objects ever knowing about the interface!

4. The Golden Rule: “Accept Interfaces, Return Structs”

During this transition, I learned the “Golden Rule” of Go design that keeps your codebase flexible yet powerful:

  • Return Structs: In your constructors (factory functions), return the concrete type. This ensures the caller gets the full “power” of the object (like an Aeroplane’s specific Takeoff() method).
  • Accept Interfaces: In your high-level logic or functions, accept an interface. This keeps your functions decoupled and makes your code incredibly “testable.”

Why this beats traditional inheritance:

  1. Decoupling: I can swap a V8Engine for an ElectricEngine without rewriting the Aeroplane logic.
  2. Testability: Mocking becomes trivial. No heavy “Mockito magic” is required—just a simple struct that satisfies the interface.
  3. Clarity: The boundaries are strict. A Vehicle can’t accidentally call Takeoff(), preventing the “leaky abstraction” common in Java.

The Key Takeaway: Embracing “Idiomatic” Go

Writing Go means moving away from deep, rigid class trees and embracing flat, horizontal structures. It feels like “Manual Dependency Injection,” and the result is code that is:

  • Easier to maintain.
  • Faster to compile.
  • Much harder to break during refactoring.

For the Java/Spring developers out there: Are you favoring the “Magic” of a framework or the “Explicit” nature of Go in your current projects? Let’s discuss in the comments!

#Golang #Programming #SoftwareEngineering #CleanCode #BackendDevelopment #JavaToGo #CloudNative

Originally published on LinkedIn