在上一讲中,我们结合两只柴犬的例子介绍了如何构建独一无二的对象,以及如何让对象“动起来”的方法。本讲我们继续深入结构体的讲解,也是结构体部分的最后一讲——使用结构体实现继承。
我们已经知道,构造函数和方法是面向对象语言所具备的能力,继承亦然。Go语言的语法设计非常灵活、简单和易用,仅依靠结构体,便可实现上述所有能力。
# 什么是继承
继承在实际开发中用途同样非常广泛,甚至在现实生活中也有很多类似的应用。
举例而言,在任何一家公司,都是由很多人组成的,不同的人有不同的职务,比如总经理、部门经理、职员等等。发现了吗?无论是总经理、部门经理还是职员,都是由人派生出来的,他们首先属于“人”,然后再属于某个职务。
如果把“人”和总经理等等具有具体职务的人都看作是结构体的话,“人”便是他们的“父结构体”,无论是何种职务的人,都由这个父结构体派生出来,具有父结构体所有的属性。然后根据职务不同,拥有独特性的属性。可以看作是“子结构体”。
从另一个角度看,这些 “子结构体”扩展了“父结构体” 。作为“子结构体”,不仅可以使用“父结构体”的属性,还拥有自己的属性。如果说 “父结构体”是概括的,抽象的,那么“子结构体”就是具体的,详细的。
具体来说,假如“人”结构体具有较为通用的姓名、年龄、性别三个属性,可以执行吃饭、睡觉、走路、奔跑四个动作。作为派生出的“总经理”还额外具有持股比例属性,可以额外执行安排工作任务的动作……
💡 提示:发现了吗?这个例子中,作为“父结构体”的“人”并没有直接使用,仅用它派生出的“子结构体”就能描述整个公司的组织架构了。但这并不表明“父结构体”永远不会直接使用,具体还需要根据实际开发需求的不同而定。
从本质上说,Go语言中继承,是通过结构体的嵌套来实现的。
为了让各位更好地理解,接下来我将使用动物(Animal)作为父结构体,鸟(Bird)和狗(Dog)作为子结构体,来介绍如何使用结构体实现面向对象中的继承特性。
# 结构体的嵌套
我们先来实现作为父结构体的动物(Animal),这个结构体具有名字(Name)、年龄(Age)和性别(Gender)属性。
💡 提示:请留意这三个属性也是本例中所有子结构体所具有的。
示例代码如下:
go
复制代码type Animal struct {
Name int
Age int
Gender string
}
接下来,以子结构体鸟(Bird)为例,它还具有翅膀颜色的属性。因此,Bird的结构体定义示例如下:
go
复制代码type Bird struct {
WingColor string
CommonAnimal Animal
}
很明显地,Bird结构体中包含了一个名为CommonAnimal的Animal类型成员,而Animal类型就是我们刚刚定义好的结构体。如此,便完成了结构体的嵌套,即把Animal嵌入Bird中。从此,Bird也具有了Animal中的Name、Age和Gender属性了。
继续使用上一讲中“构造函数”的知识,创建Bird类型的构造函数:
go
复制代码func NewBird(name string, age int, gender string, wingColor string) *Bird {
return &Bird{
WingColor: wingColor,
CommonAnimal: Animal{
Name: name,
Age: age,
Gender: gender,
},
}
}
接着,鸟还有“飞行”的动作。使用上一讲中“方法”的知识,创建Bird类型的“飞行”方法:
go
复制代码func (b *Bird) Fly() {
fmt.Println("我起飞啦!")
}
关于“鸟”的结构体定义、构造函数和方法的实现到此先告一段落。我们回到main()函数中使用它们。
在main()函数中,首先声明一个变量,名为bird,使用NewBird()构造函数为其赋值,然后再调用Fly()方法,让小鸟执行飞行动作。完整的代码如下:
go
复制代码type Animal struct {
Name string
Age int
Gender string
}
type Bird struct {
WingColor string
CommonAnimal Animal
}
func NewBird(name string, age int, gender string, wingColor string) *Bird {
return &Bird{
WingColor: wingColor,
CommonAnimal: Animal{
Name: name,
Age: age,
Gender: gender,
},
}
}
func (b *Bird) Fly() {
fmt.Println("我起飞啦!")
}
func main() {
bird := *NewBird("小鸟", 1, "公", "绿色")
fmt.Println(bird)
bird.Fly()
}
运行这段代码,可以看到控制台的输出内容如下所示:
{绿色 {小鸟 1 公}}
我起飞啦!
从输出的格式上,我们也可看出,Animal类型确实被Bird类型嵌入其中。那么,问题也随之而来:若想访问Bird中的Animal中的Name属性值,该怎么做呢?
思路其实非常简单,也是层层嵌套地访问就可以了。就拿本例来说,bird.CommonAnimal访问到的是CommonAnimal属性,它是Animal类型;bird.CommonAnimal.Name,访问到的就是CommonAnimal中的Name属性了。
类似地,我们继续定义子结构体狗(Dog),它拥有毛色(Color)属性。还有犬吠(Bark)动作。请读者参考上面小鸟(Bird)部分的代码,独立完成狗(Dog)部分的代码,要求依然使用构造函数(NewDog())和方法(Bark())。
完整的代码如下:
go
复制代码type Animal struct {
Name string
Age int
Gender string
}
type Dog struct {
Color string
CommonAnimal Animal
}
func NewDog(name string, age int, gender string, color string) *Dog {
return &Dog{
Color: color,
CommonAnimal: Animal{
Name: name,
Age: age,
Gender: gender,
},
}
}
func (d *Dog) Bark() {
fmt.Println("汪汪汪!")
}
func main() {
dog := *NewDog("小狗", 2, "公", "黄色")
fmt.Println(dog)
dog.Bark()
}
运行上述代码,控制台将输出:
{黄色 {小狗 2 公}}
汪汪汪!
有了Bird,Dog的实现应该不会有问题吧?
在继续之前,我有个问题要考考大家:dog变量是Dog类型,bird变量是Bird类型。那么,dog可以执行Fly()方法吗?反过来,bird可以执行Bark()方法吗?为什么?
答案是:统统不能。
因为Fly()方法的接收者是Bird,Bark()方法的接收者是Dog。动作的接收者不同,意味着发生或执行动作的对象不同,因此不能混用。(想想现实世界中,有谁见过小鸟犬吠,小狗起飞吗?)
但是,有一些动作确实是小狗和小鸟同时具备的,比如:吃饭(Eat)。几乎没有哪种动物(Animal)能够不吃饭而存活吧?
那么,若要实现吃饭这个动作,继续为Bird和Dog分别创建方法当然是可行的。但又没有更好的实现方式呢?
当然有!Bird和Dog都是由Animal派生而来,而吃饭(Eat)又是Animal所具有的公共动作。因此,我们便可为Animal创建一个方法,接收者是*Animal类型即可。这样一来,因为Bird和Dog都嵌入了Animal类型数据,自然也就可以执行Animal的动作了。
下面,创建一个接收者是*Animal类型的方法,名为Eat()。为了体现动作的作用对象,我们在Eat()方法中,将Name属性值一并输出到控制台中。具体代码如下:
go
复制代码type Animal struct {
Name string
Age int
Gender string
}
func (a *Animal) Eat() {
fmt.Println(a.Name, "我要吃到饱!")
}
下面,回到main()函数中,使用bird和dog变量逐层调用Eat()方法。具体代码如下:
go
复制代码func main() {
bird := *NewBird("小鸟", 1, "公", "绿色")
bird.CommonAnimal.Eat()
dog := *NewDog("小狗", 2, "公", "黄色")
dog.CommonAnimal.Eat()
}
运行程序,控制台将输出:
小鸟 我要吃到饱!
小狗 我要吃到饱!
通过上面的输出结果可以看出:虽然它们调用的是公共方法,但由于执行该动作的变量不同,最终的输出结果也会随之变化。这便是我们想要的效果。
# 匿名结构体嵌套
Go语言语法还允许开发者以一种更为简单的方式嵌套结构体使用,这种更简单的方式便是嵌套匿名结构体。在后期使用时,也会被简化。以Bird类型结构体为例,下面的写法是完全合法的:
go
复制代码type Animal struct {
Name string
Age int
Gender string
}
func (a *Animal) Eat() {
fmt.Println(a.Name, "我要吃到饱!")
}
type Bird struct {
string
Animal
}
func NewBird(name string, age int, gender string, wingColor string) *Bird {
return &Bird{
wingColor,
Animal{
name,
age,
gender,
},
}
}
func (b *Bird) Fly() {
fmt.Println("我起飞啦!")
}
func main() {
bird := *NewBird("小鸟", 1, "公", "绿色")
//访问string类型成员
fmt.Println(bird.string)
//访问Name成员
fmt.Println(bird.Name)
bird.Eat()
上述代码运行后,控制台将输出:
绿色
小鸟
小鸟 我要吃到饱!
请大家将这种简化写法与普通的写法对比,重点关注Bird结构体的定义方式、NewBird()构造函数的实现方式以及main()函数中,bird变量的字段取值和方法调用方式。
# 总结
🎉 恭喜,您完成了本次课程的学习!
📌 以下是本次课程的重点内容总结:
- 使用结构体实现继承。
本讲是结构体系列专题的第三篇,也是这个专题的最后一篇。在本讲中,需要重点掌握的就是“继承”了。首先要明确继承中“父”与“子”的概念,知道它们之间的关系——“子结构体”扩展了“父结构体” 。以及它们之间的区别:“父结构体”是概括的,抽象的,那么“子结构体”就是具体的、详细的。接着便是利用Go语言中的结构体来实现继承的语法知识了。
最后,从Go语言的语法设计中,我们还可得到如下结论:
Go语言中的继承是通过内嵌或组合来实现的,在Go语言中,相比较于继承,组合更受青睐。
灵活地使用结构体,通过嵌套的方式可以描述结构非常复杂的数据。在执行网络请求场景中,解析服务端返回的JSON格式通常会用到结构体嵌套的技能。有关这部分的扩展阅读资料请参考:Go语言使用匿名结构体解析JSON数据 (biancheng.net) (opens new window)。
➡️ 在下次课程中,我们会介绍Go语言中接口的使用技巧,具体内容是:
- 接口的定义和使用