共计 11337 个字符,预计需要花费 29 分钟才能阅读完成。
万物皆对象。学过 Java 编程的都知道 Java 是一门面向对象的语言,它拥有封装、继承和多态的特性。那可不可以说,拥有封装、继承和多态这一特性的语言就是面向对象的语言呢?
仔细想来,也确实是这样的,因为封装、继承和多态这三个特征,并不是 Java 语言的特征,而是面向对象的三大特征。
总结来看,所有包含封装、继承和多态者三大特征的语言都可以说是面向对象的语言。
那么 Go 语言是否是一门面向对象的语言呢?下面我们通过举例的方式针对封装、继承和多态这面向对象的三大特征分别进行解释。
<!– more –>
封装
Go 中有 struct
结构体,通过结构体能够实现现实世界中对象的封装。如将学生封装成对象,除了学生的基础信息外,还需要一些学生的基础行为。
定义结构体的方式之前在基础结构中进行了简单的解释,并没有针对结构体的方法进行说明。这里先说明一下定义结构体的方法。
func(alias type) func_name(parameter1 type, parameter2 type2)(ret1 type3, ret2 type4){...}
定义结构体的方法的语法与函数的语法类似,区别于普通函数,方法的定义在 func 后有一个括号(alias type)
,指定方法的附属结构体,以方便通过结构体来进行方法的使用。
看到这里不免有些 Java 的同学觉得不太好接受,毕竟在 Java 中,对象的方法都是写在 class 中的,在 Go 中方法都是写在结构体外的。
所以可以总结一句,Go 中的函数分为两类,一种是有附属于结构体的方法,一种是普通函数。附属于结构体的函数,在使用的过程中,需要结合结构体来使用,必须像 Java 那样先声明对象,然后结合对象才能使用。
普通函数仅有是否可被外部包访问的要求,不需要先声明结构体,结合结构体来使用,开盖即食哈。
方法的结构体在指定时,alias 别名可以随意设置,但是所属类型不能,(此处有坑)下面看一个例子
package main
import "fmt"
type Student struct {
Name string
Learned []string}
func (s Student) learnEnglish() {s.Learned = append(s.Learned, "i'm fine, thank you")
}
func (s *Student) learnMath() {s.Learned = append(s.Learned, "1 + 1 = 2")
}
func (s *Student) whoAmI() {fmt.Println("your name is :", s.Name)
}
func (s Student) whoAmII() {fmt.Println("your name is :", s.Name)
}
func main() {s := Student{Name: "jack"}
s.whoAmI()
s.whoAmII()
s.learnEnglish() // 学英语
s.learnMath() // 学数学
fmt.Println(s.Name, "学过:")
for _, learned := range s.Learned {fmt.Printf("\t %s \n", learned)
}
}
/*
运行结果:your name is : jack
your name is : jack
jack 学过:1 + 1 = 2
---
没有学过英语???*/
append 为 Go 自带函数,向数组和 slice 中添加元素
这里有四个方法,两个打印名字的方法和两个学习的方法,区别点在于方法的所属类型一个是指针类型,另一个是非指针类型。
执行结果显示,打印名字的方法都正确输出了名字,但是学习英语和数学后,却显示只学过数学,没学过英语,这岂不是让我等学生的老师很头疼?
这是为什么呢?
这样就牵涉到了 Go 中的值拷贝和地址拷贝了。咱们先简单看一下值拷贝和地址拷贝。
值拷贝 & 地址拷贝
在 Java 中同样有值拷贝和地址拷贝的说法,学过 Java 的自然对 Go 的这点特性会比较容易理解。
在 Go 中虽然是都是值拷贝,但是在拷贝的过程中,拷贝的可能是变量的地址,或者是变量的值,不同的内容得到的结果当然是不一样的。
在函数定义参数时,如果参数类型是指针类型,则函数内修改了参数的内容,函数外同样会察觉到改参数的变化,这就是因为在调用该函数的时候,传递给该函数的值是一个地址,发生的是地址的拷贝,而这个地址指向的参数与函数外的变量是同一个,函数内修改了该地址的内容,相应的,函数外也会发生变化。这个还是通过例子比较好理解。
咱们继续让 Jack 学习不同的知识,在上一个代码中继续添加两个函数。
func learnChinese(s *Student) {s.Learned = append(s.Learned, "锄禾日当午,汗滴禾下土")
}
func learnPingPang(s Student) {s.Learned = append(s.Learned, "ping pang")
}
func main() {s := Student{Name: "jack"} // 初始化姓名
s.whoAmI()
s.whoAmII()
learnPingPang(s) // 学习乒乓球
learnChinese(&s) // 学习中文
s.learnEnglish() // 学英语
s.learnMath() // 学数学
fmt.Println(s.Name, "学过:")
for _, learned := range s.Learned {fmt.Printf("\t %s \n", learned)
}
}
/*
运行结果:your name is : jack
your name is : jack
jack 学过:锄禾日当午,汗滴禾下土
1 + 1 = 2
---
没有学过英语???没有学过乒乓???*/
例子中添加了两个函数 learnChinese(s *Student)和 learnPingPang(s Student)两个函数,分别接收带指针和不带指针的参数,下面执行的结果却显示 Jack 只学习了中文没学习乒乓,这也说明了 learnPingPang(s Student)这个函数接收的参数发生了值拷贝,传递给该函数的值就是 Student 对象,而且是生成了一个新的 Student 对象,所以函数内发生的变化在函数外并不能感知。这个在平时的开发中还是需要特别的注意的。
看到这里应该就能理解为什么 Jack 没有学过英语了。(s Student) learnEnglish()这个函数中定义的所属类型是非指针类型,在使用时发生值拷贝,会生成新的 Student 对象,从而函数内部发生的变化并不会在函数外部有所感知。原来学英语的并不是 Jack 本人啊。
了解了如何定义方法之后就可以对封装有一个比较清晰的认识了,Go 中的结构体定义对象和结构体方法定义对象的行为,可以满足封装要求了,也算是符合了封装的条件。下面来一个完整的封装例子
package main
import "fmt"
type Class struct {Name string}
type School struct {Name string}
type Student struct {
Name string
Age int
Height float64
Weight float64
SchoolInfo School
ClassInfo Class
Learned []string}
func (s Student) learnEnglish() {
// append 为 Go 自带函数,向数组和 slice 中添加元素
s.Learned = append(s.Learned, "i'm fine, thank you")
}
func (s *Student) learnMath() {s.Learned = append(s.Learned, "1 + 1 = 2")
}
func (s *Student) whoAmI() {fmt.Println("your name is :", s.Name, "and your className is :", s.ClassInfo.Name, "and your schoolName is :", s.SchoolInfo.Name)
}
func (s Student) whoAmII() {fmt.Println("your name is :", s.Name, "and your className is :", s.ClassInfo.Name, "and your schoolName is :", s.SchoolInfo.Name)
}
func learnChinese(s *Student) {s.Learned = append(s.Learned, "锄禾日当午,汗滴禾下土")
}
func learnPingPang(s Student) {s.Learned = append(s.Learned, "ping pang")
}
func main() {
/*
定义对象时可以使用 key:value 的形式进行赋值,也可以使用 value 直接赋值,但是两中方式不能同时使用
使用 key:value 时,不需要注意顺序,可以直接赋值
使用 value 时,需要注意顺序,按照默认字段顺序进行赋值
️注意::如果最后一个字段与右大括号不在一行,需要在最后一个字段的赋值后加上逗号
*/
s := Student{
Age: 18,
Weight: 70,
Height: 180,
SchoolInfo: School{"北大附中"},
Name: "jack",
ClassInfo: Class{"高二·8 班"},
} // 初始化 student 对象
fmt.Println("学校:", s.SchoolInfo.Name)
fmt.Println("班级:", s.ClassInfo.Name)
fmt.Println("姓名:", s.Name)
fmt.Println("年龄:", s.Age, "岁")
fmt.Println("身高:", s.Height, "cm")
fmt.Println("体重:", s.Weight, "kg")
s.whoAmI()
s.whoAmII()
learnPingPang(s) // 学习乒乓球
learnChinese(&s) // 学习中文
s.learnEnglish() // 学英语
s.learnMath() // 学数学
fmt.Println(s.Name, "学过:")
for _, learned := range s.Learned {fmt.Printf("\t %s \n", learned)
}
}
/*
运行结果:学校:北大附中
班级:高二·8 班
姓名:jack
年龄:18 岁
身高:180 cm
体重:70 kg
your name is : jack and your className is : 高二·8 班 and your schoolName is : 北大附中
your name is : jack and your className is : 高二·8 班 and your schoolName is : 北大附中
jack 学过:锄禾日当午,汗滴禾下土
1 + 1 = 2
---
没有学过英语
没有学过乒乓
*/
这里的 Jack 既有班级信息又有学校信息,既能学中文又能学英文。也算是把学生这个对象封装好了。
继承
Java 中,继承是说父子类之间的关系,子类继承父类,子类就拥有父类的部分功能。这个继承通过 extend
关键字就可以实现。在 Go 中,没有这个关键字,但是也可以做到相同的效果。使用的方式就是结构体的嵌套。我们继续使用学生这个例子进行讲解,现在将学生中的部分信息抽出到 People 这个结构体中。
package main
import "fmt"
type Class struct {Name string}
type School struct {Name string}
type People struct {
Name string
Age int
Height float64
Weight float64
}
func (p *People) SayHey() {fmt.Println("爱老虎油")
}
func (p *People) Run() {fmt.Println(p.Name, "is running...")
}
func (p *People) Eat() {fmt.Println(p.Name, "is eating...")
}
func (p *People) Drink() {fmt.Println(p.Name, "is drinking...")
}
type Student struct {
People // 内嵌 people
Name string
SchoolInfo School
ClassInfo Class
Learned []string}
func (s *Student) SayHey() {fmt.Println("i love you")
}
func (s Student) learnEnglish() {
// append 为 Go 自带函数,向数组和 slice 中添加元素
s.Learned = append(s.Learned, "i'm fine, thank you")
}
func (s *Student) learnMath() {s.Learned = append(s.Learned, "1 + 1 = 2")
}
func (s *Student) whoAmI() {fmt.Println("your name is :", s.Name, "and your className is :", s.ClassInfo.Name, "and your schoolName is :", s.SchoolInfo.Name)
}
func (s Student) whoAmII() {fmt.Println("your name is :", s.Name, "and your className is :", s.ClassInfo.Name, "and your schoolName is :", s.SchoolInfo.Name)
}
func learnChinese(s *Student) {s.Learned = append(s.Learned, "锄禾日当午,汗滴禾下土")
}
func learnPingPang(s Student) {s.Learned = append(s.Learned, "ping pang")
}
func main() {
s := Student{
People: People{
Name: "jack", // 小名
Age: 18,
Weight: 70,
Height: 180,
},
Name: "jack·li", // 大名
SchoolInfo: School{"北大附中"},
ClassInfo: Class{"高二·8 班"},
} // 初始化 student 对象
fmt.Println("学校:", s.SchoolInfo.Name)
fmt.Println("班级:", s.ClassInfo.Name)
fmt.Println("姓名:", s.Name) // 打印时会打印大名
fmt.Println("年龄:", s.Age, "岁")
fmt.Println("身高:", s.Height, "cm")
fmt.Println("体重:", s.Weight, "kg")
s.whoAmI()
s.whoAmII()
learnPingPang(s) // 学习乒乓球
learnChinese(&s) // 学习中文
s.learnEnglish() // 学英语
s.learnMath() // 学数学
fmt.Println(s.Name, "学过:") // 打印时会打印大名
for _, learned := range s.Learned { // 打印学过的知识
fmt.Printf("\t %s \n", learned)
}
s.Eat() // 直接使用内嵌类型的方法
s.Drink() // 直接使用内嵌类型的方法
s.Run() // 直接使用内嵌类型的方法
s.SayHey() // 使用 Student 的 sayHey
fmt.Println("俺叫:", s.People.Name) // 使用内嵌 People 的 name 打印小名
s.People.SayHey() // 使用 内嵌 People 的 SayHey}
/*
运行结果:学校:北大附中
班级:高二·8 班
姓名:jack·li
年龄:18 岁
身高:180 cm
体重:70 kg
your name is : jack·li and your className is : 高二·8 班 and your schoolName is : 北大附中
your name is : jack·li and your className is : 高二·8 班 and your schoolName is : 北大附中
jack·li 学过:锄禾日当午,汗滴禾下土
1 + 1 = 2
jack is eating...
jack is drinking...
jack is running...
i love you
俺叫:jack
爱老虎油
*/
在这个例子中,Student 内嵌了 People,在定义 Student 对象时 People 结构体的字段单独定义在 People 对象中。但是在使用时,可以直接像 s.Eat()
,s.Run()
,s.Height
这样直接调用,也可以使用 s.People.SayHey()
和s.People.Name
这样间接的调用。这就是嵌套的使用方法。
使用嵌套结构体的方式定义对象之后,就可以直接使用内嵌类型的字段以及方法,但是在使用时遇到相同的字段(Student 的 Name 和 People 的 Name)则直接使用字段时,使用的就是结构体的字段,而不是内嵌类型的字段,或者遇到相同的方法 (Student 的 SayHey() 和 People 的 SayHey())则直接使用时,使用的就是结构体的方法,而不是内嵌类型的方法。如果要使用内嵌类型的字段或方法,可以在使用时指明内嵌结构体。这个有点像 Java 中的覆盖。所以有时在使用时需要注意要使用的是那个具体的字段,避免出错。
曲线救国也算是救国,Go 通过内嵌结构体的形式,变相的实现了面向对象的继承,但是感觉总是比 Java 中的继承要差些什么。或许差的是继承的那些条条框框吧。
多态
相同类型的对象表现出不一样的行为特征叫做多态。这个在 Go 中同样可以实现。通过 interface
就可以。
上节讲到 interface
是基础类型,这里咱们继续讲解 interface
作为接口的用法。
interface 作为接口时,可以定义一系列的函数供其他结构体实现,但是只能定义函数,不能定义字段等。它的语法如下
type name interface {func1([请求参数集]) [返回参数集]
}
Go 中的接口在实现时可没有 Java 中的 implement 关键字,在实现接口的时候只需要实现接口中定义的全部的方法就可以认为是实现了这个接口,所以说 Go 的接口实现是一种隐式的实现,并不是直观上的实现。这点也是类似 Java 中的接口的,但是接口实现的这种关系并不是那么严格,如果通过 ide 在开发的过程中,能看到很多定义的方法实现了自己不知道的接口,不过放心,这是一种正常的现象,只要在使用的过程中稍加注意即可。
让咱们继续优化上面的例子来理解 interface 接口,还是看下面的例子
package main
import "fmt"
type Class struct {Name string}
type School struct {Name string}
type Animal interface {Eat()
Drink()
Run()}
// 实现了 Animal 的三个方法,可认为 *People 实现了 Animal 接口
type People struct {
Name string
Age int
Height float64
Weight float64
}
func (p *People) SayHey() {fmt.Println("爱老虎油")
}
// 实现 Animal 接口的 Run 方法
func (p *People) Run() {fmt.Println(p.Name, "is running...")
}
// 实现 Animal 接口的 Eat 方法
func (p *People) Eat() {fmt.Println(p.Name, "is eating...")
}
// 实现 Animal 接口的 Drink 方法
func (p *People) Drink() {fmt.Println(p.Name, "is drinking...")
}
// 实现了 Animal 的三个方法,可认为 *Student 实现了 Animal 接口
type Student struct {
People // 内嵌 people
Name string
SchoolInfo School
ClassInfo Class
Learned []string}
// 实现 Animal 接口的 Run 方法
func (s *Student) Run() {fmt.Println(s.Name, "is running around campus")
}
// 实现 Animal 接口的 Eat 方法
func (s *Student) Eat() {fmt.Println(s.Name, "is eating in the school cafeteria")
}
// 实现 Animal 接口的 Drink 方法
func (s *Student) Drink() {fmt.Println(s.Name, "is drinking in the school cafeteria")
}
func (s *Student) SayHey() {fmt.Println("i love you")
}
func (s Student) learnEnglish() {
// append 为 Go 自带函数,向数组和 slice 中添加元素
s.Learned = append(s.Learned, "i'm fine, thank you")
}
func (s *Student) learnMath() {s.Learned = append(s.Learned, "1 + 1 = 2")
}
func (s *Student) whoAmI() {fmt.Println("your name is :", s.Name, "and your className is :", s.ClassInfo.Name, "and your schoolName is :", s.SchoolInfo.Name)
}
func (s Student) whoAmII() {fmt.Println("your name is :", s.Name, "and your className is :", s.ClassInfo.Name, "and your schoolName is :", s.SchoolInfo.Name)
}
func learnChinese(s *Student) {s.Learned = append(s.Learned, "锄禾日当午,汗滴禾下土")
}
func learnPingPang(s Student) {s.Learned = append(s.Learned, "ping pang")
}
func main() {
s := Student{
People: People{
Name: "jack", // 小名
Age: 18,
Weight: 70,
Height: 180,
},
Name: "jack·li", // 大名
SchoolInfo: School{"北大附中"},
ClassInfo: Class{"高二·8 班"},
} // 初始化 student 对象
fmt.Println("学校:", s.SchoolInfo.Name)
fmt.Println("班级:", s.ClassInfo.Name)
fmt.Println("姓名:", s.Name) // 打印时会打印大名
fmt.Println("年龄:", s.Age, "岁")
fmt.Println("身高:", s.Height, "cm")
fmt.Println("体重:", s.Weight, "kg")
s.whoAmI()
s.whoAmII()
learnPingPang(s) // 学习乒乓球
learnChinese(&s) // 学习中文
s.learnEnglish() // 学英语
s.learnMath() // 学数学
fmt.Println(s.Name, "学过:") // 打印时会打印大名
for _, learned := range s.Learned { // 打印学过的知识
fmt.Printf("\t %s \n", learned)
}
s.People.Eat() // 直接使用内嵌类型的方法
s.People.Drink() // 直接使用内嵌类型的方法
s.People.Run() // 直接使用内嵌类型的方法
s.SayHey() // 使用 Student 的 sayHey
fmt.Println("俺叫:", s.People.Name) // 使用内嵌 People 的 name 打印小名
s.People.SayHey() // 使用 内嵌 People 的 SayHey
var xiaoming, xiaohua Animal // 大家都是动物,尴尬
//Student 的指针类型实现了 Animal 接口,可以使用 &Student 来给 Animal 赋值
xiaoming = &s //jack 的中文名叫 xiaoming
//People 的指针类型实现了 Animal 接口,可以使用 &People 来给 Animal 赋值
xiaohua = &People{Name: "xiaohua", Age: 5, Height: 100, Weight: 50} //xiaohua 还小,每到上学的年级,不是学生
xiaoming.Run() //xiaoming 在跑步
xiaohua.Run() //xiaohua 在跑步
xiaoming.Eat() //xiaoming 在吃东西
xiaohua.Eat() //xiaohua 在吃东西
xiaoming.Drink() //xiaoming 在吃东西
xiaohua.Drink() //xiaohua 在吃东西}
/*
运行结果:学校:北大附中
班级:高二·8 班
姓名:jack·li
年龄:18 岁
身高:180 cm
体重:70 kg
your name is : jack and your className is : 高二·8 班 and your schoolName is : 北大附中
your name is : jack and your className is : 高二·8 班 and your schoolName is : 北大附中
jack 学过:锄禾日当午,汗滴禾下土
1 + 1 = 2
jack·li is eating in the school cafeteria
jack·li is drinking in the school cafeteria
jack·li is running around campus
i love you
俺叫:jack
爱老虎油
jack·li is running around campus
xiaohua is running...
jack·li is eating in the school cafeteria
xiaohua is eating...
jack·li is drinking in the school cafeteria
xiaohua is drinking...
*/
将 People 的三个方法抽象成接口Anmial
,让 People 和 Student 两个结构都实现 Animal 的三个方法。声明 xiaohua 和 xiaoming 两个对象为 Animal 类型,给 xiaohua 声明一个还没上学 People 对象,给 xiaoming 声明一个已经上学的 Student 对象,最终得到了不一样的结果。
这里可能会有疑问,问什么将 jack 赋值给 xiaoming 时,给 xiaoming 的是 &s
指针地址。这要从函数的实现说起。因为函数的实现指定的是指针形式的类型,在赋值时需要赋予指针类型的值才不会发生值拷贝,而且可以在使用的过程中修改对象中的值。但是在使用时可以不加指针直接使用,比如 s.SayHey()
就可以直接使用,不用转换为指针类型。
总结
Go 通过 interface 也实现了面向对象中多态的特征。现在总结来看,Go 能够直接实现封装和多态,变相的实现继承的概念,这个在网络上被人称为是不完全的面向对象或者是弱面向对象,不过对于面向对象的开发,这已经够用了。
源码可以通过 ’github.com/souyunkutech/gosample’ 获取。
关注我们的「微信公众号」
首发微信公众号:Go 技术栈,ID:GoStack
版权归作者所有,任何形式转载请联系作者。
作者:搜云库技术团队
出处:https://gostack.souyunku.com/…