Go 语言中的面向对象

一切皆是对象。

这在 Java 这种强面向对象语言中是一条铁律。然而什么是对象,为什么一切都是对象?初学者很难理解。

在 Java、C++ 这些语言中,实现面向对象的基础设施叫做类。类是对象的抽象,对象是类的具体。本着这样的原则,面向对象的三特性封装、继承、多态拔地而起,浑然天成。Go 语言中的设计却在多个方面有所差异,有些甚至理念迥异,剖析一下这些设计,或许能引发面向对象的更多思考。

Java 和 C++ 中,类是一个对象生成器,是语言的根基,连类型都是通过类定义的。变量和方法全部封装在 class 中,追求彻底的面向对象。

而在 Go 中没有 class,取而代之的是 type。有趣的是,变量和方法是分开的,没有全部包含在类型中。

1
2
3
4
5
6
type Circle struct {
radius float64
}
func (c Circle) area() float64 {
return c.radius*c.radius*math.Pi
}

这样的处理方式似乎更加灵活一些,不必一开始就把类中的所有东西定义好,而是不断增加新的方法,使得程序更容易扩展。

Go 中的 type 相当灵活,不仅可以定义 struct,还可以在基础类型之上定义新的类型。

1
2
3
4
5
6
type color byte
type Box struct {
width, height, depth float64
color Color
}
type BoxList []Box

这样的自定义类型随意组合,可以丰富描述问题的方式。

Go 中个方法也比较特殊,分为 func 和 method 两种。func 是通常意义上的函数,method 则是配合 type 的方法。method 在定义时会指定一个 type,但不从属于 type,只是单独存在于 type 外围。这便是 type 的扩展。

1
2
3
4
5
6
7
8
9
10
11
type Rectangle struct {
width, height float64
}
func area(r Rectangle) float64 {
return r.width*r.height
}
func (r Rectangle) area() float64 {
return r.width*r.height
}

了解了这些,再来看面向对象的三大特性。

封装

将对象的状态信息(eg:Person 对象的 age 变量)隐藏在对象内部,外部程序只能通过该类提供的方法来实现对内部信息的操作和访问。Go 中就是通过 type 和 method 实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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())
}

继承

继承就是子类继承父类的特征和行为,使得子类具有父类的各种属性和方法(重用父类代码)。Go 中的继承则没有继承关系链,子类和父类是完全不同的类型,还能重用父类的方法和成员。这得益于 Go 中的匿名字段。如果匿名字段实现了一个 method,那么包含这个匿名字段的 struct 也能调用该 method。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
}
//在human上面定义了一个method
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()
}

这种设计其实是以 Has-A 替代 了 Is-A 的方式实现。

多态

相同类型的(引用)变量,调用同一个方法(父类和子类都有的同名方法)时呈现出多种不同的行为特征。Go 中实现起来也很简单的,在上面的例子中,Student 和 Employee 都继承了 Human,再增加一个 Student 的 SayHi 方法,就实现了多态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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
}
//在human上面定义了一个method
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)
}
func (s *Student) SayHi() {
fmt.Printf("Hi, I am %s, I study at %s. Call me on %s\n", s.name,
s.school, s.phone)
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
var h human
h = &mark
h.SayHi()
h = &sam
h.SayHi()
}