Object-oriented
Object oriented languages allow programmers to declare a function inside the class definition. Go doesn't allow us to do that, we have to declare a method on a struct via a special syntax.
Methods
We defined a "rectangle" struct and we want to calculate its area. Normally, we would create a function, pass the struct's instance and calculate the area.
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))
}
The above example calculates a rectangle's area. The function and struct are two independent things as you may notice.
The problem arises when we want to generalize things, suppose we want to calculate area of a square, we need to define yet another function called area
, but we can't have two functions of the same name in a file. Also area
isn't a property of the struct Rectangle
.
A method
is affiliated with the type. It has the same syntax as functions do except for an additional parameter after the func
keyword called the receiver
, which is the main body of that method.
Using the same example, Rectangle.area()
belongs directly to rectangle, instead of as a peripheral function. More specifically, length
, width
and area()
all belong to rectangle.
As Rob Pike said.
"A method is a function with an implicit first argument, called a receiver."
Syntax of method.
func (r ReceiverType) funcName(parameters) (results)
Let's change our example using method
instead.
file: code/ObjectOriented/area/area.go
package main
import (
"fmt"
"math"
)
type Rectangle struct {
width, height float64
}
type Circle struct {
radius float64
}
func (r Rectangle) area() float64 {
return r.width*r.height
}
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}
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())
}
Notes for using methods.
- If the name of methods are the same but they don't share the same receivers, they are not the same.
- Methods are able to access fields within receivers.
- Use
.
to call a method in the struct, the same way fields are called.
In the example above, the area() methods belong to both Rectangle and Circle respectively, so the receivers are Rectangle and Circle.
One thing that's worth noting is that the method with a dotted line means the receiver is passed by value, not by reference. The difference between them is that a method can change its receiver's values when the receiver is passed by reference, and it gets a copy of the receiver when the receiver is passed by value.
Any type can be the receiver of a method.
Custom data types
Use the following format to define a custom type.
type typeName typeLiteral
Examples of customized types:
type ages int
type money float32
type months map[string]int
m := months {
"January":31,
"February":28,
...
"December":31,
}
Similar to typedef
in C, we use ages
to substitute int
in the above example.
You can use as many methods in custom types as you want.
file: code/ObjectOriented/box/box.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)
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())
fmt.Println("Let's paint them all black")
boxes.PaintItBlack()
fmt.Println("The color of the second one is", boxes[1].color.String())
fmt.Println("Obviously, now, the biggest one is", boxes.BiggestsColor().String())
}
We define some constants and customized types.
- Use
Color
as alias ofbyte
. - Define a struct
Box
which has fields height, width, length and color. - Define a struct
BoxList
which hasBox
as its field.
Then we defined some methods for our customized types.
- Volume() uses Box as its receiver and returns the volume of Box.
- SetColor(c Color) changes Box's color.
- BiggestsColor() returns the color which has the biggest volume.
- PaintItBlack() sets color for all Box in BoxList to black.
- String() use Color as its receiver, returns the string format of color name.
Is it much clearer when we use words to describe our requirements? We often write our requirements before we start coding.
Use pointer as receiver
Let's take a look at SetColor
method. Its receiver is a pointer of Box. Yes, you can use *Box
as a receiver. Why do we use a pointer here? Because we want to change Box's color in this method. Thus, if we don't use a pointer, it will only change the value inside a copy of Box.
If we see that a receiver is the first argument of a method, it's not hard to understand how it works.
You might be asking why we aren't using (*b).Color=c
instead of b.Color=c
in the SetColor() method. Either one is OK here because Go knows how to interpret the assignment. Do you think Go is more fascinating now?
You may also be asking whether we should use (&bl[i]).SetColor(BLACK)
in PaintItBlack
because we pass a pointer to SetColor
. Again, either one is OK because Go knows how to interpret it!
Inheritance of method
We learned about inheritance of fields in the last section. Similarly, we also have method inheritance in Go. If an anonymous field has methods, then the struct that contains the field will have all the methods from it as well.
file: code/ObjectOriented/employee/employee.go
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human // anonymous field
school string
}
type Employee struct {
Human
company string
}
// define a method in Human
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}
Method overload
If we want Employee to have its own method SayHi
, we can define a method that has the same name in Employee, and it will hide SayHi
in Human when we call it.
file: code/ObjectOriented/human/human.go
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human
school string
}
type Employee struct {
Human
company string
}
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //Yes you can split into 2 lines here.
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}
Methods use rule of capital letter to decide whether public or private as well.