<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")
}

<Go>5 Go 面向 “对象” 编程
https://i-melody.github.io/2022/06/19/Go/5 Go 面向对象编程/
作者
Melody
发布于
2022年6月19日
许可协议