A Programmer's Guide To Go Part 2 - Objects and Interfaces
Written by Mike James   
Thursday, 25 July 2019
Article Index
A Programmer's Guide To Go Part 2 - Objects and Interfaces
Wot No Inheritance?
Interfaces

Wot No Inheritance?!

So how do you build an object hierarchy? 

The simple answer is that you don't. Go encourages you to use composition which is, these days, generally thought to be more robust than inheritance. Although not everyone agrees.

Although some of the examples given earlier make use of more general types structs are the thing you tend to use to create objects. These provide data fields and by using the receiver mechanism methods. However as the field of a struct can be any type you can also have fields that are structs. 

For example suppose we want to build a graphics library. The first thing we need is a point object:

type point struct {
    x, y float32
}

Next we might need a circle object:

type circle struct {
    center point
    radius float32
}

func (c circle) area() float32 {
    return 3.14*c.radius * c.radius
}

Notice that we have used the point object within the circle object - a circle contains a point object.

We can now create a circle in the usual way an we can use a composite literal to initialize it:

mycircle := circle{point{10.0, 20.0}, 30.0}

Now we can call the area method:

fmt.Println(mycircle.area())

If you want to access the center co-ordinates you use a fully qualified name e.g:

circle.center.x

So far so good - now let's introduce an ellipse which for the sake of the example we want to treat as a special type of circle, one with both a horizontal and a vertical radius:

type ellipse struct { 
    c circle
    rh float32
    rv float32
}

Now we can create an ellipse using the same sort of composite literal:

myellipse := ellipse{circle{point{10.0, 20.0}, 0.0},
                                         30.0, 40.0}

Notice that we are now considering an ellipse as having or containing a circle which is not good design, but the point is that we can access the center of the ellipse using:

fmt.Println(myellipse.c.center.x)

Notice that the qualified name is getting longer. This is the problem with aggregation. Each time you add an object which is made up of objects, which are made up of objects and so on, the name gets more complicated. 

Go has a solution for this - don't give the new fields names.

goruns

Anonymous Fields

If you create an anonymous field then the struct gets all of the fields of the struct you are aggregating.  For example, if you change the definition of ellipse to:

type ellipse struct {
    circle
    rh float32
    rv float32
}

the circle is now "embedded" in the ellipse stuct - its fields are embedded in the ellipse. As a result you can now reference the fields of the circle as if they belonged to the ellipse: 

fmt.Println(myellipse.center.x)

This all seems very reasonable and embedding is a good idea. What might surprise you is that that when you embed a struct in a struct you also embed its methods. That is, ellipse has an area method that can be called using:

myellipse.area()

What is surprising here is that area has a receiver of type circle but now it is working with a receiver of type ellipse. In fact the ellipse type has an area method that will accept either a circle or an ellipse as its receiver. This is an example of a promoted method, i.e. one that is defined on one struct but used by another as if it had been defined for it. 

Of course, the promoted area method computes the area of a circle not an ellipse. At this point you are probably thinking it's time to override the area method promoted from circle and this is exactly what you can do. If you define an ellipse area method:

func (e ellipse) area() float32 {
    return 3.14 * e.rh * e.rv
}

then the circle's area method isn't promoted. 

Notice that the function has to have the same name and signature as the method that would be promoted in its place. 

You can see that the embedding mechanism gives you many of the features of inheritance. What it lacks is the usual hierarchical structure of classes and sub-classes but this is often regarded as being too difficult to manage in most projects. That is, complex inheritance hierarchies are bad.

Go doesn't have hierarchies, you simply add objects together. Notice that you can have multiple anonymous fields and this means that Go's embedding is more like multiple than single inheritance.

If you think of Go as a low level language then this is a very direct approach to creating methods for "objects" without having the complex overheads of an object hierarchy within your strong typing. It can be argued however that these are small gains and you can achieve a very similar programming style in C - but this is the road that led to C++!

goicon2



Last Updated ( Saturday, 03 August 2019 )