Did you know…? Go has a Diamond Problem!
In object-oriented languages, the Diamond Problem is a well-known pitfall of the multi-inheritance model.
In a nutshell, inheritance enables classes (that is, data structures with methods attached) to take over code from other classes and expand or override parts of that code. For instance, imagine a “parent” class A that defines a method Foo()
. “Child” classes derived from A can use, and even override, this method. Say, two classes, B and C, inherit from A, and each one changes method Foo()
to behave slightly differently (but still in accordance with the original definition).
Now imagine a class D, which, thanks to multiple inheritance, inherits from both classes B and C. It does not override method Foo()
.
+-+
|A|
+-+
/ \
+-- --+
|B| |C|
+-+ +-+
\ /
---
|D|
+-+
What happens if code calls method Foo()
on class D? Which of the three definitions of Foo()
shall class D use?
This is the Diamond Problem, named after the diamond shape of the inheritance diagram.
What does all this has to do with Go? Without inheritance, let alone multiple inheritance, what could go wrong?
Turns out that it is entirely possible to construct a diamond-shaped dependency relationship in Go, through struct embedding. While struct embedding is not the same as inheritance, it also is a form of “promoting” methods from an embedded struct to the embedding struct. If an embedded struct B has a method Foo()
, the embedding struct D can call Foo()
directly instead of calling B.Foo()
.
You can see where this is going: If struct D embeds B and C, which both define a method Foo()
(that can optionally implement an interface A), then we have a Diamond Problem. (An interface A is not strictly needed, the problem can be constructed without it, effectively creating a “V-shaped” dependency. —
Playground Link.)
Luckily, Go prevents you from inadvertently calling the wrong Foo()
. Go's solution even made it into
Wikipedia:
Go prevents the diamond problem at compile time. If a structure
D
embeds two structuresB
andC
which both have a methodF()
, thus satisfying an interfaceA
, the compiler will complain about an “ambiguous selector” ifD.F()
is called, or if an instance ofD
is assigned to a variable of typeA
.B
andC
’s methods can be called explicitly withD.B.F()
orD.C.F()
.
Another good example of why it is so important and helpful that a language catches as many problems as possible at compile time.