Methods: a taste of OOP
In the previous chapter, we saw some nice things to do with functions as values
that can be assigned to variables, passed to and returned from other functions.
We finished with the fact that we actually can use functions as
Today, we'll see a kind of an extrapolation of functions, that is functions with a receiver, which are called methods.
What is a method?
Suppose that you have a
struct representing a rectangle. And you want this
rectangle to tell you its own area.
The way we'd do this with functions would be something like this:
This works as expected, but in the example above, the function
area is not
part of the
Rectangle type. It expects a
Rectangle parameter as
Yes, so what? You'd say. No problem, it's just if you decide to add circles and triangles and other polygons to your program, and you want to compute their areas, you'd have to write different functions with different names for a functionality or a characteristic that is, after all, the same.
You'd have to write:
And this is not elegant. Because the area of a shape is a characteristic of this shape. It should be a part of it, belong to it, just like its other fields.
And this leads us to methods: A method is function that is bound or attached to
a given type. Its syntax is the same as a traditional function except that we
specify a receiver of this type just after the keyword
In the words of Rob Pike:
"A method is a function with an implicit first argument, called a receiver."
func (ReceiverType r) func_name (parameters) (results)
Let's illustrate this with an example:
A few things to note about methods:
- Methods of different receivers are different methods even if they share the name.
- A method has access to its receiver's fields (data).
- A method is called with the dot notation like
So? Are methods applicable only for
struct types? The anwser is No. In fact,
you can write methods for any named type that you define, that is not a
If you missed the comma remark, go back and re-read the code carefully.
Now, wait a minute! (You say this with your best Bill Cosby's impression) What is this "named types" thing that you're telling me now? Sorry, my bad. I didn't need to tell you before. And I didn't want to distract you with this detail back then.
It's in fact easy. You can define new types as much as you want.
struct is in fact a specific case of this syntax.
Look back, we actually used this in the previous chapter!
You can create aliases for built-in and composite types with the following syntax:
type type_name type_literal
See? It's actually easy, and it can be handy to give more meaning to your code, by giving names to complicated composite -- or even simple -- types.
Back to our methods.
So, yes, you can define methods for any named type, even if it's an alias to a pre-declared type. Needless to say that you can define as many methods, for any given named type, as you want.
Let's see a more advanced example, and we will discuss some details of it just after.
Tis the story of a set of colored boxes. They have widths, heights, depths and colors of course! We want to find out the color of the biggest box, and eventually paint them all black (Because you know... I see a red box, and I want it painted black...)
Here we Go!
So we defined some
consts with consecutive values using the
to represent some colors.
And then we declared some types:
Colorwhich is an alias to
Boxstruct to represent a box, it has three dimensions and a color.
BoxListwhich is a slice of
Simple and straightforward.
Then we wrote some methods for these types:
Volume()with a receiver of type
Boxthat returns the volume of the received box.
SetColor(c Color)sets its receiver's color to
BiggestsColor()with a receiver of type
BoxListreturns the color of the
Boxwith the biggest volume that exists within the slice.
PaintItBlack()with a receiver of type
BoxListsets the colors of all
Boxes in the slice to BLACK.
String()a method with a receiver of type
Colorreturns a string representation of this color.
All this is simple. For real. We translated our vision of the problem into things that have methods that describe and implement a behavior.
Now, look at line 25 that I highlighted on purpose. The receiver is a pointer to
Box! Yes, you can use
*Box too. The restriction with methods is that the
Box itself (or any receiver's type) shouldn't be a pointer.
Why did we use a pointer? You have 10 seconds to think about it, and then read on the next paragraph. I'll start counting:
10, 9, 8...
Ok! Let's find out if you were correct.
We used a pointer because we needed the
SetColor method to be able to
change the value of the field 'color' of its receiver. If we did not use a
pointer, then the method would recieve a copy of the receiver
(passed by value) and hence the changes that it will make will affect the copy
only, not the original.
Think of the receiver as a parameter that the method has in input, and ensure that you understand and remember the difference between :ref:`passing by value and reference<value-reference>`.
Structs and pointers simplification
Again with the method
SetColor, intelligent readers
(You are one of them, this I know!)
would say that we should have written
(*b).color = c instead of
c, since we need to dereference the pointer
b to access the field color.
This is true! In fact, both forms are accepted because Go knows that you want
to access the fields of the value pointed to by the pointer (since a pointer has
no notion of fields) so it assumes that you wanted
(*b) and it simplifies
this for you. Look Ma, Magic!
Even more simplification
Experienced readers will also say:
"On line 43 where we call
bl[i], shouldn't it be
SetColorexpects a pointer of type
*Boxand not a value of type
This is also quite true! Both forms are accepted. Go automatically does the conversion for you because it knows what type the method expects as a receiver.
In other words:
If a method
Mexpects a receiver of type
*T, you can call the method on a variable
Twithout passing it as
If a method
Mexpects a receiver of type
T, you can call the method on a variable
*Twithout passing it as
So don't worry, Go knows the type of a receiver, and knowing this it simplifies
V.M() as a shorthand of
P.M() as a
Anyone who has done much C/C++ programming will have realized at this point the world of pain that Go saves us from by simply using sane assumptions.
Well, well, well... I know these pointers/values matters hurt heads, take a break, go out, have a good coffee, and in the next chapter we will see some cool things to do with methods.