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
classkeyword: Go uses structs with methods - No inheritance: The
extendskeyword 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:
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:
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!")}
// Usagedog := 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:
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:
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:
type Speaker interface { Speak()}
type Dog struct{}
func (d Dog) Speak() { // Dog automatically satisfies Speaker fmt.Println("Woof!")}
// No "implements" keyword neededThis 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:
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:
public String readFile(String path) throws IOException { return Files.readString(Path.of(path));}
// Callertry { String content = readFile("data.txt");} catch (IOException e) { // Handle error}In Go:
func ReadFile(path string) (string, error) { data, err := os.ReadFile(path) if err != nil { return "", err } return string(data), nil}
// Callercontent, 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.
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:
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.”
// Good: Small interfacetype UserFetcher interface { GetUser(id string) (*User, error)}
// Good: Return concrete typefunc 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:
| Concept | Java/OOP | Go |
|---|---|---|
| Type definition | Class | Struct |
| Code reuse | Inheritance | Composition |
| Polymorphism | Explicit interface impl | Implicit satisfaction |
| Error handling | Exceptions | Return values |
| Initialization | Constructor | Factory 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:
- 👨💻 Go FAQ
- 👨💻 Effective Go
- 👨💻 Go by Example
- 👨💻 Reddit: r/golang discussion on OOP transition
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments