零基础 go - 53(面向对象编程三大特性 - 继承)

0 阅读11分钟

go仍然有面向对象编程的继、封装、多态的特性,只是实现方式和其他 OOP语言不同。Go 通过接口(interface)和结构体(struct)来实现这些特性

继承:Go 不支持传统意义上的类继承,但可以通过结构体嵌套来实现类似的功能。一个结构体可以包含另一个结构体作为字段,这样就可以访问被嵌套结构体的方法和字段,达到代码复用的目的。

封装:Go 通过包(package)来实现封装。一个包可以包含多个文件和结构体,只有以大写字母开头的标识符才是导出的,可以被其他包访问。这样可以控制哪些功能对外暴露,哪些功能保持私有。

多态:Go 通过接口来实现多态。一个接口定义了一组方法,任何实现了这些方法的类型都满足这个接口。这样就可以编写函数或方法来接受接口类型的参数,而不关心具体的实现类型

继承的场景

  • 写一个小学生考试的程序,包含学生信息、考试成绩等属性

  • 写完之后,又有一个需求:写一个中学生、大学生、研究生考试的程序,这些学生类都包含姓名、年龄等属性,考试类包含考试科目、考试成绩等属性,成绩类包含分数、等级等属性

  • 如果这个时候去复制小学生的代码来实现中学生、大学生、研究生的程序,会发现很多代码是重复的,比如姓名、年龄、考试科目、考试成绩等属性和方法都是一样的

  • 这时候就可以通过结构体嵌套来复用小学生的结构体,同时添加中学生、大学生、研究生特有的属性和方法

继承的实现语法

image.png


type 结构体1 struct{

    字段1 类型

    字段2 类型

    ...

}

type 结构体2 struct{

    结构体1 // 嵌套结构体1 ---- 实现继承

    字段3 类型

    字段4 类型

    ...

}

继承的代码示例


package main

import "fmt"

// 定义小学生结构体

type PrimaryStudent struct {

    Name string

    Age int

    Subject string

    Score int

}

// 定义中学生结构体,嵌套小学生结构体

type MiddleStudent struct {

    PrimaryStudent // 嵌套小学生结构体,实现继承

    Grade string

}

// 定义大学生结构体,嵌套小学生结构体

type UniversityStudent struct {

    PrimaryStudent // 嵌套小学生结构体,实现继承

    Major string

}

// 定义研究生结构体,嵌套小学生结构体

type GraduateStudent struct {

    PrimaryStudent // 嵌套小学生结构体,实现继承

    ResearchTopic string

}

func (p PrimaryStudent) SetInfo(name string, age int, subject string, score int) string {

    return fmt.Sprintf("姓名: %s, 年龄: %d, 科目: %s, 成绩: %d", name, age, subject, score)

}

func main() {

    // 创建一个中学生对象

    middle := MiddleStudent{

        PrimaryStudent: PrimaryStudent{

            Name: "小明",

            Age: 15,

            Subject: "数学",

            Score: 90,

        },

        Grade: "九年级",

    }

    // 创建一个大学生对象

    university := &UniversityStudent{

        {

            Name: "小红",

            Age: 20,

            Subject: "计算机科学",

            Score: 95,

        },

        "软件工程",

    }

    // 创建一个研究生对象

    var graduate *GraduateStudent = &GraduateStudent{

        PrimaryStudent: PrimaryStudent{

            Name: "小刚",

            Age: 25,

            Subject: "人工智能",

            Score: 98,

        },

        ResearchTopic: "深度学习",

    }

    // 输出中学生信息

    str1 := middle.SetInfo(middle.Name, middle.Age, middle.Subject, middle.Score)

    fmt.Println(str1)

    fmt.Printf("中学生: 姓名=%s, 年龄=%d, 科目=%s, 成绩=%d, 年级=%s\n", middle.Name, middle.Age, middle.Subject, middle.Score, middle.Grade)

    // 输出大学生信息

    str2 := university.SetInfo(university.Name, university.Age, university.Subject, university.Score)

    fmt.Println(str2)

    fmt.Printf("大学生: 姓名=%s, 年龄=%d, 科目=%s, 成绩=%d, 专业=%s\n", university.Name, university.Age, university.Subject, university.Score, university.Major)

    // 输出研究生信息

    str3 := graduate.SetInfo(graduate.Name, graduate.Age, graduate.Subject, graduate.Score)

    fmt.Println(str3)

    fmt.Printf("研究生: 姓名=%s, 年龄=%d, 科目=%s, 成绩=%d, 研究方向=%s\n", graduate.Name, graduate.Age, graduate.Subject, graduate.Score, graduate.ResearchTopic)

}

继承使用细节

  • 结构体可以使用 嵌套匿名结构体的所有字段和方法(不管是大写还是小写字母开头命名的字段、方法)

package main

import "fmt"

type A struct {

    Name string

    age int

}

type B struct {

    A // 嵌套结构体A

    Grade string

}

func (a *A) SayHello() {

    fmt.Printf("Hello, I am %s\n", a.Name)

}

func (a *A) setAge(age int) {

    a.age = age

}

func main() {

    b := &B{}

    b.Grade = "九年级" // 设置B自己的字段Grade

    b.A.Name = "小明" // 直接访问A的字段设置Name

    b.A.setAge(20) // 直接调用A的方法设置age

    b.A.SayHello() // 直接调用A的方法SayHello

    fmt.Printf("Name: %s, Age: %d, Grade: %s\n", b.A.Name, b.A.age, b.Grade)

}

  • 匿名结构字段访问可以简化:可以直接访问匿名结构体的字段和方法,无需通过嵌套结构体的名字来访问

当我们直接通过 b 来访问字段和方法,执行流程如下:

  1. 首先检查 B 结构体是否有 Grade 字段,发现有,直接访问并设置 Grade 字段的值。

  2. 当访问 b.Name 时,B 结构体本身没有 Name 字段,Go 语言会继续查找嵌套的结构体 A,发现 A 结构体有 Name 字段,因此直接访问并设置 Name 字段的值。

  3. 当调用 b.setAge(20) 时,B 结构体本身没有 setAge 方法,Go 语言会继续查找嵌套的结构体 A,发现 A 结构体有 setAge 方法,因此直接调用该方法设置 age 字段的值。

  4. 当调用 b.SayHello() 时,B 结构体本身没有 SayHello 方法,Go 语言会继续查找嵌套的结构体 A,发现 A 结构体有 SayHello 方法,因此直接调用该方法输出问候语


package main

import "fmt"

type A struct {

    Name string

    age int

}

type B struct {

    A // 嵌套结构体A

    Grade string

}

func (a *A) SayHello() {

    fmt.Printf("Hello, I am %s\n", a.Name)

}

func (a *A) setAge(age int) {

    a.age = age

}

func main() {

    b := &B{}

    b.Grade = "九年级" // 设置B自己的字段Grade

    b.A.Name = "小明" // 直接访问A的字段设置Name

    b.A.setAge(20) // 直接调用A的方法设置age

    b.A.SayHello() // 直接调用A的方法SayHello





    // 简写形式:直接访问A的字段设置Name(匿名嵌套结构体的字段可以直接访问)

    b.setAge(20) // 简写形式:直接调用A的方法设置age(匿名嵌套结构体的方法可以直接访问)

    b.SayHello() // 简写形式:直接调用A的方法SayHello(匿名嵌套结构体的方法可以直接访问)

    b.Name = "小明" // 简写形式:直接访问A的字段设置Name(匿名嵌套结构体的字段可以直接访问)





    fmt.Printf("Name: %s, Age: %d, Grade: %s\n", b.A.Name, b.A.age, b.Grade)

}

  • 当结构体和嵌套的匿名结构体,有相同的字段或方法时,访问会优先使用外层结构体的字段或方法,这时可以通过嵌套结构体的名字来访问被遮蔽的字段或方法

package main

import "fmt"

type A struct {

    Name string

    age int

}

type B struct {

    A // 嵌套结构体A

    Name string // 与A结构体的Name字段同名

    Grade string

}

func (a *A) SayHello() {

    fmt.Printf("Hello, I am %s\n", a.Name)

}

func (a *A) setAge(age int) {

    a.age = age

}

func (b *B) SayHello() {

    fmt.Printf("Hi, I am %s\n", b.Name)

}

func main() {

    b := &B{}

    b.Grade = "九年级" // 设置B自己的字段Grade

    b.Name = "小明" // 设置B自己的字段Name,遮蔽了A结构体的Name字段

    b.A.Name = "小红" // 通过嵌套结构体A的名字来访问被遮蔽的Name字段,设置A结构体的Name字段的值

    b.A.setAge(20) // 直接调用A的方法设置age

    b.A.SayHello() // 直接调用A的方法SayHello,输出Hello, I am 小红

    b.SayHello() // 调用B的方法SayHello,输出Hi, I am 小明

    fmt.Printf("Name: %s, Age: %d, Grade: %s\n", b.A.Name, b.A.age, b.Grade)

}

  • 当结构体嵌入多个匿名结构体,并且这些匿名结构体中有同名字段或方法时,访问会产生歧义,这时必须通过嵌套结构体的名字来明确访问哪个字段或方法.如果没有指定结构体名字来访问同名字段或方法,编译器会报错提示访问歧义

package main

import "fmt"

type A struct {

    Name string

    age int

}

type C struct {

    Name string

    hobby string

}

type B struct {

    A // 嵌套结构体A

    C // 嵌套结构体C

    Name string // 与A结构体和C结构体的Name字段同名

    Grade string

}

func (a *A) SayHello() {

    fmt.Printf("Hello, I am %s\n", a.Name)

}

func (c *C) SayHello() {

    fmt.Printf("Hi, I am %s\n", c.Name)

}

func (b *B) SayHello() {

    fmt.Printf("Hey, I am %s\n", b.Name)

}

func main() {

    b := &B{}

    b.Grade = "九年级" // 设置B自己的字段Grade

    b.Name = "小明" // 设置B自己的字段Name,遮蔽了A结构体和C结构体的Name字段

    b.A.Name = "小红" // 通过嵌套结构体A的名字来访问被遮蔽的Name字段,设置A结构体的Name字段的值

    b.C.Name = "小刚" // 通过嵌套结构体C的名字来访问被遮蔽的Name字段,设置C结构体的Name字段的值

    b.A.setAge(20) // 直接调用A的方法设置age

    b.A.SayHello() // 直接调用A的方法SayHello,输出Hello, I am 小红

    b.C.SayHello() // 直接调用C的方法SayHello,输出Hi, I am 小刚

    b.SayHello() // 调用B的方法SayHello,输出Hey, I am 小明

    fmt.Printf("Name: %s, Age: %d, Grade: %s\n", b.A.Name, b.A.age, b.Grade)




    // 如果直接访问同名字段或方法,会产生访问歧义,编译器会报错提示访问歧义

    // b.Name = "小明" // 访问歧义,编译器会报错提示访问歧义,因为B结构体的Name字段遮蔽了A结构体和C结构体的Name字段

    // b.SayHello() // 访问歧义,编译器会报错提示访问歧义,因为B结构体的SayHello方法遮蔽了A结构体和C结构体的SayHello方法

}

  • 如果结构体嵌套的是有名结构体而非匿名结构体,那么就不会有字段和方法的提升,访问时必须通过嵌套结构体的名字来访问字段和方法

package main

import "fmt"

type A struct {

    Name string

    age int

}

type B struct {

    a A // 嵌套有名结构体A

    Grade string

}

func (a *A) SayHello() {

    fmt.Printf("Hello, I am %s\n", a.Name)

}

func main() {

    b := &B{}

    b.Grade = "九年级" // 设置B自己的字段Grade

    b.a.Name = "小明" // 通过嵌套结构体a的名字来访问A结构体的Name字段,设置A结构体的Name字段的值

    b.a.setAge(20) // 通过嵌套结构体a的名字来调用A结构体的方法设置age

    b.a.SayHello() // 通过嵌套结构体a的名字来调用A结构体的方法SayHello,输出Hello, I am 小明

    fmt.Printf("Name: %s, Age: %d, Grade: %s\n", b.a.Name, b.a.age, b.Grade)




    // 如果直接访问字段或方法,会报错提示找不到字段或方法,因为没有字段和方法的提升

    // b.Name = "小明" // 编译错误,找不到字段Name,因为B结构体没有Name字段,且a是一个有名结构体,没有字段和方法的提升

    // b.SayHello() // 编译错误,找不到方法SayHello,因为B结构体没有SayHello方法,且a是一个有名结构体,没有字段和方法的提升

}

···

- 嵌套匿名结构体后,可以在创建结构体变量时,直接指定各个匿名结构体的字段值,语法如下:

```go

type A struct {

    Name string

    age int

}

type C struct {

    Name string

    hobby string

}

type B1 struct {

    A // 嵌套结构体A

    C // 嵌套结构体C

    Grade string

}

type B2 struct {

    *A // 嵌套结构体A

    *C // 嵌套结构体C

    Grade string

}

func main() {

// 创建一个B结构体变量,并直接指定A结构体的字段值和B结构体的字段值

// 直接指定字段值,Go 语言会自动创建匿名结构体并赋值

b11 := B1{

    A: A{

        Name: "小明",

        age: 20,

    },

    C: C{

        Name: "小红",

        hobby: "篮球",

    },

    Grade: "九年级",

}

// 直接指定字段值的简写形式,Go 语言会自动创建匿名结构体并赋值

b12 := B1{

    A{

        Name: "小明",

        age: 20,

    },

    C{

        Name: "小红",

        hobby: "篮球",

    },

    "九年级",

}

  


// 使用指针类型的匿名结构体时,也可以直接指定字段值,Go 语言会自动取地址创建匿名结构体的指针

b22 := B2{

    A: &A{

        Name: "小明",

        age: 20,

    },

    C: &C{

        Name: "小红",

        hobby: "篮球",

    },

    Grade: "九年级",

}

// 直接指定字段值的简写形式,Go 语言会自动取地址创建匿名结构体的指针

b23 := B2{

    &A{

        Name: "小明",

        age: 20,

    },

    &C{

        Name: "小红",

        hobby: "篮球",

    },

    "九年级",

}

}

  • 结构体中可以定义匿名字段是基本数据类型,这时匿名字段的名字就是类型名,访问时直接使用类型名来访问匿名字段

  • 注意:当结构体中定义了多个相同类型的匿名字段时,访问会产生歧义,这时必须通过嵌套结构体的名字来明确访问哪个字段


package main

import "fmt"

type B struct {

    string // 匿名字段,类型为string

    int // 匿名字段,类型为int

    // int // 匿名字段,类型为int,与上一个匿名字段类型相同,访问会产生歧义

    a int // 有名字段,类型为int ----可以的

}

func main() {

    b := &B{}

    b.string = "Hello" // 访问匿名字段string,设置值为Hello

    b.int = 100 // 访问匿名字段int,设置值为100

    b.a = 200 // 访问有名字段a,设置值为200

}

多重继承

一个结构体中嵌套多个匿名结构体,可以实现多重继承的效果

  • 如果结构体中的字段和嵌套的匿名结构体中的字段相同,那么在访问或者使用时,需要通过指定嵌套结构体的名字来明确访问哪个字段

package main

import "fmt"

type A struct {

    Name string

}

type B struct {

    A // 嵌套结构体A

    Name string // 有和A结构体的Name字段同名的字段

    Grade string

}

func main() {

    b := &B{}

    b.A.Name = "小明" // 通过嵌套结构体A的名字来访问A结构体的Name字段,设置A结构体的Name字段的值

    b.Name = "小红" // 访问B自己的Name字段

    b.Grade = "九年级" // 设置B自己的字段Grade

    fmt.Printf("Name: %s, Grade: %s, B.Name: %s\n", b.A.Name, b.Grade, b.Name) // 输出: Name: 小明, Grade: 九年级, B.Name: 小红

}