Go语言面向对象编程

引言

  1. Golang也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang支持面向对象编程特性是比较准确的。
  2. Golang没有类(class),Go语言的结构体( struct)和其它编程语言的类class有同等的地位,你可以理解 Golang是基于 struct来实现OOP特性的。
  3. Golang面向对象编程非常简洁,去掉了传统OOP语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等等
  4. Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不样,比如继承: Golang没有 extends关键字,继承是通过匿名字段来实现。
  5. Golang面向对象(OOP)很优雅,OOP本身就是语言类型系统( type system)的一部分,通过接口( interface)关联,耦合性低,也非常灵活。后面同学们会充分体会到这个特点。也就是说在 Golang中面向接口编程是非常重要的特性。

类型系统

类型系统是指一个语言的类型体系结构。一个典型的类型系统通常包含如下基本内容:

  • 基础类型:int,bool,float等
  • 复合类型,如数组、结构体、指针等
  • 值语义和引用语义
  • 面向对象,即所有具备面向对象特征(比如成员方法)的类型
  • 接口

类型系统描述的是这些内容在一个语言中如何被关联。因为Java语言自诞生以来被称为最纯
正的面向对象语言,所以我们就先以Java语言为例讲一讲类型系统。

在Java语言中,存在两套完全独立的类型系统:一套是值类型系统,主要是基本类型,如byte、int、boolean、char、double等,这些类型基于值语义;一套是以Object类型为根的对象类型系统,这些类型可以定义成员变量和成员方法,可以有虚函数,基于引用语义,只允许在堆上创建(通过使用关键字new)

相比之下,Go语言中的大多数类型都是值语义,并且都可以包含对应的操作方法。在需要的时候,你可以给任何类型(包括内置类型)“增加”新方法。而在实现某个接口时,无需从该接口继承(事实上,Go语言根本就不支持面向对象思想中的继承语法),只需要实现该接口要求的所有方法即可。任何类型都可以被Any类型引用。Any类型就是空接口,即interface{}。

为类型添加方法

在Go语言中,你可以给任意类型(包括内置类型,但不包括指针类型)添加相应的方法,
例如:

type Integer int

func (a Integer) Less(b Integer) bool {
	return a < b
}

func main() {
	var a Integer = 1
	if a.Less(2) {
		fmt.Println(a, "Less 2")
	}
}

值语义和引用语义

值语义和引用语义的差别在于赋值,比如下面的例子:

b = a
b.Modify()

如果b的修改不会影响a的值,那么此类型属于值类型。如果会影响a的值,那么此类型是引用类型。

Go语言中的大多数类型都基于值语义,包括:

  • 基本类型,如byte、int、bool、float32、float64和string等;
  • 复合类型,如数组(array)、结构体(struct)和指针(pointer)等。
    Go语言中类型的值语义表现得非常彻底。我们之所以这么说,是因为数组。

Go语言中有4个类型比较特别,引用类型

  • 数组切片:指向数组(array)的一个区间。
  • map:极其常见的数据结构,提供键值查询能力。
  • channel:执行体(goroutine)间的通信设施。
  • 接口(interface):对一组满足某个契约的类型的抽象。

结构体

Go语言的结构体(struct)和其他语言的类(class)有同等的地位,但Go语言放弃了包括继
承在内的大量面向对象特性,只保留了组合(composition)这个最基础的特性。

tag

初始化和定义

type 结构体名称 struct {
    field1 type
    field2 type
}

type Cat struct {
	Name string
	Age int
	Color string
	Hobby string
}


// 创建结构体变量的方式
var cat Cat
var cat Cat = Cat{"中分", 3, "黑白", "吃鱼"} // 必须字段顺序对应
var cat Cat = Cat{Name : "中分", Age : 3, Color : "黑白", Hobby : "吃鱼"} // 字段顺序可以不对应
var cat *Cat = new(Cat) // 返回的结构体指针
var cat *Cat = &Cat{}   // 返回结构体指针

// 结构体指针的调用方式
(*cat).Name = "杜甫"  <-----> cat.Name = "杜甫" // 两个等价,因为go设计者为了程序员使用方便,底层会对cat.Name进行处理,加上*,这是一个语法糖

方法

方法:即结构体的行为,Golang 中的方法是作用在指定的数据类型上的,因此自定义类型,都可以有方法,而不仅仅是struct

// 声明
func (recevier type) methodName (参数列表) (返回值列表) {
	方法体
	return 返回值 // 不是必须的
}

type A struct {
	Num int
}

func (a A) test0(){  // a 是副本
	...
}

func (a *A) test1(){ // a 是结构体指针,结构体变量调用的时候,会传递地址给a
	...
}

  1. 方法只能通过结构体变量来调用
  2. 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
  3. 如果希望能在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
  4. Golang中的方法作用在指定的数据类型上的(即:和指定数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct
  5. 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其他包访问
  6. 如果一个类型实现了String()这个方法,那么fmt.Println默认会调用这个变量的String()进行输出

方法与函数的区别

调用方式不一样

  1. 函数的调用方式:函数名(实参列表)
    方法的调用方式:变量.方法名(实参列表)

  2. 对应普通函数,接收者为值类型,不能将指针类型的数据直接传递

构造函数(工厂模式)

如果结构体的首字母是大写的,那么我们可以在其他包访问,并且创建它的变量,但是如果结构体首字母是小写,其他包访问不了,就需要用工厂模式来创建变量

type student struct {
	Name string
	Age  int
}

func NewStudent(name string, age int) *student {
	return &student{
		name,
		age,
	}
}

Getter && Setter

如果结构体的字段是小写的,则其他包就不能正常访问,应该借助Getter && Setter

type student struct {
	name string
	age  int
}

func (stu *student) GetName() string {
	return stu.name
}

func (stu *student) SetName(name string) {
	stu.name = name
}

func (stu *student) GetAge() int {
	return stu.age
}

func (stu *student) SetAge(age int) {
	stu.age = age
}

可见性

Go语言对关键字的增加非常吝啬,其中没有private、protected、public这样的关键
字。要使某个符号对其他包(package)可见(即可以访问),需要将该符号定义为以大写字母
开头

需要注意的一点是,Go语言中符号的可访问性是包一级的而不是类型一级的。在上面的例
子中,尽管area()是Rect的内部方法,但同一个包中的其他类型也都可以访问到它

面向对象编程的三大特性

  • 封装
  • 继承
  • 多态

封装

封装就是把抽象出来的字段和对字段的操作封装在一起,数据被保护在内部,程序的其他包只有通过被授权的操作(方法),才能对字段进行操作

封装的理解和好处

  1. 隐藏实现细节
  2. 可以对数据进行雁阵个,保证安全合理

如何体现封装
3. 对结构体中的属性进行封装
4. 通过方法,包实现封装

封装的实现步骤
5. 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)
6. 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
7. 提供一个首字母大写的Set方法(类似其它语言的 public,用于对属性判断并赋值

继承

当多个结构体存在相同的数字那个和方法时,可以从这些结构体中抽向出结构体,在该结构体中定义这些相同的属性和方法,可以成为父结构体其他的结构体不需要重新定义这些属性和方法,只需嵌套一个父结构体的匿名结构体即可

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

type Goods struct {
	Name string
	Price int
}

type Book struct {
	Goods
	Writer string
}

  1. 结构体可以使用嵌套匿名结构体所有字段和方法,即:首字母大写或者小写的字段、方法都可以使用
  2. 匿名结构体字段可以简化
  3. 当结构体和匿名子结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如果希望访问匿名结构体的字段和方法,可以通过匿名结构体来区分
  4. 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和犯法),在访问时,就必须明确自定匿名结构体名字,否则编译报错
  5. 如果一个struct嵌套了一个有名结构体,这种模式就是组合。如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
  6. 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
// 含匿名结构体的初始化
type Address struct {
	province string
	city string
}
 
type User struct {
	name string
	age int
	Address
}
 
// 方法一:正常直观方式定义
u1 := &User{
	name: "Ming",
	age: 30,
	Address: Address{
		province: "Jiangsu",
		city: "Nanjing",
	},
}
fmt.Printf("%+v\n", u1)  // &{name:Ming age:30 Address:{province:Jiangsu city:Nanjing}}
 
// 同上
var u2 User
u2.name = "Qiang"
u2.age = 35
u2.Address = Address{province: "Jiangsu", city: "Suzhou"}
fmt.Printf("%+v\n", u2)  // {name:Qiang age:35 Address:{province:Jiangsu city:Suzhou}}
 
// 方法二:匿名嵌入时可以直接访问叶子属性而不需要给出完整的路径,也可以给出完整路径
var u3 User
u3.name = "A"
u3.age = 40
u3.province = "Jiangsu"
u3.city = "Wuxi"
fmt.Printf("%+v\n", u3)  // {name:A age:40 Address:{province:Jiangsu city:Wuxi}}
 
// 但下面的方式是错误的,编译不能通过
// cannot use promoted field Address.province in struct literal of type User
// cannot use promoted field Address.city in struct literal of type User
u4 := User{
	name: "A",
	age: 29,
	province: "Jiangsu",
	city: "Wuxi",
}
fmt.Printf("%+v\n", u4)

  1. 结构体内不仅仅可以嵌套结构体,还可以嵌套int,float64等
// 如果一个结构体有int类型的匿名字段,就不能有第二个
// 如果需要有多个int类型字段,则必须给int字段指定名字
type M struct {
	Name string
}

type E struct {
	M
	int
	n int
}

func main() {
	var e E
	e.Name = "狐狸精"
	e.int = 20
	e.n = 40
	fmt.Println(e)
}

  1. 以指针方式从一个类型“派生”
    这段Go代码仍然有“派生”的效果,只是Foo创建实例的时候,需要外部提供一个Base类实例的指针。
type Foo struct {
	*Base
	...
}

  1. 多重继承

如果一个struct嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承

type M struct {
	Name string
}

type N struct {
	ability string
}

type E struct {
	M
	N
}

多态

讲多态之前先讲接口,因为Golang中的多态一般体现在接口的实现上

interface类型可以定义一组方法,但是这些不需要实现,并且interface不能包含任何变量。到某一个自定义类型要使用的时候,在根据具体情况把这些方法实现

基本语法

  • 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低耦合的思想
  • Golang中的接口,不需要显示的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有implement这样的关键字
type interfaceName interface {
	method1(参数列表) 返回值列表
	method2(参数列表) 返回值列表
	...
}


func (t 自定义类型) method1(参数列表) 返回值列表{}
func (t 自定义类型) method2(参数列表) 返回值列表{}

接口注意事项与细节

  1. 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量
type AInterface interface {
	Say()
}

type Stu struct {
	Name string
}

func (stu Stu) Say() {
	fmt.Println("Stu Say(")
}

func main() {

	var stu Stu
	var a AInterface = stu
	a.Say()
}
  1. 接口中所有的方法都没有方法体,即都是没有实现的方法
  2. 在Golang中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口
type AInterface interface {
	Say()
	Call()
}

type Stu struct {
	Name string
}

func (stu Stu) Say() {
	fmt.Println("Stu Say(")
}

func main() {

	var stu Stu
	var a AInterface = stu  // 报错
	a.Say()
}
  1. 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
  2. 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型
type AInterface interface {
	Say()
}

type intteger int

func (i intteger) Say() {
	fmt.Println("integer Say i = ", i)
}

  1. 一个自定义类型可以实现多个接口
  2. Golang接口中不能有任何变量
  3. 一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必须将B,C接口的方法也全部实现
type A interface {
	test01()
}

type B interface {
	test02()
}

type C interface {
	A
	B
	test03()
}

type Stu struct {

}

func (stu Stu) test01() {

}

func (stu Stu) test02() {
	
}

func (stu Stu) test03() {
	
}

func main() {

	var stu Stu
	var a A = stu
	a.test01()
	
}

  1. interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil
  2. 空接口 interface{} 没有任何方法,所有类型都实现了空接口,即我们可以把任何一个变量赋给空接口
  3. 结构体指针绑定接口
type A interface {
	test01()
}

type AI struct {
}

func (this *AI) test01() {
	fmt.Println("test01")
}

func main() {

	var a A = &AI{} // 如果这里没有&,则会报错
	a.test01()
	fmt.Println(a)

}

  1. 侵入式接口和非侵入式接口的区别

侵入式接口:主要表现在于实现类需要明确声明自己实现了某个接口。向Java、C++
非侵入式接口:不需要显示的声明自己实现了哪个接口。如Golang

  1. 接口赋值讨论:将对象实例赋值给接口
    假设我们定义一个Integer类型的对象实例,代码如下,怎么将其赋值给LessAdder接口呢?应该用下面的语句(1),还是语句(2)呢?
type LessAdder interface {
	Less(b Integer) bool
	Add(b Integer)
}

type Integer int

func (a Integer) Less(b Integer) bool {
	return a < b
}

func (a * Integer) Add(b Integer){
	*a += b
}

func main() {
	var a Integer = 1
	var b LessAdder = &a	... (1)
	var c LessAdder = a		... (2)
}

答案是应该用语句(1)。
原因在于, Go语言可以根据下面的函数:func (a Integer) Less(b Integer) bool自动生成一个新的Less()方法:

func (a *Integer) Less(b Integer) bool {
	return (*a).Less(b)
}

类型*Integer就既存在Less()方法,也存在Add()方法,满足LessAdder接口。而从另一方面来说,根据func (a *Integer) Add(b Integer)这个函数无法自动生成以下这个成员方法:

func (a Integer) Add(b Integer) {
	(&a).Add(b)
}

因为(&a).Add()改变的只是函数参数a,对外部实际要操作的对象并无影响,这不符合用户的预期。所以, Go语言不会自动为其生成该函数。因此,类型Integer只存在Less()方法,缺少Add()方法,不满足LessAdder接口,故此上面的语句(2)不能赋值。

  1. 接口赋值讨论:接口赋值给另一个接口
    在Go语言中,只要两个接口拥有相同的方法列表(次序不同不要紧),那么它们就是等同的,可以相互赋值。
package one
type ReadWriter interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
}

/
package two
type IStream interface {
Write(buf []byte) (n int, err error)
Read(buf []byte) (n int, err error)
}

// 下面代码都通过
var file1 two.IStream = new(File)
var file2 one.ReadWriter = file1
var file3 two.IStream = file2

接口赋值并不要求两个接口必须等价。如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A。

type Writer interface {
Write(buf []byte) (n int, err error)
}

就可以将上面的one.ReadWriter和two.IStream接口的实例赋值给Writer接口:

var file1 two.IStream = new(File)
var file4 Writer = file1

但是反过来并不成立:

var file1 Writer = new(File)
var file5 two.IStream = file1 // 编译不能通过

这段代码无法编译通过,原因是显然的: file1并没有Read()方法。

  1. 类型断言: 接口查询
var file1 Writer = ...
if file5, ok := file1.(two.IStream); ok {
...
}

  1. 类型断言:类型查询
    在Go语言中,还可以更加直截了当地询问接口指向的对象实例的类型,例如:
var v1 interface{} = ...
	switch v := v1.(type) {
	case int: // 现在v的类型是int
	case string: // 现在v的类型是string
	...
}

对于内置类型, Println()采用穷举法,将每个类型转换为字符串进行打印。对于更一般的情况,首先确定该类型是否实现了String()方法,如果实现了,则用String()方法将其转换为字符串进行打印。

  1. 接口也支持继承
  2. Any类型:interface{},可以指向任何对象

多态
变量具有多种形态,面向对象的第三大特诊,在Go语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。可以按照统一的接口来电泳不同的实现。这时接口变量就呈现不同的形态

  1. 接口体现多态的两种形式,多态参数:方法参数的体现
  2. 多态数组,接口数组中,存放实现接口的变量

类型断言

类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言

var x interface{}
	var b2 float32 = 1.1
	x = b2	// 空接口,可以接受任意类型
	// x => float32 [使用类型断言]
	y := x.(float32)	// 这里如果不是float32就会panic
	fmt.Printf("y 的类型是 %T 值是 %v", y, y)

代码说明:在进行类型断言时,如果类型不匹配,就会报 panic,因此进行类型断言时,要确保原来的空接口指向的就是断言的类型

var x interface{}
	var b2 float32 = 1.1
	x = b2	// 空接口,可以接受任意类型
	// x => float32 [使用类型断言]

	if y, ok := x.(float32); ok { // 优雅的类型断言
		fmt.Println("convert success")
		fmt.Printf("y 的类型是 %T 值是 %v\n", y, y)
	} else {
		fmt.Println("convert fail")
	}
	fmt.Println("继续执行....")