# 面向对象编程
# 面向对象编程
在讲解 Go 语言面向对象内容之前,需要说明下 Go 语言的代码是以包结构来组织的,且如果标示符(变量名,函数名,自定义类型等)如果以大写字母开头那么这些标示符是可以导出的,可以在任何导入了定义该标示符的包的包中直接使用。Go 语言中的面向对象和 C++,Java 中的面向对象不同,因为 Go 语言不支持继承,Go 语言只支持组合。
# 自定义类型
Go 语言的中结构体 struct
与 C++、JAVA 中的类 class
相似,但 Go 放弃了传统面向对象的诸多特性,只保留了组合。
- type typeName typeSpecification
其中,typeName
可以是一个包或者函数内唯一合法的 Go 标示符。typeSpecification
可以是任何内置的类型,一个接口或者是一个结构体。所谓结构体,它的字段是由其他类型或者接口组成。例如我们通过结构体定义了一下类型:
type ColorPoint struct {
color.Color // 匿名字段(嵌入)
x, y int // 具名字段(聚合)
}
2
3
4
以上代码我们通过结构体自定义了类型 ColorPoint
,结构体中 color.Color
字段是 Color 包的类型 color,这个字段没有名字,所以被称为匿名的,也是嵌入字段。字段 x
和 y
是有变量名的,所以被称为具名字段。假如我们创建了类型 ColorPoint
的一个值 point
(通过语法:point := ColorPoint{}
创建),那么这些字段可以通过 point.Color
、point.x
、point.y
访问。其他面向对象语言中的"类 (class
)"、"对象 (object
)"、"实例 (instance
)"在 Go 语言中我们完全避开使用。相反的我们使用"类型 (type
)"和其对应的"值",其中自定义类型的值可以包含方法。
定义了结构体后如何创建并初始化一个对象实例呢?Go 语言支持以下几种方法进行实现:
//先定义一个结构体Man
type Man struct{
name string
age int
}
//对象创建与初始化
man := new(Man)
man := &Man{}
man := &Man{"Tom", 18}
man := &Man{name: "Tom", age: 18}
2
3
4
5
6
7
8
9
10
为了更加方便的创建对象,我们一般会使用一个全局函数来完成对象的创建,这和传统的“构造函数”类似。
func NewMan(name string, age int) *Man {
return &Man{name, age}
}
2
3
# 方法
方法是作用在自定义类型上的一类特殊函数,通常自定义类型的值会被传递给该函数,该值可能是以指针或者复制值的形式传递。定义方法和定义函数几乎相同,只是需要在 func
关键字和方法名之间必须写上接接受者。例如我们给类型 Count
定义了以下方法:
type Count int
func (count *Count) Increment() { *count++ } // 接受者是一个 `Count` 类型的指针
func (count *Count) Decrement() { *count-- }
func (count Count) IsZero() bool { return count == 0 }
2
3
4
5
以上代码中,我们在内置类型 int
的基础上定义了自定义类型 Count
,然后给该类型添加了 Increment()
、Decrement()
和 IsZero()
方法,其中前两者的接受者为 Count
类型的指针,后一个方法接收 Count
类型的值。
类型的方法集是指可以被该类型的值调用的所有方法的集合。
一个指向自定义类型的值的指针,它的方法集由该类型定义的所有方法组成,无论这些方法接受的是一个值还是一个指针。如果在指针上调用一个接受值的方法,Go 语言会聪明地将该指针解引用。
一个自定义类型值的方法集合则由该类型定义的接收者为值类型的方法组成,但是不包括那些接收者类型为指针的方法。
其实这些限制 Go 语言帮我们解决的非常好,结果就是我们可以在值类型上调用接收者为指针的方法。假如我们只有一个值,仍然可以调用一个接收者为指针类型的方法,这是因为 Go 语言会自动获取值的地址传递给该方法,前提是该值是可寻址的。
在以上定义的类型 Count
中,*Count
方法集是 Increment()
, Decrement()
和 IsZero()
,Count
的值的方法集是 IsZero()
。但是因为 Count
类型的是可寻址的,所以我们可以使用 Count
的值调用全部的方法。
另外如果结构体的字段也有方法,我们也可以直接通过结构体访问字段中的方法。下面让我们练习下,创建源文件 struct_t.go
,输入以下代码:
package main
import "fmt"
type Count int // 创建自定义类型 Count
func (count *Count) Increment() { *count++ } // Count类型的方法
func (count *Count) Decrement() { *count-- }
func (count Count) IsZero() bool { return count == 0 }
type Part struct { // 基于结构体创建自定义类型 Part
stat string
Count // 匿名字段
}
func (part Part) IsZero() bool { // 覆盖了匿名字段Count的IsZero()方法
return part.Count.IsZero() && part.stat == "" // 调用了匿名字段的方法
}
func (part Part) String() string { // 定义String()方法,自定义了格式化指令%v的输出
return fmt.Sprintf("<<%s, %d>>", part.stat, part.Count)
}
func main() {
var i Count = -1
fmt.Printf("Start \"Count\" test:\nOrigin value of count: %d\n", i)
i.Increment()
fmt.Printf("Value of count after increment: %d\n", i)
fmt.Printf("Count is zero t/f? : %t\n\n", i.IsZero())
fmt.Println("Start: \"Part\" test:")
part := Part{"232", 0}
fmt.Printf("Part: %v\n", part)
fmt.Printf("Part is zero t/f? : %t\n", part.IsZero())
fmt.Printf("Count in Part is zero t/f?: %t\n", part.Count.IsZero()) // 尽管覆盖了匿名字段的方法,单还是可以访问
}
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
以上代码中,我们创建了 Count
类型,然后在其基础上又创建了结构体类型 Part
。我们为 Count
类型定义了 3 个方法,并在 Part
类型中创建了方法 IsZero()
覆盖了其匿名字段 Count
中 IsZero()
方法。但是我们还是可以二次访问到匿名字段中被覆盖的方法。执行代码,输出如下:
$ go run struct_t.go
Start "Count" test:
Origin value of count: -1
Value of count after increment: 0
Count is zero t/f? : true
Start: "Part" test:
Part: <<232, 0>>
Part is zero t/f? : false
Count in Part is zero t/f?: true
2
3
4
5
6
7
8
9
10
11
12
# 组合
Go 语言虽然抛弃了继承,但是却提供了一个更加方便的组合特性。相对于继承的编译期确定实现,组合的运行态指定实现,更加灵活。下面通过一段代码来了解组合的基本属性以及它与继承的不同之处。
先定义一个结构体 Base, 并为它添加两个方法 Foo()
和 Bar()
:
type Base struct{
Name string
}
func (b *Base) Foo() {...}
func (b *Base) Bar() {...}
type Seed struct {
Base
...
}
func (s *Seed) Foo() {
s.Base.Foo()
s.Bar()
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
上面代码先定义了一个 Base 类,然后定义了一个 Seed 类。Seed 类“继承”了 Base 类的所有成员属性和方法并重写了 Foo()
方法。同时在重写 Foo()
方法时调用了 Base 类的 Foo()
方法和 Bar()
方法。需要注意的是,若此时 Seed 的对象通过 s.Foo()
调用 Foo()
方法时,实际调用的是 Seed 重写过后的 Foo()
方法,而不是基类 Base 的 Foo()
方法,若想调用 Base 类的 Foo()
方法则要使用 s.Base.Foo
,而调用没有重写的 Bar()
方法时,使用 s.Bar()
和s.Base.Bar()
效果是一样的。
# 接口
在 Go 中,接口是一组方法签名。当一个类型为接口中的所有方法提供定义时,它被称为实现该接口。它与 oop 非常相似。接口指定类型应具有的方法,类型决定如何实现这些方法。
# 接口基础
之所以说 Go 语言的面向对象很灵活,很大一部分原因是由于接口的存在。接口是一个自定义类型,它声明了一个或者多个方法签名,任何实现了这些方法的类型都实现这个接口。infterface{}
类型是声明了空方法集的接口类型。任何一个值都满足 interface{}
类型,也就是说如果一个函数或者方法接收 interface{}
类型的参数,那么任意类型的参数都可以传递给该函数。接口是完全抽象的,不能实例化。接口能存储任何实现了该接口的类型。直接看例子吧,创建源文件 interface_t.go
,输入以下代码:
package main
import "fmt"
type Human struct { // 结构体
name string
age int
phone string
}
//Human实现SayHi方法
func (h Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Human实现Sing方法
func (h Human) Sing(lyrics string) {
fmt.Println("La la la la...", lyrics)
}
type Student struct {
Human //匿名字段
school string
loan float32
}
type Employee struct {
Human //匿名字段
company string
money float32
}
// Employee重载Human的SayHi方法
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)
}
// Interface Men被Human,Student和Employee实现
// 因为这三个类型都实现了这两个方法
type Men interface {
SayHi()
Sing(lyrics string)
}
func main() {
mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
Tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000}
//定义Men类型的变量i
var i Men
//i能存储Student
i = mike
fmt.Println("This is Mike, a Student:")
i.SayHi()
i.Sing("November rain")
//i也能存储Employee
i = Tom
fmt.Println("This is Tom, an Employee:")
i.SayHi()
i.Sing("Born to be wild")
//定义了slice Men
fmt.Println("Let's use a slice of Men and see what happens")
x := make([]Men, 3)
//这三个都是不同类型的元素,但是他们实现了interface同一个接口
x[0], x[1], x[2] = paul, sam, mike
for _, value := range x {
value.SayHi()
}
}
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
以上代码中,接口类型声明的变量能存储任何实现了该接口的类型的值。运行代码,输出如下:
go run interface_t.go
This is Mike, a Student:
Hi, I am Mike you can call me on 222-222-XXX
La la la la... November rain
This is Tom, an Employee:
Hi, I am Tom, I work at Things Ltd.. Call me on 222-444-XXX
La la la la... Born to be wild
Let's use a slice of Men and see what happens
Hi, I am Paul you can call me on 111-222-XXX
Hi, I am Sam, I work at Golang Inc.. Call me on 444-222-XXX
Hi, I am Mike you can call me on 222-222-XXX
2
3
4
5
6
7
8
9
10
11
// _接口_ 是方法特征的命名集合。
package main
import "fmt"
import "math"
// 这里是一个几何体的基本接口。
type geometry interface {
area() float64
perim() float64
}
// 在我们的例子中,我们将让 `square` 和 `circle` 实现
// 这个接口
type square struct {
width, height float64
}
type circle struct {
radius float64
}
// 要在 Go 中实现一个接口,我们只需要实现接口中的所有
// 方法。这里我们让 `square` 实现了 `geometry` 接口。
func (s square) area() float64 {
return s.width * s.height
}
func (s square) perim() float64 {
return 2*s.width + 2*s.height
}
// `circle` 的实现。
func (c circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
return 2 * math.Pi * c.radius
}
// 如果一个变量的是接口类型,那么我们可以调用这个被命名的
// 接口中的方法。这里有一个一通用的 `measure` 函数,利用这个
// 特性,它可以用在任何 `geometry` 上。
func measure(g geometry) {
fmt.Println(g)
fmt.Println(g.area())
fmt.Println(g.perim())
}
func main() {
s := square{width: 3, height: 4}
c := circle{radius: 5}
// 结构体类型 `circle` 和 `square` 都实现了 `geometry`
// 接口,所以我们可以使用它们的实例作为 `measure` 的参数。
measure(s)
measure(c)
}
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
47
48
49
50
51
52
53
54
55
56
57
# 接口变量值的类型
我们知道接口类型声明的变量里能存储任何实现了该接口的类型的值。有的时候我们需要知道这个变量里的值的类型,那么需要怎么做呢?可以使用类型断言,或者是 switch
类型判断分支。以下的例子 interface_t1.go
我们使用了 switch
类型判断分支。
package main
import (
"fmt"
"strconv"
)
type Element interface{}
type List []Element
type Person struct {
name string
age int
}
// 实现了fmt.Stringer接口
func (p Person) String() string {
return "(name: " + p.name + " - age: " + strconv.Itoa(p.age) + " years)"
}
func main() {
list := make(List, 3)
list[0] = 1 //an int
list[1] = "Hello" //a string
list[2] = Person{"Dennis", 70}
for index, element := range list {
switch value := element.(type) { // switch类型判断开关
case int:
fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
case string:
fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
case Person:
fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
default:
fmt.Println("list[%d] is of a different type", index)
}
}
}
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
运行结果:
$ go run interface_t1.go
list[0] is an int and its value is 1
list[1] is a string and its value is Hello
list[2] is a Person and its value is (name: Dennis - age: 70 years)
2
3
4
# 嵌入 interface
在前面的课程中我们已经知道在结构体中可以嵌入匿名字段,其实在接口里也可以再嵌入接口。如果一个 interface1
作为 interface2
的一个嵌入字段,那么 interface2
隐式的包含了 interface1
里的方法。如下例子中,Interface2
包含了 Interface1
的所有方法。
type Interface1 interface {
Send()
Receive()
}
type Interface2 interface {
Interface1
Close()
}
2
3
4
5
6
7
8
9
# 注意
了解其他面向对象编程语言的同学可能会发现,Go 语言的面向对象内容很少,那这是不是就意味着 Go 的面向对象不够强大呢?其实并不是,Go 语言的面向对象是非常成熟且实用的!只不过设计者在设计这门语言之初时就尊崇了“大道至简”的设计理念,摈弃了传统面向对象的很多繁复而不实用的语法,这不仅给学习这门语言的人降低了难度,同时在项目开发中也更加的方便、简洁。这一点大家在以后的项目实验课程的学习中就会深有体会。