Favor object composition over class inheritance. — GoF1
Unlike other languages, Go’s way of object-orientation is composition over inheritance. What this means is that you can embed items (mostly structs) in one another. This is usually convenient when you have a baseline struct that is used for many different functions, with other structs built on top of the initial struct. While Go strives to be simple, embedding is one place where the complexity of the problem arises. In this post, I’ll cover the different kinds of embedding Go supports using real code.
1. Structs in Structs
In Go —instead of Java and C++ classes— the equivalent container for encapsulation is called a struct. It describes the attributes of the objects for this class. Let’s take for instance these two structs:
type Utensils struct {
fork string
knife string
}
type Appliances struct {
oven string
refrigerator string
}
I can then use Go’s nested structuring to create another struct called Kitchen
that contains Utensils
and Appliances
structs as follows:
type Kitchen struct {
Utensils
Appliances
}
At first glance, this might seem like inheritance but one thing with Go is it doesn’t provide polymorphism. Embedding differs from subclassing in an important way: when a type is embedded, the methods of that type are available as methods of the outer type; however, for invocation of the embedded struct methods, the receiver of the method must be the inner (embedded) type, not the outer one. I can fill my kitchen struct with utensils and appliances as follows:
iansKitchen := new(Kitchen)
iansKitchen.Utensils.fork = "crab";
iansKitchen.Utensils.spoon = "dessert";
iansKitchen.Appliances.oven = "self cleaning";
iansKitchen.Appliances.refridgerator = "double-door";
fmt.Printf("%+v\n";, iansKitchen)
Output:
{Utensils:{fork:crab spoon:dessert} Appliances:{oven:self cleaning refridgerator:double door}}
We can see the kitchen items are organized in the Kitchen struct which can then be easily referenced in the other methods.
2. Interfaces in Interfaces
For polymorphic behavior, Go uses interfaces and duck typing. Duck typing implies that any class that has all the methods that an interface contains can be said to implement the said interface. What makes Go’s interfaces special is that they are implemented implicitly. A classic example of interface embedding is outlined in Go’s standard library. Let’s look at their definitions:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close(p []byte) (n int, err error)
}
Explicitly:
type ReadCloser interface {
Reader
Closer
}
This states intent in the clearest way possible i.e. in order to implement ReadCloser
, you have to implement both Reader
and Closer
.
3. Interfaces in Structs
As confusing as it may sound, embedding an interface in a struct is a very useful technique that inherits most of its behavior from the mediator pattern in software design patterns. Let’s look at a real-world example and you’ll see that the underlying mechanics are pretty simple and useful in a couple of scenarios.
type Logic interface {
Process (data string)
}
type Client struct {
L Logic
}
func (c Client) Program () {
c.L.Process(data)
}
func main() {
c := Client {
c.Program ()
}
}
By virtue of the Logic
object, the communicating classes don’t get coupled to each other’s implementation. It helps reduce the coupling between the classes communicating with each other, because they don’t need to have the knowledge of each other’s implementation. As you can see, this technique is fairly on the advanced side and mostly implemented in embedded-systems programming. Though it’s important to highlight it because it represents a markedly different use of the “embed interface in struct” tool that’s common in the standard library.
The above quote is excerpted from the 1994 book Design Patterns by Gamma, Helm, Johnson, and Vlissides (Addison-Wesley), better known as the Gang of Four book. ↩︎