Golang中的继承

Golang继承介绍

继承可以解决代码复用,让我们的编程更加靠近人类思维

当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在结构体中定义这些相同的属性和方法

在Golang中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性

下面这个图可以更好的展现Golang语言中的继承关系

尚硅谷Go语言课程


Golang中的继承基本语法

使用嵌套匿名结构体

type Goods struct {
    Name string
    Price int
}
type Book struct {
    Goods
    Writer string
}

下面我们用继承完成学生考试系统,学生包括小学生和大学生

程序目录为


首先是student.go中的代码

package model

import "fmt"

type Student struct {
    Name   string
    Age    int
    scores int
}

type Pupil struct {
    Student
}

type Graduate struct {
    Student
}

func (stu *Student) SetScores(scores int) {
    stu.scores = scores
}

func (stu *Student) GetAll() {
    fmt.Println("The all various are:", stu.Name, stu.Age, stu.scores)
}

func (pup *Pupil) Testing() {
    fmt.Println("The pupil is testing !!!")
}

func (gra *Graduate) Testing() {
    fmt.Println("The graduate is testing !!!")
}

再是main.go中的代码

package main

import (
    "fmt"

    "../model"
)

func main() {
    //小学生
    pupil := &model.Pupil{}
    pupil.Student.Name = "Tom"
    pupil.Student.Age = 10
    pupil.Testing()
    pupil.Student.SetScores(80)
    pupil.Student.GetAll()
    //大学生
    fmt.Println("----------------------------------")
    graduate := &model.Pupil{}
    graduate.Student.Name = "Bill"
    graduate.Student.Age = 21
    graduate.Testing()
    graduate.Student.SetScores(90)
    graduate.Student.GetAll()
}

运行main函数,输出的结果是

The pupil is testing !!!
The all various are: Tom 10 80
-------------------------------
The pupil is testing !!!
The all various are: Bill 21 90

有了继承,代码的复用性提高了,代码的扩展性和维护性也提高了

继承的细节讨论

①结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用,当然,这句话的大前提是在一个包的情况下,要是不同包就可能不行了

②匿名结构体字段可以简化

pupil := &model.Pupil{}
pupil.Student.Name = "Tom"   -----> pupil.Name = "Tom"
pupil.Student.Age = 10       -----> pupil.Age  = 10
pupil.Testing()
pupil.Student.SetScores(80)  -----> pupil.SetScores(80)
pupil.Student.GetAll()       -----> pupil.GetAll()

这里可以这么改,就非常的简单,每次都召唤匿名结构体太复杂了

当我们直接通过pupil访问student方法或字段时,其流程如下,比如pupil.Age,编译器会先看pupil对应的struct有没有Age,如果有就直接调用响应struct类型的Age字段,如果没有就去看pupil嵌入的匿名结构体中有没有声明,如果有就调用,如果没有就继续找student中的匿名结构体,如果也找不到就以此类推继续找,如果都没有找到就报错

③当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分

我们写代码来看这个就近原则是怎么个就近原则法,还是用pupil,graduate来说,文件目录和一开始的一样

student.go中的代码

package model

import "fmt"

type Student struct {
    Name   string
    Age    int
    scores int
}

type Pupil struct {
    Student
    Name string
}

type Graduate struct {
    Student
}

func (stu *Student) SetScores(scores int) {
    stu.scores = scores
}

func (stu *Student) GetAll() {
    fmt.Println("The all various are:", stu.Name, stu.Age, stu.scores)
}

func (pup *Pupil) GetJustPupil() {
    fmt.Println("The all various are:", pup.Name, pup.Age, pup.scores)
}

func (pup *Pupil) Testing() {
    fmt.Println("The pupil is testing !!!")
}

func (gra *Graduate) Testing() {
    fmt.Println("The graduate is testing !!!")
}

下面是main.go中的代码

package main

import (
    "../model"
)

func main() {
    //小学生
    pupil := &model.Pupil{}
    pupil.Name = "Tom"
    pupil.Student.Name = "Bill"
    pupil.Age = 10
    pupil.Testing()
    pupil.SetScores(80)
    pupil.GetAll()
    pupil.GetJustPupil()
}

输出的结果是

The pupil is testing !!!
The all various are: Bill 10 80
The all various are: Tom 10 80

我们给pupil.Name赋值的时候,pupil在其结构体内找到了Name,因此就用就近原则直接赋值,如果我们想赋值给匿名结构体中的,就需要指明出来,比如pupil.Student.Name = "Bill",因此接下来pupil.GetAll()绑定的是student结构体指针,编译器在pupil的结构体中找不到GetAll()于是就进入student匿名结构体中寻找方法,找到了之后,匿名结构体中的方法调用的就是匿名结构体中的变量,这也是就近原则。对于pupil.GetJustPupil()方法绑定的是pupil结构体指针,编译器在pupil结构体中寻找到了绑定的GetJustPupil()方法,因此,就直接调用,根据就近原则,与pupil结构体绑定的GetJustPupil()方法就直接调用pupil结构体中的变量

我们再来玩一个,再次修改student.go中的代码

package model

import "fmt"

type Student struct {
    Name   string
    Age    int
    scores int
}

type Pupil struct {
    Student
    Name string
}

type Graduate struct {
    Student
}

func (stu *Student) SetScores(scores int) {
    stu.scores = scores
}

func (stu *Student) GetAll() {
    fmt.Println("The all various are:", stu.Name, stu.Age, stu.scores)
}

func (stu *Student) GetName() {
    fmt.Println("The name is:", stu.Name)
}

func (pup *Pupil) GetName() {
    fmt.Println("The name is:", pup.Name)
}

func (pup *Pupil) GetJustPupil() {
    fmt.Println("The all various are:", pup.Name, pup.Age, pup.scores)
}

func (pup *Pupil) Testing() {
    fmt.Println("The pupil is testing !!!")
}

func (gra *Graduate) Testing() {
    fmt.Println("The graduate is testing !!!")
}

这次在student.go中让父结构体和本结构体都有相同名称的方法

我们在main.go中调用

package main

import (
    "../model"
)

func main() {
    //小学生
    pupil := &model.Pupil{}
    pupil.Name = "Tom"
    pupil.Student.Name = "Bill"
    pupil.Age = 10
    pupil.Testing()
    pupil.SetScores(80)
    pupil.GetAll()
    pupil.GetJustPupil()
    pupil.GetName()
    pupil.Student.GetName()
}

输出的是

The pupil is testing !!!
The all various are: Bill 10 80
The all various are: Tom 10 80
The name is: Tom
The name is: Bill

我们看最后两行,第一个是Tom,第二个是Bill

还是一个道理,遵从就近原则

最后再玩一个,student.go中我们将pupil结构体中的Name给注释掉

package model

import "fmt"

type Student struct {
    Name   string
    Age    int
    scores int
}

type Pupil struct {
    Student
    //  Name string
}

type Graduate struct {
    Student
}

func (stu *Student) SetScores(scores int) {
    stu.scores = scores
}

func (stu *Student) GetAll() {
    fmt.Println("The all various are:", stu.Name, stu.Age, stu.scores)
}

func (stu *Student) GetName() {
    fmt.Println("The name is:", stu.Name)
}

func (pup *Pupil) GetName() {
    fmt.Println("The name is:", pup.Name)
}

func (pup *Pupil) GetJustPupil() {
    fmt.Println("The all various are:", pup.Name, pup.Age, pup.scores)
}

func (pup *Pupil) Testing() {
    fmt.Println("The pupil is testing !!!")
}

func (gra *Graduate) Testing() {
    fmt.Println("The graduate is testing !!!")
}

我们注释掉,然后main.go中的代码不变,输出如下:

The pupil is testing !!!
The all various are: Bill 10 80
The all various are: Bill 10 80
The name is: Bill
The name is: Bill

我们可以看到两次输出都是Bill。再main函数中,pupil.Name = “Tom”, 由于在pupil结构体中没有找到Name这个字段,因此去匿名结构体中找,找到了就赋值,pupil.Student.Name = "Bill"则是直接去匿名结构体中找,找到了就赋值,因此,“Bill”相当于把“Tom”覆盖掉了

然后接下来的两个方法pupil.GetName()和pupil.Student.GetName()遵从就近原则,在pupil结构体中找不到Name字段,就在其父结构体student中找Name字段,找到了就正常运行

④结构体中嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错

为什么一定要标注结构体本身没有同名的字段和方法,因为如果有就不存在这个问题了,直接就近原则在本结构体中找就可以了

type A struct {
    Name string
    Age  int
}

type B struct {
    Name  string
    score int
}

type C struct {
    A
    B
}

A和B结构体都被嵌入到C中,A和B中都有name,但是C中没有name

在这种情况下,假如C类型的结构体有一个实例变量是c,调用name的时候就要

c.A.Name
c.B.Name

这样才能区分,如果直接用c.Name,编译器直接报错

⑤如果一个struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字

//这种就是组合
type A struct {
    Name string
    Age  int
}

type C struct {
    a A
}

在组合模式下,C结构体中无匿名结构体,这时候要调用A结构体中的Name时,编译器不会自动往上找,只会在C中寻找

⑥嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值

pupil1 := &model.Pupil{Student: model.Student{
        Name:   "zideng",
        Age:    12,
        Scores: 89},
    }
pupil2 := &model.Pupil{Student: model.Student{Name: "yichen", Age: 13, Scores: 90}}
fmt.Println("pupil1 is:", pupil1)
fmt.Println("pupil2 is:", pupil2)

输出的结果是

pupil1 is: &{{zideng 12 89}}
pupil2 is: &{{yichen 13 90}}

还有的人喜欢嵌套的时候直接用结构体指针,比如

type TV struct {
    *Goods
    *Brand
}

这个在操作的时候,记得使用取地址(&)就可以了

⑦还有一个奇怪的写法,将基本数据类型写成匿名字段

type Monster struct {
    Name string
    Age  int
}

type E struct {
    Monster
    int           //匿名字段可以是基本数据类型,当然这个int匿名字段不能重复
}

func main() {
    var e E
    e.Name = "猫"
    e.Age = 300
    e.int = 20
    fmt.Println("e=", e)
}

这样用也行

多重继承

如果一个struct嵌套了多个匿名结构体,那么改结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承 这个其实在前面的细节讨论中已经出现过了

type TV struct {
    Goods
    Brand
}

如果嵌入的匿名结构体有相同的字段名或方法名,则在访问时,需要通过匿名结构体类型名来区分

但是为了保证代码的简洁性,建议尽量不要使用多重继承