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.
And we finished with the fact that we actually can use functions as fields of
struct
s.
Today, we'll see a kind of an extrapolation of functions: functions with a receiver, simply called: methods.
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 it with functions would be something like this:
package main
import "fmt"
type Rectangle struct{
width, height float64
}
func area(r Rectangle) float64{
return r.width*r.height
}
func main(){
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
fmt.Println("Area of r1 is: ", area(r1))
fmt.Println("Area of r2 is: ", area(r2))
}
Output:
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
its input.
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: area_rectangle, area_circle, area_triangle...
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 func
.
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:
package main
import ("fmt"; "math") //Hey! Look how we used a semi-colon!
type Rectangle struct{
width, height float64
}
type Circle struct{
radius float64
}
/*
Notice how we specified a receiver of type Rectangle to this method.
Notice also how this methods -in this case- doesn't need input parameters
because the data it need is part of its receiver r
*/
func (r Rectangle) area() float64{
return r.width*r.height //using fields of the receiver
}
// Another method with the SAME name but with a different receiver.
func (c Circle) area() float64{
return c.radius*c.radius*math.Pi
}
func main(){
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
c1 := Circle{10}
c2 := Circle{25}
//Now look how we call our methods.
fmt.Println("Area of r1 is: ", r1.area())
fmt.Println("Area of r2 is: ", r2.area())
fmt.Println("Area of c1 is: ", c1.area())
fmt.Println("Area of c2 is: ", c2.area())
}
Output:
A few things about methods:
- Methods of different receivers are different methods even if they share the name.
- A method has access to its receiver fields/data
- A method is called with the dot notation like
struct
fields.
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
pointer.
Example:
package main
import "fmt"
//We define two new types
type sliceOfints []int
type AgesByNames map[string]int
func (s sliceOfints) sum() int{
sum := 0
for _, value := range s{
sum += value
}
return sum
}
func (people AgesByNames) older() string{
a := 0
n := ""
for key, value := range people{
if value > a {
a = value
n = key
}
}
return n
}
func main(){
s := sliceOfints {1, 2, 3, 4, 5}
folks := AgesByNames {
"Bob": 36,
"Mike": 44,
"Jane": 30,
"Popey": 100, //look at this comma. when it's the last and
} //the brace is here, the comma above is obligatory.
fmt.Println("The sum of ints in the slice s is: ", s.sum())
fmt.Println("The older in the map folks is:", folks.older())
}
Output:
Now, wait a minute! (You say this with your Bill Cosby's voice) What is this "named types" thing that you're telling me now? Sorry, my bad. I needn't to tell you before. And 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
just a case of this syntax. And we actually used it in the previous chapter too!
You can create aliases for built-in and composite types with the following syntax:
type type_name type_literal
Examples:
//ages is an alias for int
type ages int
//money is an alias for float32
type money float32
//we define months as a map of strings and their associated number of days
type months map[string]int
//m is a variable of type months
m := months {
"January":31,
"February":28,
...
"December":31
}
See? It's actually easy, and it can be handy to give more meaning to your programs, 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.
It's the story of a set of colored boxes. They have widths, heights and depths and of course colors. We want to find out the color of the biggest of them, and eventually paint them all black (Because you know... I see a red box, and I want it painted black...)
Here we Go!
package main
import "fmt"
const(
WHITE = iota
BLACK
BLUE
RED
YELLOW
)
type Color byte
type Box struct{
width, height, depth float64
color Color
}
type BoxList []Box //a slice of boxes
func (b Box) Volume() float64{
return b.width*b.height*b.depth
}
func (b *Box) SetColor(c Color){
b.color = c
}
func (bl BoxList) BiggestsColor() Color{
v := 0.00
k := Color(WHITE) //initialize it to something
for _, b := range bl{
if b.Volume() > v{
v = b.Volume()
k = b.color
}
}
return k
}
func (bl BoxList) PaintItBlack(){
for i, _ := range bl{
bl[i].SetColor(BLACK)
}
}
func (c Color) String() string{
strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
return strings[c]
}
func main(){
boxes := BoxList{
Box{4, 4, 4, RED},
Box{10, 10, 1, YELLOW},
Box{1, 1, 20, BLACK},
Box{10, 10, 1, BLUE},
Box{10, 30, 1, WHITE},
Box{20, 20, 20, YELLOW},
}
fmt.Printf("We have %d boxes in our set\n", len(boxes))
fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
fmt.Println("The color of the last one is", boxes[len(boxes)-1].color.String())
fmt.Println("The biggest one is", boxes.BiggestsColor().String())
//I want it painted black
fmt.Println("Let's paint them all black")
boxes.PaintItBlack()
fmt.Println("The color of the second one is", boxes[1].color.String())
//obviously, it will be... BLACK!
fmt.Println("Obviously, now, the biggest one is", boxes.BiggestsColor().String())
}
Output:
So we defined some const
s with consecutive values using the iota
idiom
to represent some colors.
And then we declared some types:
Color
which is an alias tobyte
.Box
struct to represent a box, it has three dimensions and a color.BoxList
which is a slice ofBox
.
Simple and straightforward.
Then we wrote some methods for these types:
Volume()
with a receiver of typeBox
that returns its volume.SetColor(c Color)
sets its receiver's color to c.BiggestsColor()
with a receiver of typeBoxList
that returns the color of theBox
that has the biggest volume in this slice.PaintItBlack()
with a receiver of typeBoxList
that sets the colors of all theBox
es in the slice to BLACK.String()
a method with a receiver of typeColor
that returns 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/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
type 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, We used a pointer because we needed the SetColor
method to be able to
change the value of the field 'color' of its receiver. Hadn't we used a pointer,
the method would recieve a copy of the receiver b
(passed by value) and the
changes that it will make will affect the copy, not the original.
Just think of the receiver as a parameter that the method has in input, and remember the difference between :ref:`passing by value and reference<value-reference>`.
Again with the method SetColor
, smart readers (You are one of them, I know)
would say that we should have written (*b).color = c
instead of b.color =
c
, since we need to dereference the pointer b
to access the field color.
That's true, and in fact both forms are accepted because Go knows that you want
to access the field 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.
Smarter readers would say: On line 43 where we call SetColor
on bl[i]
,
shouldn't it be (&bl[i]).SetColor(BLACK)
instead? Since SetColor
expects
a pointer of type *Box
, not a value of type Box
?
Yes, that's also true, and yes 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 M
expects a receiver of type *T
, you can
call it on a variable V
of type T
without passing it as &V
to M
.
Similarly, if a method M
expects a receiver of type T
, you can call it
on a variable P
of type *T
without passing it as *P
to M
.
Example:
package main
import "fmt"
type Number int
//method inc has a receiver of type pointer to Number
func (n *Number) inc(){
*n++
}
//method print has a receiver of type Number
func (n Number) print(){
fmt.Println("The number is equal to", n)
}
func main(){
i := Number(10) //say that i is of type Number and is equal to 10
fmt.Println("i is equal to", i)
fmt.Println("Let's increment it twice")
i.inc() //same as (&i).inc() method expects a pointer, but that's okay
fmt.Println("i is equal to", i)
(&i).inc() //this also works as expected
fmt.Println("i is equal to", i)
p := &i //p is a pointer to i
fmt.Println("Let's print it twice")
p.print() //same as (*p).print() method expects a value, but that's okay
i.print() //this also works as expected
}
Output:
So don't worry, Go knows the type of a receiver, and knowing this it simplifies
by accepting V.M()
as a shorthand of (&V).M()
and P.M()
as a
shorthand for (*P).M()
.
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.