Object-oriented

We talked about functions and structs in the last two sections, but did you ever consider using functions as fields of a struct? In this section, I will introduce you to another form of function that has a receiver, which is called a method.

method

Suppose you define a “rectangle” struct and you want to calculate its area. We’d typically use the following code to achieve this goal.

  1. package main
  2. import "fmt"
  3. type Rectangle struct {
  4. width, height float64
  5. }
  6. func area(r Rectangle) float64 {
  7. return r.width * r.height
  8. }
  9. func main() {
  10. r1 := Rectangle{12, 2}
  11. r2 := Rectangle{9, 4}
  12. fmt.Println("Area of r1 is: ", area(r1))
  13. fmt.Println("Area of r2 is: ", area(r2))
  14. }

The above example can calculate a rectangle’s area. We use the function called area, but it’s not a method of the rectangle struct (like class methods in classic object-oriented languages). The function and struct are two independent things as you may notice.

It’s not a problem so far. However, if you also have to calculate the area of a circle, square, pentagon, or any other kind of shape, you are going to need to add additional functions with very similar names.

2.5. Object-oriented - 图1

Figure 2.8 Relationship between function and struct

Obviously that’s not cool. Also, the area should really be the property of a circle or rectangle.

This is where a method comes to play. The method is a function affiliated with a type. It has similar syntax as function except, after the func keyword has a parameter 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.

  1. "A method is a function with an implicit first argument, called a receiver."

Syntax of method.

  1. func (r ReceiverType) funcName(parameters) (results)

Let’s change our example using method instead.

  1. package main
  2. import (
  3. "fmt"
  4. "math"
  5. )
  6. type Circle struct {
  7. radius float64
  8. }
  9. type Rectangle struct {
  10. width, height float64
  11. }
  12. // method
  13. func (c Circle) Area() float64 {
  14. return c.radius * c.radius * math.Pi
  15. }
  16. // method
  17. func (r Rectangle) Area() float64 {
  18. return r.width * r.height
  19. }
  20. func main() {
  21. c1 := Circle{10}
  22. c2 := Circle{25}
  23. r1 := Rectangle{9, 4}
  24. r2 := Rectangle{12, 2}
  25. fmt.Println("Area of c1 is: ", c1.Area())
  26. fmt.Println("Area of c2 is: ", c2.Area())
  27. fmt.Println("Area of r1 is: ", r1.Area())
  28. fmt.Println("Area of r2 is: ", r2.Area())
  29. }

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.

2.5. Object-oriented - 图2

Figure 2.9 Methods are different in different structs

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.

Can the receiver only be a struct? Of course not. Any type can be the receiver of a method. You may be confused about customized types. Struct is a special kind of customized type -there are more customized types.

Use the following format to define a customized type.

  1. type typeName typeLiteral

Examples of customized types:

  1. type age int
  2. type money float32
  3. type months map[string]int
  4. m := months {
  5. "January":31,
  6. "February":28,
  7. ...
  8. "December":31,
  9. }

I hope that you know how to use customized types now. Similar to typedef in C, we use ages to substitute int in the above example.

Let’s get back to talking about method.

You can use as many methods in custom types as you want.

  1. package main
  2. import "fmt"
  3. const (
  4. WHITE = iota
  5. BLACK
  6. BLUE
  7. RED
  8. YELLOW
  9. )
  10. type Box struct {
  11. width, height, depth float64
  12. color Color
  13. }
  14. type Color byte
  15. type BoxList []Box //a slice of boxes
  16. // method
  17. func (b Box) Volume() float64 {
  18. return b.width * b.height * b.depth
  19. }
  20. // method with a pointer receiver
  21. func (b *Box) SetColor(c Color) {
  22. b.color = c
  23. }
  24. // method
  25. func (bl BoxList) BiggestsColor() Color {
  26. v := 0.00
  27. k := Color(WHITE)
  28. for _, b := range bl {
  29. if b.Volume() > v {
  30. v = b.Volume()
  31. k = b.color
  32. }
  33. }
  34. return k
  35. }
  36. // method
  37. func (bl BoxList) PaintItBlack() {
  38. for i, _ := range bl {
  39. bl[i].SetColor(BLACK)
  40. }
  41. }
  42. // method
  43. func (c Color) String() string {
  44. strings := []string{"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
  45. return strings[c]
  46. }
  47. func main() {
  48. boxes := BoxList{
  49. Box{4, 4, 4, RED},
  50. Box{10, 10, 1, YELLOW},
  51. Box{1, 1, 20, BLACK},
  52. Box{10, 10, 1, BLUE},
  53. Box{10, 30, 1, WHITE},
  54. Box{20, 20, 20, YELLOW},
  55. }
  56. fmt.Printf("We have %d boxes in our set\n", len(boxes))
  57. fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
  58. fmt.Println("The color of the last one is", boxes[len(boxes)-1].color.String())
  59. fmt.Println("The biggest one is", boxes.BiggestsColor().String())
  60. // Let's paint them all black
  61. boxes.PaintItBlack()
  62. fmt.Println("The color of the second one is", boxes[1].color.String())
  63. fmt.Println("Obviously, now, the biggest one is", boxes.BiggestsColor().String())
  64. }

We define some constants and customized types.

  • Use Color as alias of byte.
  • Define a struct Box which has fields height, width, length and color.
  • Define a struct BoxList which has Box 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.

  1. package main
  2. import "fmt"
  3. type Human struct {
  4. name string
  5. age int
  6. phone string
  7. }
  8. type Student struct {
  9. Human // anonymous field
  10. school string
  11. }
  12. type Employee struct {
  13. Human
  14. company string
  15. }
  16. // define a method in Human
  17. func (h *Human) SayHi() {
  18. fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
  19. }
  20. func main() {
  21. sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
  22. mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
  23. sam.SayHi()
  24. mark.SayHi()
  25. }

Method Overriding

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.

  1. package main
  2. import "fmt"
  3. type Human struct {
  4. name string
  5. age int
  6. phone string
  7. }
  8. type Student struct {
  9. Human
  10. school string
  11. }
  12. type Employee struct {
  13. Human
  14. company string
  15. }
  16. func (h *Human) SayHi() {
  17. fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
  18. }
  19. func (e *Employee) SayHi() {
  20. fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
  21. e.company, e.phone) //Yes you can split into 2 lines here.
  22. }
  23. func main() {
  24. sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
  25. mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
  26. sam.SayHi()
  27. mark.SayHi()
  28. }

You are able to write an Object-oriented program now, and methods use rule of capital letter to decide whether public or private as well.