go仍然有面向对象编程的继、封装、多态的特性,只是实现方式和其他 OOP语言不同。Go 通过接口(interface)和结构体(struct)来实现这些特性
继承:Go 不支持传统意义上的类继承,但可以通过结构体嵌套来实现类似的功能。一个结构体可以包含另一个结构体作为字段,这样就可以访问被嵌套结构体的方法和字段,达到代码复用的目的。
封装:Go 通过包(package)来实现封装。一个包可以包含多个文件和结构体,只有以大写字母开头的标识符才是导出的,可以被其他包访问。这样可以控制哪些功能对外暴露,哪些功能保持私有。
多态:Go 通过接口来实现多态。一个接口定义了一组方法,任何实现了这些方法的类型都满足这个接口。这样就可以编写函数或方法来接受接口类型的参数,而不关心具体的实现类型
继承的场景
-
写一个小学生考试的程序,包含学生信息、考试成绩等属性
-
写完之后,又有一个需求:写一个中学生、大学生、研究生考试的程序,这些学生类都包含姓名、年龄等属性,考试类包含考试科目、考试成绩等属性,成绩类包含分数、等级等属性
-
如果这个时候去复制小学生的代码来实现中学生、大学生、研究生的程序,会发现很多代码是重复的,比如姓名、年龄、考试科目、考试成绩等属性和方法都是一样的
-
这时候就可以通过结构体嵌套来复用小学生的结构体,同时添加中学生、大学生、研究生特有的属性和方法
继承的实现语法
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 来访问字段和方法,执行流程如下:
-
首先检查 B 结构体是否有 Grade 字段,发现有,直接访问并设置 Grade 字段的值。
-
当访问 b.Name 时,B 结构体本身没有 Name 字段,Go 语言会继续查找嵌套的结构体 A,发现 A 结构体有 Name 字段,因此直接访问并设置 Name 字段的值。
-
当调用 b.setAge(20) 时,B 结构体本身没有 setAge 方法,Go 语言会继续查找嵌套的结构体 A,发现 A 结构体有 setAge 方法,因此直接调用该方法设置 age 字段的值。
-
当调用 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: 小红
}