Inheritance is a well-known and widely practiced concept in the major programming languages. However, the way to achieve “inheritance” in golang is a bit different from the other object-oriented programming languages. In this article, we are going to learn (or, review) how inheritance is done in golang and compare with its equivalent C++ code.
The article uses one problem as a running example and explores its design and implementation in Go and C++. It is meant for those programmers who are experienced in using the inheritance construct in other languages like C++, Java, Python and are looking to learn the same in Go.
In object-oriented programming, inheritance is the mechanism of basing one class (the “derived” class or the “subclass”) upon another (the “base” class or the “superclass”). The name suggests that all the derived classes “inherit” from the base class.
Note that, inheritance has at least two important aspects. First is the structural aspect, where the derived classes inherit the data members, if any, of the base class. Second is the behavioral aspect, where the derived classes inherit the behavior of the base class. I believe that the behavioral aspect should be the only design consideration. This way, the base class serves as an interface while the various derived classes offer different implementations of the same interface.
The following diagram shows a base class for logging, Logger
, and three subclasses derived from it, namely InMemoryLogger
, LocalLogger
, and RemoteLogger
. The base class offers a virtual method for logging, Log()
, and each subclass implements it in its way. Here, Logger
is the interface class and Log()
is an interface method.
The conventional usage of this structure is the construction of logger as shown in the following C++ example. Its static type is Logger
, but its dynamic type could be any one of the Logger
subclasses (here, InMemoryLogger
).
Logger * logger = new InMemoryLogger;
Beyond the creation, the logger object can be used in the same way — via the interface method — irrespective of whatever dynamic/concrete type it holds.
logger->Log(“Hello from logger”);
The clients and APIs almost always use the base class type.
For many decades, since its introduction to Simula in 1969, this has been the conventional structure for inheritance. Although there are differences between the languages (e.g., Java methods are virtual by default while C++ methods are not), the following basic idea always held.
The derived class type and the base class type are explicitly connected through some language mechanism.
Following are a few examples.
C++
class InMemoryLogger : public Logger
Java
public class InMemoryLogger extends Logger
Python
class InMemoryLogger(Logger)
However, in golang, there is no explicit connection between a base class type and a derived class type; the behavior is obtained implicitly.
From “A Tour of Go” at https://tour.golang.org/methods/10
Interfaces are implemented implicitly
A type implements an interface by implementing its methods. There is no explicit declaration of intent, no “implements” keyword.
An inheritance in golang is structured in two steps as outlined below.
Step 1: Define an interface
type LoggerInterface interface {
Log(string)
}
Step 2: Define the “derived” struct and its method with a signature compatible with the interface function defined above. Note that the struct may contain member variables as necessary for its implementation.
// InMemoryLogger saves the log messages in memory.type InMemoryLogger struct {
messages []string
}func (this *InMemoryLogger) Log(mesg string) {
this.messages = append(this.messages, mesg)
}
Thanks to the implicit interfaces, the golang compiler can automatically deduce that the InMemoryLogger
struct type satisfies the LoggerInterface
interface type despite there being no explicit connection between them. This is based on the fact that InMemoryLogger
implements all the methods of LoggerInterface
(all the method names along with their exact signatures).
However, as a courtesy to the human readers, the struct can, optionally, list the interfaces it satisfies.
type InMemoryLogger struct {
LoggerInterface
messages []string
}
Once such a structure is laid out, the usage is as follows.
First, define a logger interface object, say, logger.
var logger LoggerInterface
Second, construct a concrete logger interface implementer object (here, InMemoryLogger
) and assign it to the interface object.
logger = &InMemoryLogger{}
The above two statements (definition and assignment) can be combined as follows.
var logger LoggerInterface = &InMemoryLogger{}
Finally, invoke the interface method on the interface object.
logger.Log(“Hi, there!”)
Based on the assignment to the interface object, the call will be routed to the appropriate concrete object.
I know what you are thinking. With only one concrete implementation of the interface, the polymorphism is not quite so obvious. So, to make it clear and obvious, let us add another concrete implementation(LocalLogger
) into the mix.
To illustrate the point on multiple concrete implementations, let us see some illustrative usage code. The following code creates multiple LoggerInterface
objects of different concrete implementations, saves them in a slice, and sends the same sequence of messages to both.
Furthermore, we can add another method to the LoggerInterface
to retrieve the logged messages. Once we do that, we can verify that both the InMemoryLogger
implementation and the LocalLogger
implementation retrieve the same content as testMessages. The complete examples for both C++ and golang are available in the github repository https://github.com/arunksaha/inheritance.
The article demonstrates how to achieve behavioral inheritance in golang.
Appendix
All the code used in the article can be found in the github repository at https://github.com/arunksaha/inheritance
Here is the complete C++ code from the github project.
Here is the complete golang code from the github project.