A Programmer's Guide To Go Part 2 - Objects And Interfaces
Written by Mike James   
Thursday, 05 December 2013
Article Index
A Programmer's Guide To Go Part 2 - Objects And Interfaces
Interfaces

In the second part of our mini-series on Google's alternative to C, it is time to come face to face with Go's most interesting features - objects and interfaces

A Programmer's Guide To Go

  1. A Programmer's Guide To Go With LiteIDE
  2. A Programmer's Guide To Go Part 2 - Objects And Interfaces
  3. A Programmer's Guide To Go Part 3 - Goroutines And Concurrency

Part 2  Objects And Interfaces

goicon2

Go takes a very different approach to objects than the class based object-oriented languages that most programmers are familiar with. You don't define a class and then use it to instantiate an object and there is no type hierarchy. There also is no inheritance, no function overloading and... 

If this all sound as if Go is going to turn out to be a primitive low level language think again. Its approach is best described as low level yes but it also quite sophisticated. 

Objects

So how do you create objects in Go?

The simple answer is you don't exactly create objects. You simply associate functions with types and you can use these functions as if they were methods of the objects or values that you create using the types.

The way that this is implemented is very simple and very much like the way that languages such as JavaScript do the job.  All you have to do  is to assign a "receiver" to a function to create a method.

A receiver acts like an extra parameter of a specified type passed to the function. If you know how objects work in other languages then you might recognize it as being very similar to the "this" parameter passed into object methods. 

For example,  

type point struct {
        x, y int    
}

func (p point) Sum() int {
        return p.x+p.y
}

this binds the function Sum to the receiver type point. You can see that it looks like an additional parameter for the function and indeed this is what it is. The function you have created corresponds to a function type with a signature which has an additional parameter of the receiver type as its first parameter. This is also how receivers are implemented - it is very simple and low level. Notice also that unlike the use of a default receiver e.g. "this" you can give your receiver a sensible name.  

Now when you create a point value you can also call the Sum method as if it was a method belonging to a point object. For example:

var p1 = point{1, 5}
fmt.Println(p1.Sum())

When you use the Sum method the value that it is being called from is passed as the receiver parameter.  This is how one function definition can be shared between a lot of different values of the same type. 

Notice that while in this example the method was attached to a struct you can attach methods to any type - except for the built in types. If you want to attach a method to a built in type you have to first create a new type with the same underlying type. 

For example if you want to create an integer "object" with a square method you would use something like:

type myint int
func (i myint) square() myint {
    return i * i
}

After this you can do things like:

var j myint = 3    
fmt.Println(j.square())

There is just one complication but it isn't a difficult one.  Go passes all parameters by value and this includes the receiver. This is fine as long as you only want to make use of the values that the receiver provides. If you want to change the receiver in any way you need to pass a pointer.

For example, if we change myint's square method to read:

func (i myint) square() myint {
    i = i * i
    return i
}

Then it returns the same result but it doesn't change the value stored in i. To do this you have to pass the receiver as a pointer to the type:

func (i *myint) square() myint {
    *i = *i * *i
    return *i
}

Now when you do:

var j myint = 3    
fmt.Println(j.square())

the result 9 is displayed but also j is changed to 9. Notice that you don't have to change the way you use a method to take advantage of passing a pointer Go takes care of it for you. However you do have to write your method function remembering to dereference the pointer. 

It is true that the statement:

*i=*i * *i 

looks horrible, but this is about as bad an example as I can find. In practice things look better than this and the type checking helps you to not  forget a dereference operator when you need one..

That's about all there is to Go objects. As stated in the introduction there is no inheritance and methods are not overloaded. 

goicon1

 

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 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

<ASIN:1469769166>

<ASIN:0321774639>



Last Updated ( Thursday, 09 January 2014 )
 
 

   
RSS feed of all content
I Programmer - full contents
Copyright © 2014 i-programmer.info. All Rights Reserved.
Joomla! is Free Software released under the GNU/GPL License.