Go语言面向对象编程

引言

l. 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等
  • 复合类型,如数组、结构体、指针等
  • 值语义和引用语义
  • 面向对象,即所有具备面向对象特征(比如成员方法)的类型
  • 接口

值语义和引用语义

  • 值语义:值类型
  • 引用语义:引用类型

结构体

基本语法

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进行处理,加上*,这是一个语法糖
  1. 结构体是自定义的数据类型,代表一类事物
  2. 结构体变量是具体的,实际地,代表一个具体变量
  3. 结构体的所有字段在内存中的分布是连续的
type Point struct {
	x int
	y int
}

type Rect1 struct {	// x, y四个int全部连续
	leftUp, rightDown Point
}

type Rect2 struct { // 指针变量内存连续
	leftUp, rightDown *Point
}
  1. 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个,结构体是值类型
  2. 结构体是用户单独定义的类型,和其他类型进行转换时需要有完全相同的字段
  3. 结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转
  4. struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用场景就是序列化和反序列化

方法

方法:即结构体的行为
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
}

抽象

将一个实物,抽象出其特征和行为,形成结构体,这个过程就是抽象

面向对象编程的三大特性

  • 封装
  • 继承
  • 多态

封装

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

封装的理解和好处

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

如何体现封装

  1. 对结构体中的属性进行封装
  2. 通过方法,包实现封装

封装的实现步骤

  1. 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)
  2. 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
  3. 提供一个首字母大写的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
", 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
", 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
", 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
", 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
	...
}

多重继承

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

type M struct {
	Name string
}

type N struct {
	ability string
}

type E struct {
	M
	N
}

接口

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

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

  1. 基本语法
    • 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低耦合的思想
    • 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++
    • 非侵入式接口:不需要显示的声明自己实现了哪个接口
  2. 接口赋值讨论:将对象实例赋值给接口
    假设我们定义一个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. 接口体现多态的两种形式
  • 多态参数:方法参数的体现
  1. 多态数组
  • 接口数组中,存放实现接口的变量

类型断言

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

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
", y, y)
	} else {
		fmt.Println("convert fail")
	}
	fmt.Println("继续执行....")
原文地址:https://www.cnblogs.com/lxlhelloworld/p/14286032.html