To start off with, in golang, there are no classes, only structs.
To add fuel to the fire, there is no type hierarchy, (i.e. no inheritance). This is probably the biggest difference as compared to other object oriented languages. Actually, it can't be truly considered an object-oriented language.
Ok, let's talk about type hierarchy.
Not having a type hierarchy was an intentional design choice in golang. Type hierarchies result in brittle code. The hierarchy must be designed early, often as the first step of designing the program, and early decisions can be difficult to change once the program is written. As a consequence, the model encourages early overdesign as the programmer tries to predict every possible use the software might require, adding layers of type and abstraction just in case. This is upside down. The way pieces of a system interact should adapt as it grows, not be fixed at the dawn of time. - Source
Let's take a quick look at type embedding. At first, it seems to be a mechanism for implementing inheritance, but upon close inspection, it becomes clear that it is not. It's simply a tool to borrow pieces of an implementation.
For example, take a look at the below code:
type Animal struct { Name string } type Dog struct{ Animal } // Firstly, you can't just instantiate a Dog struct by // passing in the field "Name". // d := Dog{Name:"dog"} // This is the proper way to instantiate a Dog struct d := Dog{Animal: Animal{Name: "dog"}} // Next, you might want to employ polymorphism by // creating a variable of type Animal, // and assigning the dog struct to it. // Again, doesn't work. Just because you "embedded" // the Animal struct inside the Dog struct // doesn't create a type hierarchy. // var a Animal; // a = Dog{Animal : Animal{Name:"dog"}}
It's clear that embedding doesn't create a type hierarchy. Note that you can still access the Name
field by doing d.Name
. That's because embedded fields and methods are "promoted" to the outer struct.
Ok, so if there is no type hierarchy, embedding seems like it creates one, but doesn't, how do you then group structs together to represent an abstraction? For example, how do you group together a cat and a dog as an "Animal" abstraction as you would in other OOP languages by creating an Animal super class and a Cat, Dog subclass?
Answer: You group structs not by their type, but by their behavior, i.e. interfaces.
Interfaces
An interface is just a set of methods. Any type that implements these methods satisfies this interface implicitly. The idea is that you can create an abstraction of different concrete type values and work with the values through their common behavior, without having to define a type hierarchy. Here's a nice example that explains how.
Further, Interfaces allow you to embody the dependency inversion principle. It is a recommended practice in go to accept interfaces and return structs. This way your code depends on abstractions rather than concrete implementations.
Unlike interfaces in other languages, rather than declaring the interface in the package where you're implementing a concrete type that satisfies that interface, it is recommended to declare interfaces in the package where you're using that interface.
It's an interesting and fundamental shift in how programmers should think - You group structs not by their type, but by their behavior.
Sure, but every OOP language has interfaces. How do I reuse code in golang?
Composition over inheritance
You reuse code in golang using embedding and composition. There's a famous principle in object-oriented programming to favour composition over inheritance. Here's a nice article that describes how go embodies that principle.