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 structures B and C which both have a method F(), thus satisfying an interface A, the compiler will complain about an “ambiguous selector” if D.F() is called, or if an instance of D is assigned to a variable of type A. B and C’s methods can be called explicitly with D.B.F() or D.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.