<Go>5 Go 面向 “对象” 编程
本文最后更新于:2023年6月27日 上午
5 Go 面向 “对象” 编程
Go 也支持面向对象编程(OOP),但并不是纯粹的面向对象语言。准确地讲,Go 语言支持面向对象编程特性
Go 语言没有类(class)。Go 语言的结构体(struct)与其他编程语言的类有同等地位。可以认为 Go 语言是通过 struct 来实现 OOP 特性的
Go 面向对象编程非常简洁,其去掉了 OOP 语言的继承、方法重载、构造函数、析构函数、隐藏的 this 指针等。Go 语言仍有 OOP 的继承、封装、多态的特性。但实现方式不同于其他 OOP 语言。
Go 语言的 OOP 很优雅,其本身就是语言类型系统的一部分,通过接口关联。耦合度低,也十分灵活。Go 语言中,面向接口编程是非常重要的特性
面向对象编程的步骤:声明(定义)结构体、编写结构体字段、编写结构体方法
5.1 结构体
将一类事物的特性提取出来,形成新的数据类型,就是一个结构体。通过这个结构体,可以创建多个变量。
#定义结构体
type Example struct {
A_Str string
A_Int int
}
结构体名、字段名的首字母大写时,该结构体、字段可以在其他包使用,否则只能在本包使用
#声明结构体变量
var e Example
e2 := Example{"e2 AStr", 2} // 此时字段初始化列表要么为空,要么全部初始化
e3 := Example{
A_Str: "e3 AStr",
}
ep := new(Example) // 此时 ep 是一个指针
结构体是 值类型,其赋值方式是 值拷贝。
结构体是 值类型,其赋值方式是 值拷贝。
结构体是 值类型,其赋值方式是 值拷贝。
5.1.1 字段
字段(属性,field)是结构体的一个组成部分。可以是基本数据类型,数组,也能是引用类型。字段的声明方法与变量相同。
创建结构体变量后,没有给字段赋值时,字段对应一个零值。
不同结构体变量的字段是互相独立,互不影响的。一个结构体变量字段的更改不会影响其他结构体变量
访问字段:结构体变量名.字段名
e.A_Int = 50
(*ep).A_Int = 100
ep.A_Int = -1 // 该写法是上面写法的简化
指针名.字段名
的写法等同于(*指针名).字段名
#使用细节
-
结构体的所有字段在内存中是连续分布的
-
结构体是用户单独定义的类型。与其他类型进行强制转换时,需要有完全相同的字段(包括字段名、字段个数、字段类型、顺序)
-
对结构体进行 type 重定义,Go 语言会认为是新的数据类型。但是可以进行强制转换
-
在结构体的每个字段上,可以写一个 tag。该 tag 可以通过反射机制获取。
常见场景:序列化、反序列化
type Examp struct { Name string `json:"name"` // json:"name" 就是 tag }
5.2 方法
在某些情况下,需要声明(定义)方法。
方法与函数不同。Go 中的方法与指定的数据类型绑定。
不仅是 struct,只要是自定义类型,都可以有方法。
#方法的声明
type A struct { // 一个自定义类型 A
str string
}
func (a A) act() { // 绑定类型 A 的一个方法
fmt.Println(a.str)
}
func main() {
s := A{"★"}
s.act() // 调用方法
}
#使用细节
-
方法必须被指定的数据类型的对象调用。不能直接调用,也不能被其他数据类型调用
-
方法名前的括号中是该方法的接收者。这个概念类似于 Java 的隐式参数。
方法调用时,将调用该方法的变量作为实参传递给方法作为接收者。
-
如果接收者是值类型,则接收者是值拷贝。否则是引用传递。
那结构体是值类型嘛,你懂的。如果希望修改结构体变量的值,可以通过指针方式
和函数不同,不论指针还是变量,都能直接调用方法。但接收者的类型只取决于声明方法时接收者的类型。
-
方法的访问范围控制与函数相同:首字母大写时才能在其他包访问
-
如果一个变量实现了
String() string
方法,则 fmt.Print 会默认调用该方法进行输出
#工厂模式
Golang 的结构体没有构造函数,通常使用工厂模式解决这个问题
type ast struct { // 一个自定义类型 ast
str string
}
func GetA(str string) *ast { // 相当于构造器
return &a{str}
}
fonc (a *ast) GetAStr() string { // getter
return a.str
}
5.3 接口
interface 类型可以定义一组方法,这些方法不需要实现。某个自定义类型需要使用时,再实现这些方法。接口体现了程序设计的 多态 和 高内聚低耦合 的思想。
在 Go 语言中,interface 不能包含任何变量,其方法都是没有实现的方法
在 Go 语言中,接口不需要显式的实现。只要一个变量实现了接口中的 所有 方法,那么该变量就实现这个接口。
#声明/定义接口
type I interface{ // 接口
method() int // 一个未实现的方法
}
type C struct {} // 不需要显式地实现接口
func (c C) method() int { // 实现接口方法
return 0
}
func main() {
var i I = A{}
fmt.Println(i.act())
}
#注意事项
-
接口不能创建实例,但可以指向一个实现了该接口的变量
-
接口的所有方法都没有实现
-
在 Go 语言中,一个自定义类型实现了某个接口的所有方法,就认为该类型实现了该接口
只有实现了一个接口,才能将其赋给该接口类型
通过指针类型实现方法时,只能把指针赋给接口
-
只要是自定义数据类型就能实现接口,而不仅仅是结构体
-
一个自定义类型可以实现多个接口
-
在 Go 语言中,接口不能拥有任何变量,但可以继承别的接口。
继承别的接口时,接口中不能包含同名方法。否则,会导致重定义
-
interface 类型是一个指针,其零值为 nil
-
空接口没有任何方法,所以所有类型都实现了空接口
5.4 面向对象
面向对象编程的三大特性:封装、继承、多态
5.4.1 封装
把抽象出的字段和对字段的操作封装在一起。数据被保护在内部,程序的其他包只有通过授权的操作(方法)才能对字段进行访问。
封装的好处:
- 隐藏实现的细节
- 对数据进行验证,保证安全合理
实现封装的方法:
-
对结构体的属性进行封装
将结构体、字段的首字母小写
-
通过方法,包实现封装
提供一个工厂模式的函数,首字母大写,类似于构造函数
提供首字母大写的 Get 和 Set 方法,用以获取属性值或为属性赋值
在 Go 语言中并不特别强调封装。
5.4.2 继承
继承能解决代码复用的问题,让编程更靠近人类思维
当多个结构体存在相同的字段和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法。其他结构体不需要重新定义字段和方法,只需要嵌套一个匿名结构体即可。
在 Go 中,如果一个结构体嵌套了另一结构体,则该结构体能访问匿名结构体的字段和方法,从而实现了继承特性。
继承的好处:
- 提高代码复用性
- 提高代码维护性、可扩展性
#语法
type Father struct {
str string
}
type Son struct {
Father // 嵌套匿名结构体
}
嵌入了匿名结构体后,使用方式会发生变化
s := Son{}
fmt.Println(s.Father.str) // 使用匿名构造体的字段
fmt.Println(s.str) // 访问匿名结构体字段可以简化
#注意事项
-
结构体能使用嵌套匿名结构体的所有字段和方法
-
访问匿名结构体字段可以简化。
直接通过字段构造体对象访问字段时,编译器会先查看结构体本身是否有该字段。
如果没有,会进而对内嵌匿名结构体进行查找。都找不到时会报错。
结构体嵌入多个匿名结构体,且当前嵌套层级的匿名结构体拥有相同字段或方法时,必须指定匿名结构体的名字,以消除二义性。否则报错
-
一个结构体嵌套了一个有名结构体时,这种模式就是 组合。组合关系中,访问组合结构体字段时必须带上该有名结构体的名字
-
嵌套匿名结构体后,创建构造体实例时,也能直接指定那些匿名构造体字段的值
-
继承与接口的区别
-
继承:继承其他结构体后,自动获得其所有字段和方法,并可以直接使用
继承的价值在于解决代码的复用性和可维护性
-
接口:希望扩展功能,又不想破坏继承关系时,就可以去实现接口。接口是对继承的补充
接口的价值在于设计号各种规范,让其他自定义类型实现这些方法。接口在一定程度上实现了代码解耦
-
5.4.3 多态
多态:变量(实例)具有多种形态。
在 Go 语言中,多态特征是通过接口实现的。通过统一的接口调用不同的实现,此时接口就呈现不同的形态。
#类型断言
由于接口是一般类型,不知道具体类型。要转成具体类型时,需要类型断言
type I interface{}
func main() {
var i I = 10
var t int
/* t = i */ // 不允许这样赋值
t = i.(T) // 进行类型断言。此时如果类型不匹配,会导致 panic
}
进行类型断言时,如果实际类型不匹配,会报 panic。应该确保指向的就是断言类型
为避免上述情况,在类型断言时,应该带上检测机制:
var i I = 10
if t, done := i.(int); done {
fmt.Println(t)
} else {
fmt.Println("Failed")
}
或者也可以这样:
var i I = 10
if t := i.(type); t == int {
fmt.Println("i == int")
} else if t == float32 {
fmt.Println("i == float")
}