【跟着我们学Golang】之面向对象
万物皆对象。学过Java编程的都知道Java是一门面向对象的语言,它拥有封装、继承和多态的特性。那可不可以说,拥有封装、继承和多态这一特性的语言就是面向对象的语言呢?
仔细想来,也确实是这样的,因为封装、继承和多态这三个特征,并不是Java语言的特征,而是面向对象的三大特征。
总结来看,所有包含封装、继承和多态者三大特征的语言都可以说是面向对象的语言。
那么Go语言是否是一门面向对象的语言呢?下面我们通过举例的方式针对封装、继承和多态这面向对象的三大特征分别进行解释。
封装
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
版权归作者所有,任何形式转载请联系作者。
作者:搜云库技术团队