万物皆对象。学过Java编程的都知道Java是一门面向对象的语言,它拥有封装、继承和多态的特性。那可不能够说,拥有封装、继承和多态这一特性的语言就是面向对象的语言呢? 仔细想来,也确实是这样的,由于封装、继承和多态这三个特征,并非Java语言的特征,而是面向对象的三大特征。 总结来看,全部包含封装、继承和多态者三大特征的语言均可以说是面向对象的语言。git
那么Go语言是不是一门面向对象的语言呢?下面咱们经过举例的方式针对封装、继承和多态这面向对象的三大特征分别进行解释。github
Go中有struct
结构体,经过结构体可以实现现实世界中对象的封装。如将学生封装成对象,除了学生的基础信息外,还须要一些学生的基础行为。编程
定义结构体的方式以前在基础结构中进行了简单的解释,并无针对结构体的方法进行说明。这里先说明一下定义结构体的方法。数组
func(alias type) func_name(parameter1 type, parameter2 type2)(ret1 type3, ret2 type4){
...
}
复制代码
定义结构体的方法的语法与函数的语法相似,区别于普通函数,方法的定义在func后有一个括号(alias type)
,指定方法的附属结构体,以方便经过结构体来进行方法的使用。bash
看到这里难免有些Java的同窗以为不太好接受,毕竟在Java中,对象的方法都是写在class中的,在Go中方法都是写在结构体外的。微信
因此能够总结一句,Go中的函数分为两类,一种是有附属于结构体的方法,一种是普通函数。附属于结构体的函数,在使用的过程当中,须要结合结构体来使用,必须像Java那样先声明对象,而后结合对象才能使用。 普通函数仅有是否可被外部包访问的要求,不须要先声明结构体,结合结构体来使用,开盖即食哈。网络
方法的结构体在指定时,alias别名能够随意设置,可是所属类型不能,(此处有坑)下面看一个例子app
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中添加元素ide
这里有四个方法,两个打印名字的方法和两个学习的方法,区别点在于方法的所属类型一个是指针类型,另外一个是非指针类型。函数
执行结果显示,打印名字的方法都正确输出了名字,可是学习英语和数学后,却显示只学过数学,没学过英语,这岂不是让我等学生的老师很头疼?
这是为何呢?
这样就牵涉到了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
版权归做者全部,任何形式转载请联系做者。
做者:搜云库技术团队 出处:gostack.souyunku.com/2019/05/13/…