3 结构体与面向对象
本文为极客时间《Go语言核心36讲》的学习笔记,梳理了相关的知识点。
1. 面向对象编程
我们看下面向对象的定义:
面向对象程序设计(Object Oriented Programming,OOP)是一种计算机编程架构。OOP的一条基本原则是计算机程序由单个能够起到子程序作用的单元或对象组合而成。OOP达到了软件工程的三个主要目标:重用性、灵活性和扩展性。OOP=对象+类+继承+多态+消息,其中核心概念是类和对象。
本质上讲,它是一种对于世界的认知,是一种思想,它认为世间万物都有具体的属性组成,把拥有相同属性的事务抽象为一个集体,就是这一类事务的对象。举个例子:一只狗,它应该有四条腿,它有鼻子,它的叫声是“汪汪汪”。
我们看一个经典面试题:
提示
游戏规则和程序
1.有两个房间,一间房里有三盏灯,另一间房有控制着三盏灯的三个开关,这两个房间是 分割开的,从一间里不能看到另一间的情况。
2.现在要求受训者分别进这两房间一次,然后判断出这三盏灯分别是由哪个开关控制的。
3.有什么办法呢?
面向对象的集大成者,就是Java。在Java语言中有非常多的关键字来描述对象的一些属性和特点。比如:
- class 在Java里,没有类就没有对象(在其他语言里不一定哦)
- new 实例化一个类得到一个对象(其他语言不一定要new这个关键字哦)
- extends 继承一个类(最新的语言例如Go和Rust都没有继承哦,但它们也有OOP特性)
- this 获取当前方法调用的接收者
- public共有属性
- private私有属性
- protected受到保护的属性
这是符合过去十年人们对于程序,软件的基本认识的,并且确实有利于大型项目的开发和迭代,大家也都这么开发的。但是,语法限定的这么死板是不利于后续优化的,会浪费很多时间去理解代码,不利于开发一些小而专的场景。
1.1. 面向对象的特点
1.1.1. 封装
封装是指将一个计算机系统中的数据以及与这个数据相关的一切操作语言(即描述每一个对象的属性以及其行为的程序代码)组装到一起,一并封装在一个有机的实体中,把它们封装在一个“模块”中,也就是一个类中,为软件结构的相关部件所具有的模块性提供良好的基础。
一句话总结,就是利用分治法的思想,将大项目拆成小模块,先完成部分再组装整体。这非常符合“高内聚,低耦合”的设计思想。
1.1.2.继承
继承性是面向对象技术中的另外一个重要特点,其主要指的是两种或者两种以上的类之间的联系与区别。继承,顾名思义,是后者延续前者的某些方面的特点,而在面向对象技术则是指一个对象针对于另一个对象的某些独有的特点、能力进行复制或者延续。
一句话总结:既保留他人整体特征,又自己的特点。
1.1.3. 多态
从宏观的角度来讲,多态性是指在面向对象技术中,当不同的多个对象同时接收到同一个完全相同的消息之后,所表现出来的动作是各不相同的,具有多种形态;从微观的角度来讲,多态性是指在一组对象的一个类中,面向对象技术可以使用相同的调用方式来对相同的函数名进行调用,即便这若干个具有相同函数名的函数所表示的函数是不同的。
举个例子:不小心踩了一只猫,猫发出了一些反应。有些猫跑了,有些猫叫个不停,有些猫直接反手挠人。
1.2. 面向对象的核心理念
重要
- Program to an ‘interface’, not an ‘implementation’.
- Favor ‘object composition’ over ‘class inheritance’.
1.3. 特殊的一类:鸭子类型
鸭子类型是很多面向对象(OOP)语言中的常见做法。它的名字来源于所谓的“鸭子测试”:
当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。
这是一种逆向思维,当我们不再关注一个对象该有哪些熟悉、哪些方法,而是关注一个已经有一些方法的实体是个什么身份时,世界就开始变得不一样了。
归根到底,面向对象是一种思想,不是具体的方法。思想的落地可以有多种多样的实践,学过马哲的同学一定会有所感悟。过去Java的体系确实解决了很多很多的问题,但不能代表他就是面向对象的标准和模范。不能有这种固化思维。
面向对象编程带来了非常多的优点,比如高效,便于维护和扩展,同时它也有自己的局限。“如果你手上只有锤子,那么你看什么都是钉子”。面向对象也有不那么适用的场景,既无法替代面向过程,还会平白无故提高系统的设计难度和复杂度。
2. GO中的面向对象
GO语言反复强调自己没有对象(class)的概念,官方文档中也没有明确写过。这不代表他不能面向对象,更不能代表他只能面向接口或者过程。GO的面向对象,不如说GO的结构体用法更切合。
2.1. 不需要对象,只需要类型
//一个结构体类型
type AnimalCategory struct {
kingdom string // 界。
phylum string // 门。
class string // 纲。
order string // 目。
family string // 科。
genus string // 属。
species string // 种。
}
//类型方法
func (ac AnimalCategory) String() string {
return fmt.Sprintf("%s%s%s%s%s%s%s",
ac.kingdom, ac.phylum, ac.class, ac.order,
ac.family, ac.genus, ac.species)
}
提示
引申 (ac AnimalCategory)(ac *AnimalCategory)有什么区别
2.2. 嵌入取代继承
重要
再次注意!再次注意!再次注意!
嵌入字段是实现类型间组合的一种方式,这与继承没有半点儿关系。Go 语言虽然支持面向对象编程,但是根本就没有“继承”这个概念。
结构体嵌入的典型案例:
//一个新的结构体
type Animal struct {
scientificName string // 学名。
AnimalCategory // 动物基本分类。嵌入字段
String //引申,这也是嵌入字段 并且会引发屏蔽
}
func (a Animal) Category() string {
return a.AnimalCategory.String()
}
// 实现一个有嵌入字段的结构体
animal := Animal{
scientificName: "American Shorthair",
AnimalCategory: category,
}
fmt.Printf("The animal: %s\n", animal)
animal.String()
Go 语言规范规定,如果一个字段的声明中只有字段的类型名而没有字段的名称,那么它就是一个嵌入字段,也可以被称为匿名字段。我们可以通过此类型变量的名称后跟“.”,再后跟嵌入字段类型的方式引用到该字段。也就是说,嵌入字段的类型既是类型也是名称。
2.3. 屏蔽与包装代替重写
//外层的String 会替换掉嵌入字段的String
func (a Animal) String() string {
return fmt.Sprintf("%s (category: %s)",
a.scientificName, a.AnimalCategory)
}
我们无法通过正常的操作访问到 AnimalCategory 中的String方法,它已经被Animal给屏蔽了。我们仍然可以直接显示调用:
a.AnimalCategory.String()
提示
引申:两个问题
1.多级嵌套的时候,最外层的会屏蔽内层的。
2.同级嵌套多个结构体时,如果拥有同名字段或者方法,会报出编译错误。换句话说,拥有相似方法属性的结构体只能嵌套一个。
2.4.不需要声明,只需要大小写
这个很简单:大写代表Public,小写代表Private。
3. 问题引申
3.1. 嵌入的方式比继承有哪些优势?
再次强调:Go 语言中根本没有继承的概念,它所做的是通过嵌入字段的方式实现了类型之间的组合。
这样做的具体原因和理念请见 Go 语言官网的 FAQ 中的Why is there no type inheritance?。
- 面向对象的继承其实是通过大量的代码来换取扩展性,这种扩展性是侵入式的。嵌入的话,不需要显示的声明任何东西,大大降低了代码量,也不会破坏封装和多态,而且也没有增加耦合度。
- 它提供了“包装”和“屏蔽”的方法来调整和优化嵌入的结构体。简单有效,好理解。
- 简而言之,在实现基础的“继承”的功能上,降低了使用复杂度和管理成本。
3.2. 为什么都推荐使用指针方法而不是值方法?
- 还是要回到“GO语言没有指针传递”这个概念上。值方法传递的是值的副本,修改某个属性时原值不会改变;指针方法 传递的是值的地址的副本,如果修改,原值也会被修改。可以看下面的代码:
type Cat struct {
Name string
Age int64
string
}
func (c Cat) SetName(s string) Cat {
c.Name = s
return c
}
//func (c *Cat) SetName(s string) Cat {
// c.Name = s
// return c
//}
func Test(){
C := Cat{
Name: "abc1",
Age: 12,
string:"sss",
}
A := C.SetName("123")
fmt.Printf("C:%#v",C)
fmt.Printf("A:%#v",A)
}
- 值类型只能看到所有的值方法,指针类型可以看到所有方法。
- 对于接口而言,如果指针类型实现了某个接口,但是它的值类型却不一定是该接口的实现类型。这一点很容易出错。
可以看下代码:
func Test2() {
C := Cat{
Name: "abc1",
Age: 12,
string:"sss",
}
Cat1 := C
Cat1P := &C
fmt.Printf("Cat:%+v\n",Cat1)
Cat1.Age = 13
fmt.Printf("Cat:%+v\n",Cat1)
fmt.Printf("CatP:%+v\n",Cat1P)
Cat1P.Age = 14
fmt.Printf("Cat:%+v\n",Cat1)
fmt.Printf("CatP:%+v\n",Cat1P)
Cat1P.String1()
}
3.3. 能不能在结构体里嵌入另一个结构体的指针?
我们可以在结构体中嵌入某个类型的指针类型, 它和普通指针类似,默认初始化为nil。因此在用之前需要人为初始化,否则可能引起错误。这一点在业务开发的时候经常遇到。
3.4. 如何理解空结构体struct{}?
空结构体不占用内存空间,但是具有结构体的一切属性,如可以拥有方法,可以写入channel。所以当我们需要使用结构体而又不需要具体属性时可以使用它。
3.5. 重要:结构体能否比较?
答案是:可以,但不能简单的直接使用 ==,应该是用反射中的 reflect.DeepEqual。
分以下几种情况,直接看代码:
type struct1 struct {
Name string
Age int
}
type struct2 struct {
Name string
Age []int
}
type struct3 struct {
Name string
Age *int
}
type struct4 struct {
Name string
Age int
}
func testStructs1() {
//第一种情况 两个相同的,可比较字段的结构体
a := struct1{"关注香香编程喵喵喵", 1}
b := struct1{"关注香香编程喵喵喵", 1}
fmt.Printf("第一次比较结果:%t\n", a == b)
b = struct1{"关注香香编程谢谢喵喵喵", 1}
fmt.Printf("第二次比较结果:%t\n", a == b)
}
func testStructs2() {
//第二种情况 两个相同的,不可比较字段的结构体
//a := struct2{"关注香香编程喵喵喵", []int{1, 2, 3}}
//b := struct2{"关注香香编程喵喵喵", []int{1, 2, 3}}
//无法通过编译
//fmt.Printf("第一次比较结果:%v\n", a = b)
}
func testStructs3() {
//第三种情况 两个相同的,带有指针字段的结构体
a := struct3{"关注香香编程喵喵喵", new(int)}
b := struct3{"关注香香编程喵喵喵", new(int)}
fmt.Printf("第一次比较结果:%t\n", a == b) //false
i := new(int)
a = struct3{"关注香香编程喵喵喵", i}
b = struct3{"关注香香编程喵喵喵", i}
fmt.Printf("第一次比较结果:%t\n", a == b) //true
}
func testStructs4() {
//第四种情况 两个拥有相同的,可比较字段的结构体 无法通过编译
//a := struct1{"关注香香编程喵喵喵", 1}
//b := struct4{"关注香香编程喵喵喵", 1}
//fmt.Printf("第一次比较结果:%t\n", a == b)
}
func testStructs5() {
//使用反射来判断结构体是否相同
a := struct1{"关注香香编程喵喵喵", 1}
b := struct1{"关注香香编程喵喵喵", 1}
fmt.Printf("第一次比较结果:%t\n", reflect.DeepEqual(a, b)) //true
c := struct2{"关注香香编程喵喵喵", []int{1, 2, 3}}
d := struct2{"关注香香编程喵喵喵", []int{1, 2, 3}}
fmt.Printf("第二次比较结果:%t\n", reflect.DeepEqual(c, d)) //true
z := struct3{"关注香香编程喵喵喵", new(int)}
y := struct3{"关注香香编程喵喵喵", new(int)}
fmt.Printf("第三次比较结果:%t\n", z == y) //true
e := struct1{"关注香香编程喵喵喵", 1}
g := struct4{"关注香香编程喵喵喵", 1}
fmt.Printf("第四次比较结果:%t\n", reflect.DeepEqual(e, g))//false
}
结论:
- 两个相同的,拥有**不可比较字段(引用类型)**的结构体和两个相同字段不同的结构体不能用 == 判等,无法通过编译。
- 两个相同的,可比较字段的结构体可以使用 == 判等,会根据每个字段的结果进行判定。
- 使用reflect.DeepEqual可以判定所有情况,并且对每个字段进行深度判等,最后返回结果。
相关信息
引申:可以了解一下go语言中反射的用法。
6. 引申知识
- 如何理解面向对象中的S.O.L.I.D(单一功能,开闭原则,里氏替换,接口隔离,依赖反转)?
- 如何理解IoC/DIP?