The question whether Go without direct support for inheritance is
object-oriented or not pops up frequently on the Go user forum,
Stackoverflow, various blogs, and other places on the Internet. Even
some books about Go don't explain this well. Having developed about 10
years in Smalltalk, which is widely considered to be a pure
object-oriented language, I thought my background about OOP is
sufficient to write an article that sheds some light on this matter. It's a long read, I fear. But the matter is therefore being dealt with thoroughly. If you get tired in the meanwhile you can also just skip to the last conclusive chapter ;-).
Message Passing
It is often said that object-oriented programming is about inheritance. At the beginning of OOP inheritance received great attention. It was often sold to management as the tool to reduce development costs: you inherit already existing functionality from super classes and this way have much less development work to do. This idea quickly turned out to be too optimistic. In my old Smalltalk times it was already essential in job interviews to be of the opinion that deep class inheritance trees are hard to change and should therefore be avoided. And that was more than 15 years ago. Maybe because inheritance was new and somewhat "spectacular" it received so much attention although OOP was always about message passing [1, 2]. Let's have a look at a sample that shows message passing in Smalltalk:
| foo |
foo := Foo new.
That's all? Yep, but there is a lot happening. The message with the selector (e.g. message label) new is sent to the class Foo. The resulting instance of class Foo is stored in the locally defined variable foo. In Smalltalk a class is an instance of class MetaClass. Being an instance a class is by definition also an object. So Smalltalk is true to the "everything is an object paradigm". This is how the same thing looks like in Go:
foo := new(Foo)
Here new is not a message that is sent to a receiver object. It is a language built-in keyword that is called with the parameter Foo, where Foo is not an object. So Go does not apply message passing here. But relax, the case is purposefully a little extreme one. Many many OOP languages do instance creation the way as in Go, for example Java, C#, C++, Scala, Groovy, D (or even shorter simply Foo() as in Kotlin). It is a trade-off between purity and efficiency and in this sense has little to do with being object-oriented or not. It is more about the degree of object-orientedness. In fact, the only languages I know that do instance creation by applying message passing and "everything is an object" beside Smalltalk are Objective-C [3] and Ruby [4].
The important point is that Go allows for the creation of instances. Otherwise we would be doing modular programming as in old venerable Modula II. As a side note Go allows for some flexibility with regards to function invocation that most languages don't offer:
type myint int
func (self *myint) catch22() int {
return int(*self) + 22
}
func main() {
myi := myint(123)
println(myi.catch22())
}
But this has nothing to do with message passing and being object-oriented or not. The broader applicability of methods in Go was only something I thought was worth noting.
It is often said that object-oriented programming is about inheritance. At the beginning of OOP inheritance received great attention. It was often sold to management as the tool to reduce development costs: you inherit already existing functionality from super classes and this way have much less development work to do. This idea quickly turned out to be too optimistic. In my old Smalltalk times it was already essential in job interviews to be of the opinion that deep class inheritance trees are hard to change and should therefore be avoided. And that was more than 15 years ago. Maybe because inheritance was new and somewhat "spectacular" it received so much attention although OOP was always about message passing [1, 2]. Let's have a look at a sample that shows message passing in Smalltalk:
| foo |
foo := Foo new.
That's all? Yep, but there is a lot happening. The message with the selector (e.g. message label) new is sent to the class Foo. The resulting instance of class Foo is stored in the locally defined variable foo. In Smalltalk a class is an instance of class MetaClass. Being an instance a class is by definition also an object. So Smalltalk is true to the "everything is an object paradigm". This is how the same thing looks like in Go:
foo := new(Foo)
Here new is not a message that is sent to a receiver object. It is a language built-in keyword that is called with the parameter Foo, where Foo is not an object. So Go does not apply message passing here. But relax, the case is purposefully a little extreme one. Many many OOP languages do instance creation the way as in Go, for example Java, C#, C++, Scala, Groovy, D (or even shorter simply Foo() as in Kotlin). It is a trade-off between purity and efficiency and in this sense has little to do with being object-oriented or not. It is more about the degree of object-orientedness. In fact, the only languages I know that do instance creation by applying message passing and "everything is an object" beside Smalltalk are Objective-C [3] and Ruby [4].
The important point is that Go allows for the creation of instances. Otherwise we would be doing modular programming as in old venerable Modula II. As a side note Go allows for some flexibility with regards to function invocation that most languages don't offer:
type myint int
func (self *myint) catch22() int {
return int(*self) + 22
}
func main() {
myi := myint(123)
println(myi.catch22())
}
But this has nothing to do with message passing and being object-oriented or not. The broader applicability of methods in Go was only something I thought was worth noting.
Inheritance
Now we are going to attack the real beast: Inheritance. Long matter short, inheritance is not built into the Go language as in OO languages. But Go has interfaces and therefore dynamic dispatch. This gives us the tools with which we can implement with little extra effort what inheritance is basically about and that is method overwriting (not to be confused with method overloading). From my experience answering questions on the Go user forum about Go being object-oriented or not I know that we first need to spend some time explaining very well why delegation as such (or specifically embedding in Go) is not the same thing as inheritance. The sample code I'm making use of to demonstrate this is taken from this well written Go Primer. It looks like this:
Now we are going to attack the real beast: Inheritance. Long matter short, inheritance is not built into the Go language as in OO languages. But Go has interfaces and therefore dynamic dispatch. This gives us the tools with which we can implement with little extra effort what inheritance is basically about and that is method overwriting (not to be confused with method overloading). From my experience answering questions on the Go user forum about Go being object-oriented or not I know that we first need to spend some time explaining very well why delegation as such (or specifically embedding in Go) is not the same thing as inheritance. The sample code I'm making use of to demonstrate this is taken from this well written Go Primer. It looks like this:
type Base struct {}
func (Base) Magic() { fmt.Print("base magic") }
type Foo struct {
Base
}
func (Foo) Magic() { fmt.Print("foo magic") }
Now let's run we run the code. First we call Magic() on the Base class:
base := new(Base)
base.Magic() // prints "base magic"
No big surprise it prints "base magic" to the console. Next, let's call Magic() on Foo:
foo := new(Foo)
foo.Magic() // prints "foo magic"
And here we go: Go prints "foo magic" to the console and not "base magic". In the sample code Foo delegates to Base. It does not inherit from Base. Nevertheless, the same thing happens here as with inheritance: foo.Magic() prints "foo magic". So we have the same effect as with inheritance and the whole discussion about Go supporting inheritance or not is void. Right? No, not at all! In fact, the sample code above does not introduce any code that makes the difference between delegation and inheritance visible. This was done on purpose as a little pedagogical trick knowing from experience that a stepwise approach makes people better understand the matter.
Okay, but now we are getting real and are adding another method named MoreMagic() to the show:
func (self Base) MoreMagic() {
self.Magic()
self.Magic()
}
Note, that it is added to Base and very importantly not to Foo. Otherwise we would get the same misleading effect as in the code just shown before. The decisive factor about MoreMagic() is that it is defined in Base (and not in a "subclass") and calls a method Magic(), which is defined in both, Base and Foo. All right, now let's see what happens when we call MoreMagic():
base := new(Base)
base.Magic() // prints "base magic"
base.MoreMagic() // prints "base magic base magic"
foo := new(Foo)
foo.Magic() // prints "foo magic"
foo.MoreMagic() // prints "base magic base magic" !!
And here we have the proof that method overwriting does not take place in Go. Because what is happening here is delegation and not inheritance. What's that? Well, foo.MoreMagic() prints "base magic base magic". In a language that supports inheritance (e.g. method overrwriting) "foo magic foo magic" would be printed to the console. We can work around it by adding MoreMagic() to Foo as well, but this results in code duplication. More importantly, the idea of method overwriting is that this is done once and forever in a superclass and no subclass needs to know about it. In fact, there is a way you can get method overwriting to work with little extra coding effort. It is described in the blog "Inner Pattern to mimic Method Overwriting in Go" I once wrote some time ago. I believe hardcore Gophers would call the solution described in that blog as "against the language". However, it shows that method overwriting can be done in Go without too much effort. And this is what matters for this article.
Inheritance is also about inheriting variables, which I haven't talked about so far. This a little spectacular issue. There is little use in defining a variable in a subclass with the same name in a superclass. In fact, many OO languages don't allow for this as it is the source of sometimes hard to find bugs. So "variable overwriting" is nothing interesting to look into except for the issue with scoping. If an inherited variable has protected scope it can be seen from subclasses, but not from classes outside of the inheritance path. This cannot be modelled in Go as variables or methods are either private or public. The Go creators applied a smart little trick here to get around this by defining private variables and methods as visible for all methods within the same package (and not only visible within the class where defined).
func (Base) Magic() { fmt.Print("base magic") }
type Foo struct {
Base
}
func (Foo) Magic() { fmt.Print("foo magic") }
Now let's run we run the code. First we call Magic() on the Base class:
base := new(Base)
base.Magic() // prints "base magic"
No big surprise it prints "base magic" to the console. Next, let's call Magic() on Foo:
foo := new(Foo)
foo.Magic() // prints "foo magic"
And here we go: Go prints "foo magic" to the console and not "base magic". In the sample code Foo delegates to Base. It does not inherit from Base. Nevertheless, the same thing happens here as with inheritance: foo.Magic() prints "foo magic". So we have the same effect as with inheritance and the whole discussion about Go supporting inheritance or not is void. Right? No, not at all! In fact, the sample code above does not introduce any code that makes the difference between delegation and inheritance visible. This was done on purpose as a little pedagogical trick knowing from experience that a stepwise approach makes people better understand the matter.
Okay, but now we are getting real and are adding another method named MoreMagic() to the show:
func (self Base) MoreMagic() {
self.Magic()
self.Magic()
}
Note, that it is added to Base and very importantly not to Foo. Otherwise we would get the same misleading effect as in the code just shown before. The decisive factor about MoreMagic() is that it is defined in Base (and not in a "subclass") and calls a method Magic(), which is defined in both, Base and Foo. All right, now let's see what happens when we call MoreMagic():
base := new(Base)
base.Magic() // prints "base magic"
base.MoreMagic() // prints "base magic base magic"
foo := new(Foo)
foo.Magic() // prints "foo magic"
foo.MoreMagic() // prints "base magic base magic" !!
And here we have the proof that method overwriting does not take place in Go. Because what is happening here is delegation and not inheritance. What's that? Well, foo.MoreMagic() prints "base magic base magic". In a language that supports inheritance (e.g. method overrwriting) "foo magic foo magic" would be printed to the console. We can work around it by adding MoreMagic() to Foo as well, but this results in code duplication. More importantly, the idea of method overwriting is that this is done once and forever in a superclass and no subclass needs to know about it. In fact, there is a way you can get method overwriting to work with little extra coding effort. It is described in the blog "Inner Pattern to mimic Method Overwriting in Go" I once wrote some time ago. I believe hardcore Gophers would call the solution described in that blog as "against the language". However, it shows that method overwriting can be done in Go without too much effort. And this is what matters for this article.
Inheritance is also about inheriting variables, which I haven't talked about so far. This a little spectacular issue. There is little use in defining a variable in a subclass with the same name in a superclass. In fact, many OO languages don't allow for this as it is the source of sometimes hard to find bugs. So "variable overwriting" is nothing interesting to look into except for the issue with scoping. If an inherited variable has protected scope it can be seen from subclasses, but not from classes outside of the inheritance path. This cannot be modelled in Go as variables or methods are either private or public. The Go creators applied a smart little trick here to get around this by defining private variables and methods as visible for all methods within the same package (and not only visible within the class where defined).
Classes
When talking about OOP we cannot miss out on classes. It is sometimes said that Go does not have them, because Go has a language construct called "structs", but no one called "class". In fact, structs in Go serve as structs and classes at the same time. You can invoke a method on a struct and therefore a struct in Go can also do what classes can do (except for inheritance). The very first code snippet in this article actually did just this:
type Base struct {}
func (Base) Magic() { fmt.Print("base magic") }
base := new(Base)
base.Magic()
Here method Magic() is defined for struct Base. You need to create an instance of Base before you can invoke Magic() on it. In Go methods are defined outside structs (akka classes). This is unusual as for most class-based languages a method that can be invoked on an instance of some class needs to be defined inside that class. The way it is done in Go is just a different way of denoting that a method is associated with some struct. In fact, this is nothing new. It was done in Oberon like that long time before Go. When I looked at Go for the first time it reminded me a lot of Oberon.
Polymorphism
A gross concept in OOP is polymorhpism. Actually, it is probably the least understood idea of OOP. I never understood why, although polymorhpism is very important to enable the developer to do things in a transparent way. Polymorhpism means that different objects respond to the same message with their own proprietary behavior. This is why this feature is named polymorphism, which means something like "different shapes". Needless to say that Go supports this:
type Cat struct {
}
func (self Cat) MakeNoise() {
fmt.Print("meow!")
}
type Dog struct {
}
func (self Dog) MakeNoise() {
fmt.Print("woof!")
}
cat := new(Cat)
cat.MakeNoise() // prints "meow!"
dog := new(Dog)
dog.MakeNoise() // prints "woof!"
When talking about OOP we cannot miss out on classes. It is sometimes said that Go does not have them, because Go has a language construct called "structs", but no one called "class". In fact, structs in Go serve as structs and classes at the same time. You can invoke a method on a struct and therefore a struct in Go can also do what classes can do (except for inheritance). The very first code snippet in this article actually did just this:
type Base struct {}
func (Base) Magic() { fmt.Print("base magic") }
base := new(Base)
base.Magic()
Here method Magic() is defined for struct Base. You need to create an instance of Base before you can invoke Magic() on it. In Go methods are defined outside structs (akka classes). This is unusual as for most class-based languages a method that can be invoked on an instance of some class needs to be defined inside that class. The way it is done in Go is just a different way of denoting that a method is associated with some struct. In fact, this is nothing new. It was done in Oberon like that long time before Go. When I looked at Go for the first time it reminded me a lot of Oberon.
Polymorphism
A gross concept in OOP is polymorhpism. Actually, it is probably the least understood idea of OOP. I never understood why, although polymorhpism is very important to enable the developer to do things in a transparent way. Polymorhpism means that different objects respond to the same message with their own proprietary behavior. This is why this feature is named polymorphism, which means something like "different shapes". Needless to say that Go supports this:
type Cat struct {
}
func (self Cat) MakeNoise() {
fmt.Print("meow!")
}
type Dog struct {
}
func (self Dog) MakeNoise() {
fmt.Print("woof!")
}
cat := new(Cat)
cat.MakeNoise() // prints "meow!"
dog := new(Dog)
dog.MakeNoise() // prints "woof!"
This and That
This section deals with some other things that are often provided by OO languages, but are not worth being dedicated each an entire chapter for.
Method Overloading. There is some other feature that is supported by many OO languages
which is called method overloading where methods of the same name
defined in the same class or inheritance path may have different
paramter lists (different length, different types). Go does not have
that and good old pure object-oriented Smalltalk also doesn't have it.
So let's agree that it is not essential in OOP. It is just a nicety.
Some people even argue that method overloading can have bad side
effects (I remember having read a good article about it by Gilad
Bracha, but couldn't find it again even after searching through the
Internet for a long time).
No Class Methods. Go does not have class methods or class variables. A class method is a method that does not act on an instance of a class and produces and instance-specific result, but produces the same result for all instances of a class. In the same way a class variable contains the same value for instances of a class [5]. For example, the lack of class methods can be compensated for in Go this way:
package juice
type Juice struct{
}
func FromApples(apples []Apple) Juice {
}
func FromOranges(oranges []Orange) Juice {
}
juice := juice.FromApples(...)
type Juice struct{
}
func FromApples(apples []Apple) Juice {
}
func FromOranges(oranges []Orange) Juice {
}
juice := juice.FromApples(...)
The code above was shamelessly stolen from a post on the Go user forum. This looks almost as if FormApples were a class method invoked on a class named Juice. Truly, it is a function defined in a package named juice. What is decisive is that you don't have to know by heart that a constructor FromApples exists somewhere in the namespace. You want to create an instance of Juice and hence you look for constructors of Juice in package juice where you will find it, because it's the place to look for it first. Similarly, class methods can be easily mapped by package-level variables.
Go Base Library not always really OO
Go on language level has almost everything it takes to be called object-oriented. To me it does not look exactly the same way on the level of Go's base library. Here is some code to show what I mean:
package main
import (
"fmt"
"strconv"
"strings"
)
func main() {
i := strconv.Itoa(123)
fmt.Println(i)
i2, _ := strconv.Atoi(t)
fmt.Println(i2)
str := "foo"
str = strings.ToUpper(str)
fmt.Println(str)
}
Go Base Library not always really OO
Go on language level has almost everything it takes to be called object-oriented. To me it does not look exactly the same way on the level of Go's base library. Here is some code to show what I mean:
package main
import (
"fmt"
"strconv"
"strings"
)
func main() {
i := strconv.Itoa(123)
fmt.Println(i)
i2, _ := strconv.Atoi(t)
fmt.Println(i2)
str := "foo"
str = strings.ToUpper(str)
fmt.Println(str)
}
When converting between strings and integers you have to know to find the functions you need in package strconv. When converting strings to upper or lower case you have to know to look in package strings for the functions that do that. All right, strings and numbers are language built-in types in Go as in many other languages whether being object-oriented or not. But you should, for example, not have to know about strconv. Functions Itoa or Atoi should be in package strings as well are maybe in a sub-package of it like strings.conv.
Here is some other sample where some not existing file named "none" is opened and afterwards a check for errors is done:
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
_, err := ioutil.ReadDir("none")
if err != nil {
fmt.Println(os.IsNotExist(err))
}
}
The way to figure out what kind of error happened is to do the call os.IsNotExist(err). Here you need to know that this function IsNotExist exists in package os. You wouldn't have to know what method exists to do that and where to find it if you could just ask the error struct what kind of error it is. The error struct would simply have a method like this (the method below does just what os.IsNotExist is doing):
func (self IoError) IsNotExist() bool {
switch pe := self.(type) {
case nil:
return false
case *PathError:
err = pe.Err
case *LinkError:
err = pe.Err
}
return self == syscall.ERROR_FILE_NOT_FOUND ||
self == syscall.ERROR_PATH_NOT_FOUND || err == ErrNotExist
}
You would just need to have a look what methods struct IoError provides (IoError is a name I just made up). You see that is has a method IsNotExist(), which is what you need and the you can call it to get the job done:
if err != nil {
fmt.Println(err.IsNotExist())
}
That's all. If you don't want to call this object-oriented programming, then you could call it programing with abstract data types or something. However, what is done in many places in the Go standard library is programming with global functions as in C.
So the impression of the Go base library gives some mixed feelings. In many ways the Go base library has a function-oriented organisation structure as in C and not a class-based organisation structure as in modular languages or OO languages. To me this is really unfortunate, because the lack of inheritance would not be that much of a problem given the fact that you can work around it. But the function-oriented organisation of the base library in many cases inevitably throws you back into some function-oriented programming as in C. Rust, which has some areas of overlap with Go, is in this way really different and does that kind of things the err.IsNotExist()-style way everywhere in the standard library.
Conclusion
Go has everything it takes to be called a hybrid object-oriented language except for inheritance. Method overwriting, the core feature of inheritance, can be implemented with little extra effort (one extra method per overwritten method) as Go has interfaces and this way also dynamic dispatch. How to do this is explained in this blog. So if you really can't live without inheritance, the good news is that it can be done. The function-oriented organisation structure of the Go base library rather than being class-oriented leaves some bad aftertaste behind, though.
A well-balanced Last Comment
Programming languages are like human beings: When you are too strict with them about a particular trait you might overlook many of the good traits. The real good trait to mention about Go is concurrency. IMHO, if some application is heavily concurrent Go might be a suitable choice and the language being in some ways relatively simple becomes a secondary issue. CSP-style inter-process communication as in Go using channels and goroutines makes things so much simpler than working with locks, mutexes, semaphores, etc.
I have spent some years working on a quite concurrent application where I was mostly busy with reproducing deadlocks and race conditions and looking for ways to fix them (neither nor is easy and often takes quite some time). From that point of view concurrency control in Go through the use of channels greatly simplifies things in concurrent programming and saves an enormous amount of time.
Also, Go solves the C10K problem out of the box. This comes for the price for a reduced level of pre-emptiveness. But this can be dealt with in most cases and for some applications this issue does not even matter.
Related Articles
Inner Pattern to mimic Method Overwriting in Go
Is Go an Object Oriented language?
Go Tutorial: Object Orientation and Go's Special Data Types
A well-balanced Last Comment
Programming languages are like human beings: When you are too strict with them about a particular trait you might overlook many of the good traits. The real good trait to mention about Go is concurrency. IMHO, if some application is heavily concurrent Go might be a suitable choice and the language being in some ways relatively simple becomes a secondary issue. CSP-style inter-process communication as in Go using channels and goroutines makes things so much simpler than working with locks, mutexes, semaphores, etc.
I have spent some years working on a quite concurrent application where I was mostly busy with reproducing deadlocks and race conditions and looking for ways to fix them (neither nor is easy and often takes quite some time). From that point of view concurrency control in Go through the use of channels greatly simplifies things in concurrent programming and saves an enormous amount of time.
Also, Go solves the C10K problem out of the box. This comes for the price for a reduced level of pre-emptiveness. But this can be dealt with in most cases and for some applications this issue does not even matter.
Related Articles
Inner Pattern to mimic Method Overwriting in Go
Is Go an Object Oriented language?
Go Tutorial: Object Orientation and Go's Special Data Types
[1] Inheritance was invented in 1967 for Simula and then was picked up later by Smalltalk, see this article on Wikipedia.
[2] see this article about message passing on Wikepedia
[3] Objective-C: Foo *foo = [[Foo alloc] init];
[4] Ruby: foo = Foo.new()
[2] see this article about message passing on Wikepedia
[3] Objective-C: Foo *foo = [[Foo alloc] init];
[4] Ruby: foo = Foo.new()
[5] Class methods and class variables are called static methods and
variables in Java borrowing the terminology from C/C++, where it it
actually exactly mean the same thing.
Keine Kommentare:
Kommentar veröffentlichen