介绍 Golang
中的面向对象思想实践,主要讨论以下四个部分:
- 类和对象
- 封装
- 继承
- 多态
目录 Table of Contents
简介
Golang
的起源受诸多早期编程语言的影响。类C
让Golang
本质上更倾向于是一门面向过程的语言,同时Golang
也借鉴了Alef
来设计Golang
的函数式编程特性,融合CSP
中使用管道进行通信和控制同步的思想则很好地体现了如何面向消息编程。虽然
Golang
不是一门传统的面向对象语言,但是Golang
的设计却深受面向对象思想的影响。我们可以通过一种Golang
的方式来实现面向对象的重要特性,这也是接下来将要讨论的重点。
PS:本文 just 一点自己的见解,学识有限难免有误,也希望可以抛砖引玉,欢迎大家的勘误和讨论╰( ̄ω ̄o)
类和对象
众所周知🤫,类和对象是面向对象编程的灵魂(?类定义了一件事物的抽象特点,包含了数据的形式和对数据的操作;对象是类的实例,可以通过构造函数和析构函数来进行对生成和销毁的特殊处理。
C++
的类和对象
1 | class Person { |
Golang
的“类和对象”
1 | type Person struct { |
区别联系
耦合程度
C++
的类是面向class
而言的,数据成员和数据方法都必须在class
内修改,可见耦合程度较高。Golang
的“类”是面向type
而言的,数据成员在struct
内修改,数据方法则是可以在任意处增删(recv *receiver_type)
对应的方法,可见耦合程度较低。【PS:这里
type
的外延比class
要广,type
除包括自定义类型外还支持内置类型的别名】
this
指针
C++
对象的this
指针常常是隐式的,每一个数据方法实际上都隐式传入了一个指向该对象的this
指针:1
2
3void setName([Person* this], string name) {
this->name = name;
}Golang
“对象”的this
指针必须是显式的,不难看出this
指针是连接Golang
中类型和方法的关键桥梁:1
2
3func (this *Person) setName(string name) {
this.name = name
}
构造与析构
C++
的构造函数和析构函数是比较容易理解的,构造函数在对象创建时被自动调用,析构函数在对象销毁时被自动调用。由于C++
无垃圾回收机制,对象的生命周期和作用域紧密相关。Golang
严格上来说没有构造函数和析构函数的说法,可以通过用来专门做初始化的函数来模拟构造函数,而defer
和finalizer
有类似析构的意味,但本质还是很不同的。由于Golang
有垃圾回收机制,对象的生命周期取决于何时被GC
进程回收,一般而言当变量不再被引用就会被垃圾回收掉。
封装
封装 aka 信息隐藏,其实包含了两层意思:一是调用方无须关心实现细节,二是调用方无法更改实现细节。封装在编程语言中一般体现在访问权限中。
Java
的封装
- 以经典的 OO 语言
Java
为例,由于Java
同时存在类和包的概念,其访问权限需要考虑到两个维度,相对而言比较复杂。其中,Java
的访问权限通过关键字定义:public
:公共可见,所有类可见protected
:继承可见,必须为继承关系,允许跨包[default]
:包内可见,不要求继承关系,仅限同包private
:私有可见,仅本类可见
Golang
的封装
- 而在
Golang
中没有所谓的类和对象概念(或者说可以用很Golang
的方式类似实现),但引入了包管理机制,Golang
中只有简化的两层访问权限。其中,Golang
的访问权限通过标识符大小写定义:标识符首字母大写
:包外可见,所有的包均可见标识符首字母小写
:包内可见,本包文件均可见
一些说明
Golang
中的标识符首字母大写
类似于Java
中的public
Golang
中的标识符首字母小写
类似于Java
中的[default]
继承
继承涉及三方面的内容:一是子类可以使用父类的属性和方法,避免重复编码;二是子类可以覆盖父类的属性和方法,是实现多态的必要条件之一;三是子类可以追加属于自己的属性和方法,完成子类的定制功能。
C++
的继承
1 | class Parent { |
Golang
的继承
1 | type Parent struct { |
一些说明
C++
的继承更像是链式继承,从父类到子类进行构造,从子类到父类进行访问Golang
的继承更像是组合继承,子类内嵌一个或多个父类- 从
Golang
的继承机制容易看出它支持多继承,一些编程语言(如Java
)仅支持单继承
多态
多态指同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。当我们讨论多态时,我们常常会讨论重载以及重写和动态绑定。
语言基础
接口
- 类型绑定方法集,接口定义方法集。如果类型绑定的方法集和接口定义的方法集重合,那么类型实现了接口。
- 类型内定义了有什么属性,接口内定义了有什么操作,两者产生关联的关键是方法是否都被实现。
- 接口是隐式实现的,不需要显式声明;接口是一种特殊的类型,它可以被赋值成实例的指针或引用。
- 基于以上事实,我们可以知道:
- 不同的接口可以完成不同的组合操作;
- 多个类型可以实现同个接口,一个类型可以实现多个接口;
- 不同的类型完成不同的组合操作,看起来却是同一个接口,这就是多态!
断言
虽然我们提供对外提供了统一接口调用的方案,但是对内我们到底如何从接口出发辨别纷繁的类型呢?给定一个类型我们又该如何确定它是否实现了某个接口?
类型断言和类型选择:给定接口确定类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 类型断言
if _, ok := varI.(T); ok {
// ...
}
// 类型选择
switch t := varI.(type) {
case T:
// ...
case nil:
// ...
default:
// ...
}接口断言:给定类型确定接口
1
2
3
4// 接口断言
if _, ok := varT.(I); ok {
// ...
}
Golang
的多态
重载
重载是指根据不同的方法签名调用不同的函数实现。
Golang
的设计思想中是不允许任何形式的重载的,这是为了强化显式化的风格。【PS:不同类型的接收器绑定的同名方法,严格来说不算重载】
尽管
Golang
本身不提供重载的机制,我们还是可以借助接口来实现类似的功能。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22func Speak(persons ...interface{}) {
for _, person := range persons {
switch t := person.(type) {
case Chinese:
// Speak Chinese
case American:
// Speak English
case nil:
// Error handler
default:
// Default handler
}
}
}
func main() {
chinese := Chinese{}
american := american{}
Speak(chinese) // Speak Chinese
Speak(american) // Speak English
Speak(chinese, american) // chinese Speak Chinese, american Speak English
}
重写和动态绑定
重写和动态绑定是为了允许将子类类型的指针赋值给父类类型的指针,在运行时可以通过指向父类的指针来调用实现子类中的方法。
既然
Golang
中不存在严格的类和对象,重写和动态绑定的理论其实并不太适合Golang
,我们只需要关心怎么将利用一个接口访问可以定位到具体的类型就可以了。Golang
实现了编译时静态接口判断(类型是否实现接口),运行时动态类型选择(到底是哪种类型)。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
32type Animal interface {
move()
// ...
}
type Bird struct {
// ...
}
func (b *Bird) move() {
// fly
}
type Pig struct {
// ...
}
func (p *Pig) move() {
// walk
}
func main() {
var animal Animal
bird := Bird{}
pig := Pig{}
animal = bird
animal.move() // fly
animal = pig
animal.move() // walk
}
参考链接
致谢
感谢王同学坚持不懈的“八点钟检查”以及一点都不嫌弃的“康康博客”,让我得以在快要写不下去的时候还坚持着做一些有意义的复读,XOXO。