Skip to content

Why Does Go Feel Weird to Developers Coming from OOP Languages?

Problem

I came across a Reddit post where a developer asked: “Everytime I learn a new language that is not OOP it feels off for me. I am currently learning Go and there is a lot that just doesn’t feel right like it did with C++.”

That resonated with me. When I first started Go after years of Java and C#, I felt the same way. Where were my classes? Where was inheritance? Why does everything feel… wrong?

This post is about what creates that “weird” feeling and how to get past it.

The Missing Pieces

The discomfort starts when you realize Go doesn’t have familiar OOP tools:

  • No class keyword: Go uses structs with methods
  • No inheritance: The extends keyword doesn’t exist
  • No explicit interface implementation: You don’t declare implements Interface
  • No exceptions: Error handling uses return values
  • No constructors: No special initialization methods
  • No generics (until Go 1.18 in 2022): Limited generic programming

The first time I tried to write Go, I instinctively looked for patterns I’d used for years. They weren’t there. Let me show you what I mean.

Class vs Struct: The First Shock

In Java, I’d write:

Animal.java
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void speak() {
System.out.println("Some sound");
}
}
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void speak() {
System.out.println("Woof!");
}
}

In Go, there’s no extends. No super. No @Override. Here’s the equivalent:

animal.go
type Animal struct {
Name string
}
func (a *Animal) Speak() {
fmt.Println("Some sound")
}
type Dog struct {
Animal // This is embedding, not inheritance
}
func (d *Dog) Speak() {
fmt.Println("Woof!")
}
// Usage
dog := Dog{Animal{Name: "Buddy"}}

The difference isn’t just syntax. It’s a fundamental shift in how you think about relationships between types.

The “Is-A” vs “Has-A” Problem

In Java, I’d think: “A Dog IS AN Animal.”

+--------+ +--------+
| Animal |<------| Dog |
+--------+extends+--------+

In Go, the mental model changes. A Dog HAS AN Animal inside it:

+--------+ +--------+
| Dog |------>| Animal |
+--------+ has-a +--------+

This is composition, not inheritance. The Dog struct contains an Animal struct. When you call dog.Speak(), Go first looks for a Speak method on Dog. If it doesn’t find one, it “promotes” the method from the embedded Animal.

But here’s the key difference: Go doesn’t support polymorphism through inheritance. You can’t do this:

Java polymorphism works
Animal myDog = new Dog("Buddy");
myDog.speak(); // Prints "Woof!"

In Go, if you need polymorphism, you use interfaces.

Interfaces: Implicit Satisfaction

This was the second shock. In Java, you explicitly declare that a class implements an interface:

explicit_interface.java
public interface Speaker {
void speak();
}
public class Dog implements Speaker { // Must declare "implements Speaker"
public void speak() {
System.out.println("Woof!");
}
}

In Go, interfaces are satisfied implicitly:

implicit_interface.go
type Speaker interface {
Speak()
}
type Dog struct{}
func (d Dog) Speak() { // Dog automatically satisfies Speaker
fmt.Println("Woof!")
}
// No "implements" keyword needed

This feels wrong at first. What if I implement the wrong method signature? The compiler will catch it when you try to use the type as that interface:

interface_check.go
func MakeItSpeak(s Speaker) {
s.Speak()
}
func main() {
d := Dog{}
MakeItSpeak(d) // Works! Dog satisfies Speaker
}

If Dog didn’t have a Speak() method, the compiler would error at the call site. This is actually safer than Java in some ways because you can’t accidentally satisfy an interface.

Error Handling: No Try-Catch

The third major difference is error handling. In Java:

exception_handling.java
public String readFile(String path) throws IOException {
return Files.readString(Path.of(path));
}
// Caller
try {
String content = readFile("data.txt");
} catch (IOException e) {
// Handle error
}

In Go:

error_handling.go
func ReadFile(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
return string(data), nil
}
// Caller
content, err := ReadFile("data.txt")
if err != nil {
// Handle error
return err
}

I hated this at first. Every function call seemed to require if err != nil. But after a few weeks, I realized something: I always knew exactly where errors could happen. No more wondering “what exceptions might this method throw?”

The Adaptation Curve

One Reddit comment stuck with me: “Go felt weird for like two weeks and then it just clicked.”

That was my experience too. The discomfort lasted about 2-4 weeks. Here’s what helped me adapt:

Week 1: Stop Fighting It

I kept trying to write Java in Go syntax. I’d create deep type hierarchies and fight against the lack of inheritance. That was the wrong approach.

Week 2: Embrace Composition

Instead of asking “what IS this thing?”, I started asking “what DOES this thing have?” A User doesn’t extend Auditable and Serializable. A User has audit information and can be serialized.

composition_example.go
type AuditInfo struct {
CreatedAt time.Time
UpdatedAt time.Time
CreatedBy string
}
type User struct {
ID string
Name string
AuditInfo // Embedded - composition
}

Week 3: Think in Interfaces

Go interfaces are small. The standard library has interfaces with single methods:

small_interfaces.go
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}

Any type that has a Read method satisfies Reader. Any type with a Write method satisfies Writer. This makes your code flexible and testable.

Week 4: Accept Explicit Errors

I stopped wishing for try-catch. Explicit error returns forced me to think about failure modes at every step. My code became more robust because I couldn’t ignore errors.

What You Gain

The top comment on the Reddit thread was critical: “Go is just weird and poorly designed.” But another comment caught my attention: “The simplicity that annoyed me at first ended up being the thing I liked most about it for security work, less abstraction means fewer places for bugs to hide.”

After the adaptation period, I understood what Go gives you:

Fast compilation - No complex type hierarchies to resolve. Go compiles fast.

Readable code - No “where does this method come from?” confusion. The code you see is the code that runs.

Simple testing - No mock frameworks needed for interfaces. Just implement the interface with a test double.

Better concurrency - Goroutines are simpler than Java threads.

Common Mistakes I Made

Mistake 1: Forcing Inheritance

I tried to simulate inheritance with embedding and method overrides. It works for simple cases but breaks down when you need true polymorphism. The solution: use interfaces.

Mistake 2: Fighting Error Handling

I tried to create a ” Result” type that wrapped errors, thinking I was improving on Go’s design. I wasn’t. Go’s error handling is verbose on purpose. It forces you to handle errors.

Mistake 3: Large Interfaces

I created interfaces with 10+ methods, like Java interfaces. This defeats Go’s purpose. The idiom is: “Accept interfaces, return structs.”

interface_idiom.go
// Good: Small interface
type UserFetcher interface {
GetUser(id string) (*User, error)
}
// Good: Return concrete type
func NewUserService() *UserService {
return &UserService{}
}

Mistake 4: Ignoring the Standard Library

I reached for third-party libraries for things the standard library already did well. Go’s standard library is extensive. Use it.

Summary

Go feels weird to OOP developers because it’s fundamentally different:

ConceptJava/OOPGo
Type definitionClassStruct
Code reuseInheritanceComposition
PolymorphismExplicit interface implImplicit satisfaction
Error handlingExceptionsReturn values
InitializationConstructorFactory functions

The “weirdness” is a feature, not a bug. Go trades OOP flexibility for simplicity and compilation speed. Most developers adapt in 2-4 weeks.

The key insight: don’t write Java in Go syntax. Learn Go’s idioms, and the weirdness becomes familiarity.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments