Note: I wrote a similar blog post "Is Google's Go object-oriented now or not?" that is not specifically about inheritance and Go, but about Go and object-orientedness in general.
Go [1, 2] is a relatively new statically typed programming language developed at Google that compiles to the metal. It feels like a modernized C in the way that is resembles C a lot, but has closures, garbage collection, communicating sequential processes (CSP) [3, 4, 5] and indeed very fast build times (and no exception handling, no templates, but also no macros). Build times in Go are that fast that it is almost instant and feels like working with a dynamically typed scripting language with almost no turnaround times. Performance in Go 1.2 is somewhat in the league with Java [6, 7, 8, 9]. Could be faster, but it is already a lot faster than scripting languages like Ruby, Python, or Lua. Go is also underpowered at the moment as there is room for optimization. From looking at job ads Go seems to appeal most to people in the Python, PHP or Ruby camp. Contrary to Python, Ruby, and Lua, Go does multi-threading well and makes thread synchronization easier through the use of CSP [10].
Motivation
Go [1, 2] is a relatively new statically typed programming language developed at Google that compiles to the metal. It feels like a modernized C in the way that is resembles C a lot, but has closures, garbage collection, communicating sequential processes (CSP) [3, 4, 5] and indeed very fast build times (and no exception handling, no templates, but also no macros). Build times in Go are that fast that it is almost instant and feels like working with a dynamically typed scripting language with almost no turnaround times. Performance in Go 1.2 is somewhat in the league with Java [6, 7, 8, 9]. Could be faster, but it is already a lot faster than scripting languages like Ruby, Python, or Lua. Go is also underpowered at the moment as there is room for optimization. From looking at job ads Go seems to appeal most to people in the Python, PHP or Ruby camp. Contrary to Python, Ruby, and Lua, Go does multi-threading well and makes thread synchronization easier through the use of CSP [10].
Motivation
Go relies on delegation, which by Go aficionados is called embedding (see chapter "Inheritance" in [2]). There is no inheritance and hence no method overriding. This means that "f.MoreMagic()" in the last line in the code snippet below (sample shamelessly stolen from [2]) does not print "foo magic" to the console as expected but "base magic":
package main import "fmt" type Base struct {} func (Base) Magic() { fmt.Print("base magic") } func (self Base) MoreMagic() { self.Magic() } type Foo struct { Base } func (Foo) Magic() { fmt.Print("foo magic") } f := new(Foo) f.Magic() //=> foo magic f.MoreMagic() //=> base magic |
So is there a way to mimic method overriding in Go at a reasonable cost? Many Go developers would consider only the idea of mimicking it in Go as not in line with the language and beside the point. But method overriding offers a great deal of flexibility being able to redefine default inherited behavior and make an object or algorithm behave as appropriate for its kind in a transparent way.
Inner Pattern
What first stroke my mind when looking at the Magic/MoreMagic sample was the inner construct [12] in the Beta programming language, which is some kind of opposite super. Applying that idea in this case I came up with a solution, which I named the "inner pattern":
package main import "fmt" type Animal interface { Act() // Definition of ActInner tells developer to define a function Act with the // body calling ActInner as in Dog.Act() or Cat.Act() ActInner(inner Animal) makeNoise() } type Mamal struct { Animal } func (self Mamal) makeNoise() { fmt.Println("default noise") } func (self Mamal) ActMamal() { self.makeNoise() } func (self Mamal) ActInner(inner Animal) { inner.makeNoise() } type Dog struct { Mamal } func (self Dog) makeNoise() { fmt.Println("woof! woof!") } func (self Dog) Act() { self.ActInner(self) } type Cat struct { Mamal } func (self Cat) makeNoise() { fmt.Println("meow! meow!") } func (self Cat) Act() { self.ActInner(self) } func main() { dog := new(Dog) dog.ActMamal() // prints "default noise" but not "woof! woof!" dog.Act() // prints "woof! woof!" as expected cat := new(Cat) cat.ActMamal() // prints "default noise" but not "meow! meow!" cat.Act() // prints "meow! meow!" as expected } |
Note that the function makeNoise has to be public (e.g. MakeNoise) in case structs Cat and Dog are placed in separate packages which for simplicity reasons is not the case in the sample code above. Otherwise, the code would still compile but at runtime always Mamal.makeNoise would be called instead of Cat.makeNoise or Dog.makeNoise depending on the type of the receiver object.
So we get "method overriding" this way at the cost of having to stick to some kind of convention: If there is a method in a struct delegated to that has a parameter named inner like ActInner(inner Animal), we need to add a method Act() in our "subclass" calling ActInner:
So we get "method overriding" this way at the cost of having to stick to some kind of convention: If there is a method in a struct delegated to that has a parameter named inner like ActInner(inner Animal), we need to add a method Act() in our "subclass" calling ActInner:
func (self Dog) Act() { self.ActInner(self) } func (self Cat) Act() { self.ActInner(self) } |
This solution is not nicely transparent as f.ex. in Java where you would just add a method act() to your subclass that overrides the inherited method act() and that's it. Coming to think of it in C++ you can only override an inherited method if the inherited one is marked as virtual. So in C++ and other languages like Kotlin [13] or Ceylon [14] you also need to "design ahead" and think of whether a method is intended to be overridable. And this solution with actInner(inner Animal) in Go does not even carry the runtime overhead of dynamically dispatched virtual functions.
Also, in case struct Dog or Cat does not implement the function makeNoise along with function ActInner, the function Mamal.makeNoise() will be called at runtime. The Go compiler won't complain about some "subclass" Dog or Cat not implementing the "abstract" method makeNoise as for instance in Java or other OO languages that support abstract classes. There is no way round that as in the end a price has to be paid for not having a performance penalty in Go due to dynamically dispatched method calls compared to OO languages that support method overwriting.
Conclusion
My preliminary conclusion of all this is to use the "inner pattern" in Go as an ex post refactoring measure when code starts to lack "too much" in transparency. To apply it to begin with is too much pain where the gain in the end is uncertain. Otherwise, only apply it ex ante when it is clear from the beginning that the flexibility will be needed anyway as f.ex. with templated algorithms.
By the way, I think Rob Pike is right with what he says about CSP [10]. So I had a look how it can be done in Java. Groovy has it already [15] and for Java there is [16]. When casting CSP into an object-oriented mold you end up with something like actors. The best actor implementation for Java/Scala is probably Akka.
Update: Discussion on Reddit
There has been a discussion going on on Reddit concerning the proposed approach in this blog post. Below my remarks to the various comments made in that discussion.
« The link within the post for "Inner Pattern to mimic Method Overwriting in Go" is a bit odd. The Mamal struct contains an Animal interface but it is never assigned to or used, instead he's passing around an Animal interface explicitly and having to redeclare the Act func for both Dog and Cat. This is a bit cleaner, it uses the previously unused member interface Animal, and only has one Act func at the Mamal level which can dispatch generically to either Dog or Cat. »
The main function in the suggested solution ("This is a bit cleaner") looks like this:
By the way, I think Rob Pike is right with what he says about CSP [10]. So I had a look how it can be done in Java. Groovy has it already [15] and for Java there is [16]. When casting CSP into an object-oriented mold you end up with something like actors. The best actor implementation for Java/Scala is probably Akka.
Update: Discussion on Reddit
There has been a discussion going on on Reddit concerning the proposed approach in this blog post. Below my remarks to the various comments made in that discussion.
« The link within the post for "Inner Pattern to mimic Method Overwriting in Go" is a bit odd. The Mamal struct contains an Animal interface but it is never assigned to or used, instead he's passing around an Animal interface explicitly and having to redeclare the Act func for both Dog and Cat. This is a bit cleaner, it uses the previously unused member interface Animal, and only has one Act func at the Mamal level which can dispatch generically to either Dog or Cat. »
The main function in the suggested solution ("This is a bit cleaner") looks like this:
func main() {
dog := new(Dog)
dog.Animal = dog
dog.Mamal.makeNoise() // prints "default noise"
dog.makeNoise() // prints "woof! woof!"
dog.Act()
}
dog := new(Dog)
dog.Animal = dog
dog.Mamal.makeNoise() // prints "default noise"
dog.makeNoise() // prints "woof! woof!"
dog.Act()
}
The problem with the solution above is that it is not transparent. The developer needs to do some explicit variable switching as in "dog.Animal = dog" which breaks encapsulation. In the approach proposed in this blog post a "subclass" only needs to implement a method Act() that calls ActInner(...) and that's it. Some variable switching from the outside (which breaks encapsulation) is not required and the developer does not need to know that it has to be done (applying information hiding). The is conceptually the idea of method overwriting in OOP. I'm not proficient in C, but I would guess that such a solution with field assignment in records is what has to be resorted to when using procedural languages.
« You are correct about the interfaces, here's an example based on the code in the article: http://play.golang.org/p/de5-18d6aP »
The main function in the suggested solution ("http://play.golang.org/p/de5-18d6aP") looks like this:
The solution above applies a procedural approach where compared to object-oriented message passing receiver object and method argument are swapped. The receiver object (the object, the message is being sent to) is passed as the parameter to a procedure (aka function), which for OOP would be the wrong way round. The solution in my proposed approach defines an additional indirection to "restore" message passing as in OOP with Dog{} or Cat{} being the receiver objects (e.g. dog.Act(), cat.Act()).
« Yes, he is just showing that two independent structs can have a method with the same name, which isn't really surprising. »
Yes, I agree with that. My intention was only to show that this is supported compared to languages that are modular and not class based such as Modula II. I might be missing further criteria that need to be fulfilled.
« You are correct about the interfaces, here's an example based on the code in the article: http://play.golang.org/p/de5-18d6aP »
The main function in the suggested solution ("http://play.golang.org/p/de5-18d6aP") looks like this:
func main() {
noise(Dog{})
noise(Cat{})
}
noise(Dog{})
noise(Cat{})
}
The solution above applies a procedural approach where compared to object-oriented message passing receiver object and method argument are swapped. The receiver object (the object, the message is being sent to) is passed as the parameter to a procedure (aka function), which for OOP would be the wrong way round. The solution in my proposed approach defines an additional indirection to "restore" message passing as in OOP with Dog{} or Cat{} being the receiver objects (e.g. dog.Act(), cat.Act()).
« Yes, he is just showing that two independent structs can have a method with the same name, which isn't really surprising. »
Yes, I agree with that. My intention was only to show that this is supported compared to languages that are modular and not class based such as Modula II. I might be missing further criteria that need to be fulfilled.
References
[1] | Effective Go | |
[2] | Google Go Primer | |
[3] | Communicating Sequential Processes by Hoare | |
[4] | Goroutines | |
[5] | Race Detector | |
[6] | Benchmarks Game | |
[7] | Performance of Rust and Dart in Sudoku Solving | |
[8] | Benchmarks Round Two: Parallel Go, Rust, D, Scala | |
[9] | Benchmarking Level Generation in Go, Rust, Haskell, and D | |
[10] | Robert Pike in "Origins of Go Concurrency" (Youtube video, from
about position 29:00):
"The thing that is hard to understand until you've tried it is that the
whole business about finding deadlocks and things like this doesn't
come up very much. If you use mutexes and locks and shared memory it
comes up about all the time. And that's because it is just too low
level. If you program like this (using channels) deadlocks don't happen
very much. And if they do it is very clear why, because you have got
this high-level state of your program 'expressing this guy is trying to
send here and he can't, because he is here'. It is very very clear as
opposed to being down on the mutex and shared memory level where you
don't know who owns what and why. So I'm not saying it is not a
problem, but it is not harder than any other class of bugs you would
have."
|
|
[11] | Go FAQ Overloading | |
[12] | Super and Inner — Together at Last! (PDF) | |
[13] | Inheritance in Kotlin | |
[14] | Inheritance in Ceylon | |
[15] | CSP in Groovy | |
[16] | Go-style Goroutines in Java and Scala using HawtDispatch |