Go语言学习之路第7天(面向对象)

一.面向对象

  (1)面向对象与面向过程的区别

  面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了;面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。

  可以拿生活中的实例来理解面向过程与面向对象,例如五子棋,面向过程的设计思路就是首先分析问题的步骤:1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。把上面每个步骤用不同的方法来实现。

  如果是面向对象的设计思想来解决问题。面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为1、黑白双方,这两方的行为是一模一样的,2、棋盘系统,负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。

  可以明显地看出,面向对象是以功能来划分问题,而不是步骤。同样是绘制棋局,这样的行为在面向过程的设计中分散在了多个步骤中,很可能出现不同的绘制版本,因为通常设计人员会考虑到实际情况进行各种各样的简化。而面向对象的设计中,绘图只可能在棋盘对象中出现,从而保证了绘图的统一。
  总结下来就两句话:面向对象就是高度实物抽象化、面向过程就是自顶向下的编程!

 

  (2)面向对象的特点

  在了解其特点之前,咱们先谈谈对象,对象就是现实世界存在的任何事务都可以称之为对象,有着自己独特的个性

  属性用来描述具体某个对象的特征。比如小志身高180M,体重70KG,这里身高、体重都是属性。
  面向对象的思想就是把一切都看成对象,而对象一般都由属性+方法组成!

  属性属于对象静态的一面,用来形容对象的一些特性,方法属于对象动态的一面,咱们举一个例子,小明会跑,会说话,跑、说话这些行为就是对象的方法!所以为动态的一面, 我们把属性和方法称为这个对象的成员!

  类:具有同种属性的对象称为类,是个抽象的概念。比如“人”就是一类,期中有一些人名,比如小明、小红、小玲等等这些都是对象,类就相当于一个模具,他定义了它所包含的全体对象的公共特征和功能,对象就是类的一个实例化,小明就是人的一个实例化!我们在做程序的时候,经常要将一个变量实例化,就是这个原理!我们一般在做程序的时候一般都不用类名的,比如我们在叫小明的时候,不会喊“人,你干嘛呢!”而是说的是“小明,你在干嘛呢!”

  面向对象有三大特性,分别是封装性、继承性和多态性。

  (3)面向过程与面向对象的优缺点

  用面向过程的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭。所谓盖浇饭,北京叫盖饭,东北叫烩饭,广东叫碟头饭,就是在一碗白米饭上面浇上一份盖菜,你喜欢什么菜,你就浇上什么菜。我觉得这个比喻还是比较贴切的。

  蛋炒饭制作的细节,我不太清楚,因为我没当过厨师,也不会做饭,但最后的一道工序肯定是把米饭和鸡蛋混在一起炒匀。盖浇饭呢,则是把米饭和盖菜分别做好,你如果要一份红烧肉盖饭呢,就给你浇一份红烧肉;如果要一份青椒土豆盖浇饭,就给浇一份青椒土豆丝。

  蛋炒饭的好处就是入味均匀,吃起来香。如果恰巧你不爱吃鸡蛋,只爱吃青菜的话,那么唯一的办法就是全部倒掉,重新做一份青菜炒饭了。盖浇饭就没这么多麻烦,你只需要把上面的盖菜拨掉,更换一份盖菜就可以了。盖浇饭的缺点是入味不均,可能没有蛋炒饭那么香。

  到底是蛋炒饭好还是盖浇饭好呢?其实这类问题都很难回答,非要比个上下高低的话,就必须设定一个场景,否则只能说是各有所长。如果大家都不是美食家,没那么多讲究,那么从饭馆角度来讲的话,做盖浇饭显然比蛋炒饭更有优势,他可以组合出来任意多的组合,而且不会浪费。

  盖浇饭的好处就是”菜”“饭”分离,从而提高了制作盖浇饭的灵活性。饭不满意就换饭,菜不满意换菜。用软件工程的专业术语就是”可维护性“比较好,”饭” 和”菜”的耦合度比较低。蛋炒饭将”蛋”“饭”搅和在一起,想换”蛋”“饭”中任何一种都很困难,耦合度很高,以至于”可维护性”比较差。软件工程追求的目标之一就是可维护性,可维护性主要表现在3个方面:可理解性、可测试性和可修改性。面向对象的好处之一就是显著的改善了软件系统的可维护性。

  简单的总结一下!

  面向过程:

优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。 
缺点:没有面向对象易维护、易复用、易扩展

  面向对象:

优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护 
缺点:性能比面向过程低

(上面这些是我在网上看到一哥们写的,觉得写的很不错就拷贝过来了,他博文地址是:https://blog.csdn.net/jerry11112/article/details/79027834)

  (4)GO语言中的面向对象

  前面我们了解了一下,什么是面向对象,以及类和对象的概念。但是,GO语言中的面向对象在某些概念上和其它的编程语言还是有差别的。

  严格意义上说,GO语言中没有类(class)的概念,但是我们可以将结构体比作为类,因为在结构体中可以添加属性(成员),方法(函数)。

//结构体:类,结构体中的成员变量:类属性
type Student struct {
	Id int
	name string
	age int
	sex string
	addr string
}

   类的实例化产生类对象

func main() {
	//借助类,实例化后,生成类对象
	stu := Student{1001, "张三", 26, "M", "北京"}
	fmt.Println(stu)
}

  Go语言中实现面向对象的封装,继承,多态的方式分别为:方法,匿名字段和接口。

1.1 匿名字段

  所谓继承指的是,我们可能会在一些类(结构体)中,写一些重复的成员,我们可以将这些重复的成员,单独的封装到一个类(结构体)中,作为这些类的父类(结构体),我们可以通过如下图来理解:   

  根据上面的图,我们发现学生类(结构体),讲师类(结构体)等都有共同的成员(属性和方法),这样就存在重复,所以我们把这些重复的成员封装到一个父类(结构体)中,然后让学生类(结构体)和讲师类(结构体)继承父类(结构体)。

  接下来,我们可以先将公共的属性,封装到父类(结构体)中实现继承,关于方法(函数)的继承后面再讲。

  (1)匿名字段创建与初始化

  那么怎样实现属性的继承呢?

  可以通过匿名字段(也叫匿名组合)来实现,什么是匿名字段呢?通过如下使用,大家就明白了。

type Person struct {
	id   int
	name string
	age  int
}

type Student struct {
	Person  //匿名字段
	score float64
}

  以上代码通过匿名字段实现了继承,将公共的属性封装在Person中,在Student中直接包含Person,那么Student中就有了Person中所有的成员,Person就是匿名字段。注意:Person匿名字段,只有类型,没有名字。

  那么接下来说我们就可以给Student赋值了,具体初始化的方式如下:

func main() {
	var stu Student = Student{Person{1001, "李四", 27}, 99.5}
	fmt.Println("stu = ",stu)
}

  以上代码中创建了一个结构体变量stu,这个stu我们可以理解为就是Student对象,但是要注意语法格式,以下的写法是错误的:

var stu Student = Student{1001,"李四",27,99.5}

  其它初始化方式如下:

  自动推导类型:

//自动推导类型
stu := Student{Person{1001, "张三", 27}, 98.5}
//%+v:显示更加详细
fmt.Printf("stu = %+v\n", stu)

  制定初始化成员:

//指定成员初始化,没有初始化的整型自动赋值为0,字符串为空
stu := Student{score: 99.7}
fmt.Printf("stu = %+v\n", stu)

接下来还可以对Person中指定的成员进行初始化:

//指定成员初始化,没有初始化的整型自动赋值为0,字符串为空
stu := Student{Person{name: "李四"}, 97}
fmt.Printf("stu = %+v\n", stu)

  (2)成员操作

  创建完成对象后,可以根据对象来操作对应成员属性,是通过"."运算符来完成操作的。具体案例如下:

func main() {
	//指定成员初始化,没有初始化的整型自动赋值为0,字符串为空
	stu := Student{Person{1001, "张三", 25}, 96}
	stu.score = 98
	stu.Person.id = 1002
	stu.age = 30
	fmt.Printf("stu:%+v\n", stu)
}

  由于Student继承了Person,所以Person具有的成员,Student也有,所以根据Student创建出的对象可以直接对age成员项进行修改。

  由于在Student中添加了匿名字段Person,所以对象s1,也可以通过匿名字段Person来获取age,进行修改。

  当然也可以进行如下修改:

func main() {
	//指定成员初始化,没有初始化的整型自动赋值为0,字符串为空
	stu := Student{Person{1001, "张三", 25}, 96}
	stu.Person = Person{1002, "李四", 30}
	fmt.Printf("stu:%+v\n", stu)
}

  直接给对象stu中的Person成员(匿名字段)赋值。

  通过以上案例我们可以总结出,根据类(结构体)可以创建出很多的对象,这些对象的成员(属性)是一样的,但是成员(属性)的值是可以完全不一样的。

 

  (3)同名字段

  现在将Student结构体与Person结构体,进行如下的修改:

type Person struct {
	id   int
	name string
	age  int
}

type Student struct {
	Person //匿名字段
	name   string  //与父类Person中的属性同名
	score  float64
}

  在Student中也加入了一个成员name,这样与Person重名了,那么如下代码是给Student中name赋值还是给Person中的name 进行赋值?

func main() {
	var stu Student
	stu.name = "李四"
	fmt.Printf("stu:%+v\n", stu)
}

  输出结构如下:

stu: {Person:{id:0 name: age:0} name:李四 score:0}

  通过结果发现是对Student中的name进行赋值。在子类属性中,包含与父类相同的属性时,创建子类时,不会覆盖父类。在操作同名字段时,有一个基本的原则:如果能够在自己对象所属的类(结构体)中找到对应的成员,那么直接进行操作,如果找不到就去对应的父类(结构体)中查找。这就是所谓的就近原则。

  (4) 指针匿名字段

  结构体(类)中的匿名字段的类型,也可以是指针:

  例如:

type Person struct {
	id   int
	name string
	age  int
}

type Student struct {
	*Person //指针类型匿名字段
	score   float64
}

func main() {
	stu := Student{&Person{1001, "李四", 27}, 95}
	fmt.Println(stu)
}

  得到的结果如下:

{0xc00000c080 95}

  输出了结构体的地址。如果要取值,可以进行如下操作:

func main() {
	stu := Student{&Person{1001, "李四", 27}, 95}
	fmt.Println(stu.id, stu.name, stu.age, stu.score)
}

  在定义对象stu时,完成初始化,然后通过"."的操作完成成员的操作。

  但是,注意以下的写法是错误的:

type Person struct {
	id   int
	name string
	age  int
}

type Student struct {
	*Person //指针类型匿名字段
	score   float64
}

func main() {
	var stu Student
	stu.id = 1002
	stu.name = "李四"
	stu.age = 27
	stu.score = 96
	fmt.Printf("stu: %+v\n", stu)
}

  大家可以思考一下,以上代码为什么会出错?

  会出错,错误信息如下:

  invalid memory address or nil pointer dereference

  翻译成中文:无效的内存地址或nil指针引用

  意思是*Person没有指向任何的内存地址,那么其默认值为nil.

  也就是指针类型匿名字段*Person没有指向任何一个结构体,所以对象s也就无法操作Person中的成员。

  具体的解决办法如下:

func main() {
	var stu Student
	stu.Person = new(Person)  //使用new分配空间
	stu.id = 1002
	stu.name = "李四"
	stu.age = 27
	stu.score = 96
	fmt.Println(stu.id, stu.name, stu.age, stu.score)
}

  new( )的作用是分配空间,new( )函数的参数是一个类型,这里为Person结构体类型,返回值为指针类型,所以赋值给*Person,这样*Person也就指向了结构体Person的内存。

  (5)多重继承

  在上面的案例,Student类(结构体)继承了Person类(结构体),那么Person是否可以在继承别的类(结构体)呢,或者Student也继承了别的类(结构体)呢?

  可以,这就是多重继承。

  多重继承有两种继承方式:

  1.C——B——A(C继承B,B继承A)

  具体案例如下:

type Object struct {
	id   int
	flag bool
}

type Person struct {
	*Object
	name string
	age  int
}

type Student struct {
	*Person
	name  string //与Person中的属性同名
	score float64
}

  接下来,看一下怎样对多重继承中的成员进行操作:

func main() {
	var stu Student
	stu.Person = new(Person)
	stu.Object = new(Object)
	stu.name = "张三"
	stu.Person.name = "张老三"
	stu.Person.Object.id = 1003
	fmt.Println(stu.Person.Object.id, stu.Person.name, stu.name)
}

  (2)C——B同时C——A(C继承B,同时C继承A)

  具体案例如下:

type Person struct {
	id int
	name string
	age  int
}

type Address struct {
	addr string
} 

type Student struct {
	*Person
	*Address
	name  string //与Person中的属性同名
	score float64
}

  接下来,看一下怎样对多重继承中的成员进行操作:

func main() {
	var stu Student
	stu.Person = new(Person)
	stu.Address = new(Address)
	stu.name = "李四"
	stu.Person.name = "李老四"
	stu.Address.addr = "北京"
	fmt.Println(stu.name,stu.Person.name,stu.Address.addr)
}

  注意:多重继承,很容易出现同名字段,可以使用实名字段解决。 应该在程序中,尽量避免使用多重继承。

1.2 方法

  通过上边内容的讲解,大家能够体会出面向对象编程中继承的优势了,接下来会给大家介绍面向对象编程中另外的特性:封装性,其实关于封装性,在前面的编程中,大家也已经能够体会到了,就是通过函数来实现封装性。

  大家仔细回忆一下,当初在讲解函数时,重点强调了函数的作用,就是将重复的代码封装来,用的时候,直接调用就可以了,不需要每次都写一遍,这就是封装的优势。(超级玛丽案例)

  在面向对象编程中,也有封装的特性。面向对象中是通过方法来实现。下面,将详细的给大家讲解一下方法的内容。

  (1)基本方法创建

  在介绍面向对象时,讲过可以通过属性和方法(函数)来描述对象。

  那什么是方法呢?

  方法,大家可以理解成就是函数,但是在定义使用方面与前面讲解的函数还是有区别的。

  我们先定义一个传统的函数:

func Test(a, b int) int {
	return a + b
}

func main() {
	result := Test(1, 2)
	fmt.Println(result)
}

  这个函数非常简单,下面定义一个方法,看一下在语法与传统的函数有什么区别:

  方法的定义:

//为int类型定义别名
type Integer int

//为Integer绑定方法Test
func (a Integer) Test(b Integer) Integer {
	return a + b
}

func main() {
	//定义一个Integer类型变量
	var result Integer = 3
	r := result.Test(4)
	fmt.Println(r)
}

  type Integer int :表示的意思是给int类型指定了一个别名叫Integer,别名可以随便起,只要符合GO语言的命名规则就可以。

  指定别名后,后面可以用Integer来代替int 来使用。

 

  func (a Integer) Test(b Integer) Integer{
  }

  表示定义了一个方法,方法的定义与函数的区别:

  第一:在关键字后面加上( a Integer), 这个在方法中称之为接收者,所谓的接受者就是接收传递过来的第一个参数,然后复制a,a的类型是Integer,由于Integer是int的别名,所以a的类型为int。

  第二:在表示参数的类型时,都使用了对应的别名。

  通过方法的定义,可以看出方法其实就是给某个类型绑定的函数。在该案例中,是为整型绑定的函数,只不过在给整型绑定函数(方法)时,一定要通过type来指定一个别名,因为int类型是系统已经规定好了,无法直接绑定函数,所以只能通过别名的方式。

  第三:调用方式不同

  var result Interger=3

  表示定义一个整型变量result,并赋值为3.

  result.Test(3)

  通过result变量,完成方法的调用。因为,Test( )方法,是为int类型绑定的函数,而result变量为int类型。所以可以调用Test( )方法。result变量的值会传递给Test( )方法的接受者,也就是参数a,而实参Test(3),会传递形参b.

  当然,我们也可以将Test( )方法,理解成是为int类型扩展了,追加了的方法。因为系统在int类型时,是没有该方法的。

  在以上案例中,Test( )方法是为int类型绑定的函数,所以任何一个整型变量,都可以调用该方法。

var sum Integer = 6
r := sum.Test(10)
fmt.Println(r)

  (2)给结构体添加方法

  上面给整型创建了一个方法,那么直接通过整型变量加上"点",就可以调用该方法了。

  大家想一下,如果给结构体(类)加上了方法,那么根据结构体(类)创建完成对象后,是不是就可以通过对象加上"点",就可以完成方法的调用,这与调用类中定义的属性的方式是完全一样的。这样就完成了通过方法与属性来描述一个对象的操作。

  给结构体添加方法,语法如下:

type Student struct {
	id    int
	name  string
	age   int
	score float64
}

//将方法绑定在指定的结构体(类)上
func (stu Student) PrintInfo() {
	fmt.Printf("stu: %+v\n", stu)
}

func main() {
	stu := Student{1001, "李四", 26, 97}
	//定义完对象后,调用方法
	stu.PrintInfo()
}

  给结构体添加方法的方式与前面给int类型添加方法的方式,基本一致。唯一不同的是,不需要给结构体指定别名,因为结构体Student就是相当于其所有成员属性的别名(id,name,score),所以这里不要在给结构体Student创建别名。

  调用方式:根据结构体(类)创建的对象,完成了方法的调用。

  PrintInfo( )方法的作用,只是将结构体的成员(属性)值打印出来,如果要修改其对应的值,应该怎么做呢?

  由于结构体是值传递,所以必须通过指针来修改,所以要将方法的接收者,修改成对应的指针类型。

  具体修改如下:

type Student struct {
	id    int
	name  string
	age   int
	score float64
}

//将方法绑定在指定的结构体(类)上
func (stu Student) PrintInfo() {
	fmt.Printf("stu: %+v\n", stu)
}

func (stu *Student) EditInfo(new_id int, new_name string, new_age int, new_score float64) {
	stu.id = new_id
	stu.name = new_name
	stu.age = new_age
	stu.score = new_score
}
func main() {
	stu := &Student{1001, "李四", 26, 97}
	//定义完对象后,调用方法
	stu.EditInfo(1003, "王五", 30, 100)
	fmt.Printf("stu: %+v\n", *stu)
}

  在创建方法时,接收者类型为指针类型,所以在调用方法时,创建一个结构体变量,同时将结构体变量的地址,传递给方法的接收者,然后调用EditInfo( )方法,完成要修改的数据传递。

  在使用方法时,要注意如下几个问题:

  第一:只要接收者类型不一样,这个方法就算同名,也是不同方法,不会出现重复定义函数的错误

type long int

func (l long)Test()  {
	
}

type char byte

func (c char)Test()  {
	
}

但是,如果接收者类型一样,但是方法的参数不一样,是会出现错误的。

type long int

func (tmp long) Test() {

}

func (res long) Test(a,b int) {

}

  也就是,在GO中没有方法重载(所谓重载,指的是方法名称一致,参数类型,个数不一致)。

 

  第二:关于接收者不能为指针类型。

type long int

func (tmp *long) Test() {

}

  以上定义是正确的

  但下面的定义就是错误的

type pointer *int

//pointer为接收者类型,它本身不能是指针类型
func (tmp pointer) Test() {

}

  第三:接收者为普通变量,非指针,值传递

type Student struct {
	id    int
	name  string
	age   int
	score float64
}

//将方法绑定在指定的结构体(类)上
func (stu Student) PrintInfo(id int, name string, age int, score float64) {
	stu.id = id
	stu.name = name
	stu.age = age
	stu.score = score
}

func main() {
	var stu Student
	stu.PrintInfo(1001, "张三", 26, 99)
	fmt.Printf("stu: %+v\n", stu)
}

  结果如下

stu: {id:0 name: age:0 score:0}

  接收者为指针变量,引用传递:

type Student struct {
	id    int
	name  string
	age   int
	score float64
}

//将方法绑定在指定的结构体(类)上
func (stu *Student) EditInfo(id int, name string, age int, score float64) {
	stu.id = id
	stu.name = name
	stu.age = age
	stu.score = score
}

func main() {
	var stu Student
	(&stu).EditInfo(1001, "张三", 26, 99)
	fmt.Printf("stu: %+v\n", stu)
}

  结果如下:

stu: {id:1001 name:张三 age:26 score:99}

  (3)指针变量的方法值

  在上面的案例中,我们定义了两个方法,一个是PrintInfo( ), 该方法的接收者为普通变量,一个EditInfo( )方法,该方法的接收者为指针变量,那么大家思考这么一个问题:定义一个结构体指针变量,能否调用PrintShow( )方法呢?如下所示:

type Student struct {
	id    int
	name  string
	age   int
	score float64
}

func (stu Student) PrintInfo(id int, name string, age int, score float64) {
	stu.id = id
	stu.name = name
	stu.age = age
	stu.score = score
	fmt.Printf("stu: %+v\n", stu)
}
func (stu *Student) EditInfo(id int, name string, age int, score float64) {
	stu.id = id
	stu.name = name
	stu.age = age
	stu.score = score
}

func main() {
	var stu Student
	(&stu).PrintInfo(1003, "李四", 30, 98)
}

  结果如下:

stu: {id:1003 name:李四 age:30 score:98}

  通过测试,发现是可以调用的。

  为什么结构体指针变量,可以调用PrintShow( )方法呢?

  原因是:先将指针stu,转换成*stu(解引用)再调用。

  等价如下代码:

(*(&stu)).PrintInfo(1003, "李四", 30, 98)

  所以,如果结构体变量是一个指针变量,它能够调用哪些方法,这些方法就是一个集合,简称方法集。

  如果是普通的结构体变量能否调用EditInfo( )方法。

type Student struct {
	id    int
	name  string
	age   int
	score float64
}

func (stu Student) PrintInfo(id int, name string, age int, score float64) {
	stu.id = id
	stu.name = name
	stu.age = age
	stu.score = score
	fmt.Printf("stu: %+v\n", stu)
}
func (stu *Student) EditInfo(id int, name string, age int, score float64) {
	stu.id = id
	stu.name = name
	stu.age = age
	stu.score = score
}

func main() {
	var stu Student
	stu.EditInfo(1002, "王五", 25, 97)
	fmt.Printf("stu: %+v\n", stu)
}

  结果如下:

stu: {id:1002 name:王五 age:25 score:97}

  是可以调用的,原因是:将普通的结构体类型的变量转换成(&stu)在调用EditInfo( )方法。

  这样的好处是非常灵活,创建完对应的对象后,可以随意调用方法,不需要考虑太多指针的问题。

  下面进行面向对象编程的练习

  练习1:

  定义一个学生类,有六个属性,分别为姓名、性别、年龄、语文、数学、英语成绩。

  有2个方法:

  第一个打招呼的方法:介绍自己叫XX,今年几岁了,是男同学还是女同学。

  第二个是计算自己总分数和平均分的方法。{显示:我叫XX,这次考试总成绩为X分,平均成绩为X分}

  1:结构体定义如下:

type Student struct {
	name string
	sex  string
	age  int
	ch   float64
	math float64
	eng  float64
}

  2:为结构体定义相应的方法,并且在方法中可以完成对传递过来的数据的校验

func (stu *Student) SayHello(name string, sex string, age int) {
	stu.name = name
	stu.sex = sex
	stu.age = age
	if stu.age < 0 || stu.age > 120 {
		stu.age = 0
	}
	if stu.sex != "男" || stu.sex != "女" {
		stu.sex = "男"
	}
	fmt.Printf("我叫%s,今年%d岁了,是%s同学。\n", stu.name, stu.age, stu.sex)
}

func (stu *Student) SumAndAvg(ch, math, eng float64) {
	stu.ch = ch
	stu.math = math
	stu.eng = eng
	var sum float64
	sum = stu.math + stu.ch + stu.eng
	fmt.Printf("我叫%s,这次考试总成绩是%.1f,平均成绩是%.1f。\n", stu.name, sum, sum/3)
}

  3:完成方法的调用

func main() {
	var stu Student
	stu.SayHello("张三", "男", 25)
	stu.SumAndAvg(96, 97, 98)
	fmt.Printf("stu :%+v\n", stu)
}

  结果如下:

我叫张三,今年25岁了,是男同学。
我叫张三,这次考试总成绩是291.0,平均成绩是97.0。
stu :{name:张三 sex:男 age:25 ch:96 math:97 eng:98}

  在以上的案例中,SayHello()方法中已经完成了name属性的赋值,所以在SumAndAvg( )方法中,可以直接使用,因为我们使用指针指向了同一个结构体内存。

  在调用的过程中,也能体会出确实很方便不需要考虑太多指针的问题。

1.3 方法继承

  现在我们已经实现了为结构体添加成员(属性),和方法,并且实现了成员属性的继承,那么方法能否继承呢?

  具体如下:

type Person struct {
	name string
	sex  string
	age  int
}

//Person类型,实现了一个方法
func (per *Person) PrintInfo() {
	fmt.Printf("name=%s,sex=%s,age=%d\n", per.name, per.sex, per.age)
}

//定义一个Student类,继承Person类
type Student struct {
	Person
	id    int
	score float64
}

func main() {
	stu := Student{Person{"张三", "M", 26}, 1001, 99}

	//子类对象调用父类方法
	stu.PrintInfo()
}

  方法继承与属性继承一致,子类对象可以直接调用父类方法。

  练习:根据以下信息,实现对应的继承关系

  记者:我是记者  我的爱好是偷拍 我的年龄是34 我是一个男狗仔

  程序员:我叫孙权 我的年龄是23 我是男生 我的工作年限是3年

  思路:1.找出公共的属性,定义父类(结构体)

type Person struct {
	name string
	sex string
	age int
}

  2:找出公共的方法,定义在父类(结构体)

func (per *Person)serValue(name string,sex string,age int)  {
	per.name = name
	per.sex = sex
	per.age = age
}

  3:找出独有的属性,定义在自己的结构体(类)中。

  4:找出独有的方法,定义在自己的结构体(类)中。

type Reporter struct {
	Person
	hobby string
}

func (rep *Reporter) ReporterSayHello(hobby string) {
	rep.hobby = hobby
	fmt.Printf("我叫%s,是一名狗仔,我是%s生,今年%d岁,爱好是%s。\n", rep.name, rep.sex, rep.age, rep.hobby)
}

type Programmer struct {
	Person
	workyear int
}

func (prog *Programmer) ProgrammerSayHello(workyear int) {
	prog.workyear = workyear
	fmt.Printf("我叫%s,是一名程序员,我是%s生,今年%d岁,工作%d年了。\n",prog.name,prog.sex,prog.sex,prog.workyear)
	
}

  完成调用:

func main() {
	var rep Reporter
	rep.setValue("张三", "男", 34)
	rep.ReporterSayHello("偷拍")

	var prog Programmer
	prog.setValue("孙权", "男", 23)
	prog.ProgrammerSayHello(3)
}

  结果如下:

我叫张三,是一名狗仔,我是男生,今年34岁,爱好是偷拍。
我叫孙权,是一名程序员,我是男生,今年23岁,工作3年了。

1.4 方法重写

  在前面的案例中,子类(结构体)可以继承父类中的方法,但是,如果父类中的方法与子类的方法是重名方法会怎样呢?

type Person struct {
	name string
	age  int
	sex  string
}

func (per *Person) PrintInfo() {
	fmt.Printf("name=%s,age=%d,sex=%s", per.name, per.age, per.sex)
}

type Student struct {
	Person
	id    int
	score float64
}

//子类跟父类定义了相同的方法
func (stu *Student) PrintInfo() {
	fmt.Printf("stu: %+v\n", *stu)
}

func main() {
	stu := Student{Person{"张三", 27, "男"}, 1001, 99}
	stu.PrintInfo()
}

  上面子类和父类都定义了PrintInfo方法,子类在调用PrintInfo时,是调用子类的方法还是父类的方法呢?

  结果如下:

stu: {Person:{name:张三 age:27 sex:男} id:1001 score:99}

  如果子类(结构体)中的方法名与父类(结构体)中的方法名同名,在调用的时候是先调用子类(结构体)中的方法,这就方法的重写。

  所谓的重写:就是子类(结构体)中的方法,将父类中的相同名称的方法的功能重新给改写了。

  如果想调用父类的方法该怎么做呢?

  子类对象.父类名.父类方法 —— 使用父类方法

  按上面的例子就是:

stu.Person.PrintInfo()

  为什么要重写父类(结构体)的方法呢?

  通常,子类(结构体)继承父类(结构体)的方法,在调用对象继承方法的时候,调用和执行的是父类的实现。但是,有时候需要

  对子类中的继承方法有不同的实现方式。例如,假设动物存在"叫"的方法,从中继承有,猫类和狗类两个子类,但是它们的叫是不一样的。

  例如以下案例:

type Animal struct {
	age int
}

func (p *Animal) Bark() {
	fmt.Println("叫")
}

type Dog struct {
	Animal
}

func (d *Dog) Bark() {
	fmt.Println("汪汪叫")
}

type Cat struct {
	Animal
}

func (c *Cat) Bark() {
	fmt.Println("喵喵叫")
}
func main() { 
  var dog Dog
  dog.Bark()
  var cat Cat
  cat.Bark()
}

  在改案例中,定义了一个动物类(结构体),并且有一个叫的方法,接下来小狗的类(结构体)继承动物类,小猫的类继承动物类,它们都有了叫的方法,但是动物类中的叫的方法无法满足小猫和小狗的叫的要求,只能重写。

1.5 方法地址和放大表达式

  在前面的案例中,我们调用结构体(类)中的方法,一般都是通过如下的方式:

var dog Dog
dog.Bark()
	
var cat Cat
cat.Bark()

  或者是指针变量,现在,在给大家补充另外一种方式。

  如下所示:

var dog Dog
dFunc := dog.Bark
dFunc()

  以上调用的方式称为方法值。这种方式隐藏了接收者。

  还有一种调用的方式是通过方法表达式,如下所示:

type Person struct {
	name string
	sex  string
	age  int
}

func (p Person) SetInfoValue() {
	fmt.Printf("SetInfoValue: %p,%v\n", &p, p)
}

func (p *Person) SetINfoPointer() {
	fmt.Printf("SetInfoPointer: %p,%v\n", p, p)
}

func main() {
	p := Person{"李四", "男", 27}
	fmt.Printf("main: %p,%v\n", &p, p)

	/*
		f := p.SetInfoValue
		f()
		方法值:隐藏了接受者
	*/

	//方法表达式
	f := (Person).SetInfoValue
	f(p) //显示把接受者传递出去 ======》p.SetInfoValue()

	f2 := (*Person).SetINfoPointer
	f2(&p) //显示把接受者传递出去 ======》p.SetInfoPointer()
}

1.6 接口

  在讲解具体的接口之前,先看如下问题。

  使用面向对象的方式,设计一个加减的计算器

  代码如下:

type ObjectOperate struct {
	num1 int
	num2 int
}

type AddOperate struct {
	ObjectOperate
}

func (add *AddOperate) Operate(a, b int) int {
	add.num1 = a
	add.num2 = b
	return add.num1 + add.num2
}

type SubOperate struct {
	ObjectOperate
}

func (sub *SubOperate) Operate(a, b int) int {
	sub.num1 = a
	sub.num2 = b
	return sub.num1 - sub.num2
}

func main() {
	var sub SubOperate
	fmt.Println(sub.Operate(7, 2))
}

  以上实现非常简单,但是有个问题,在main( )函数中,当我们想使用减法操作时,创建减法类的对象,调用其对应的减法的方法。但是,有一天,系统需求发生了变化,要求使用加法,不再使用减法,那么需要对main( )函数中的代码,做大量的修改。将原有的代码注释掉,创建加法的类对象,调用其对应的加法的方法。有没有一种方法,让main( )函数,只修改很少的代码就可以解决该问题呢?有,要用到接下来给大家讲解的接口的知识点。

 

  (1)什么是接口?

  接口就是一种规范与标准,在生活中经常见接口,例如:笔记本电脑的USB接口,可以将任何厂商生产的鼠标与键盘,与电脑进行链接。为什么呢?原因就是,USB接口将规范和标准制定好后,各个生产厂商可以按照该标准生产鼠标和键盘就可以了。

  在程序开发中,接口只是规定了要做哪些事情,干什么。具体怎么做,接口是不管的。这和生活中接口的案例也很相似,例如:USB接口,只是规定了标准,但是不关心具体鼠标与键盘是怎样按照标准生产的。

  在企业开发中,如果一个项目比较庞大,那么就需要一个能理清所有业务的架构师来定义一些主要的接口,这些接口告诉开发人员你需要实现那些功能。

 

  (2)接口的定义

  接口定义的语法如下:

//定义接口类型
type Human interface {
	//接口中的方法,只声明,不实现;由别的类型(自定义类型)实现
	sayhi()
}

  怎样具体实现接口中定义的方法呢?

type Student struct {
	name  string
	score float64
}

//Student实现了此方法
func (stu *Student) sayhi() {
	fmt.Printf("学生%s考了%.1f分。\n", stu.name, stu.score)
}

type Teacher struct {
	name    string
	subject string
}

//Teacher实现了此方法
func (tea *Teacher) sayhi() {
	fmt.Printf("教师%是教%s的。\n", tea.name, tea.subject)
}

  具体的调用如下:

func main() {
	//定义接口类型变量
	var h Human
	//只要实现了此接口方法的类型,那么这个类型的变量(接受者类型)就可以给h赋值
	stu := Student{"张三", 99}
	h = &stu  //这里必须赋值地址
	h.sayhi()

	tea := Teacher{"李四", "语文"}
	h = &tea
	h.sayhi()
}

  只要类(结构体)实现对应的接口,那么根据该类创建的对象,可以赋值给对应的接口类型。

  接口的命名习惯以er结尾。

  现在我们用接口来修改一下开始的计算器程序

type Operater interface {
	result(a, b int) int
}

type ObjectOperate struct {
	num1 int
	num2 int
}

type AddOperate struct {
	ObjectOperate
}

func (add *AddOperate) result(a, b int) int {
	add.num1 = a
	add.num2 = b
	return add.num1 + add.num2
}

type SubOperate struct {
	ObjectOperate
}

func (sub *SubOperate) result(a, b int) int {
	sub.num1 = a
	sub.num2 = b
	return sub.num1 - sub.num2
}

func main() {
	var o Operater
	var sub SubOperate
	o = &sub
	res := o.result(10, 2)
	fmt.Println(res)

}

  (3)多态

  接口有什么好处呢?实现多态。

  所谓多态指的是多种表现形式,如下图所示:

  该拖拉机既可以扫地又可以当风扇。功能非常强大。

  使用接口实现多态的方式如下:

//定义接口类型
type Humaner interface {
	//接口中的方法只声明,不实现,由别的类型(自定义类型)实现
	PrintInfo()
}

type Person struct {
	name string
}

type Student struct {
	Person
	score float64
}

//Student实现了该方法
func (stu *Student) PrintInfo() {
	fmt.Printf("学生%s考了%.1f分。\n", stu.name, stu.score)
}

type Teacher struct {
	Person
	subject string
}

//Teacher实现了该方法
func (tea *Teacher) PrintInfo() {
	fmt.Printf("教师%s是教%s的。\n", tea.name, tea.subject)
}

//定义一个普通函数,参数类型是接口类型
//只有一个函数,可以有多种表现,多态
//实现了多态
func WhoSay(h Humaner) {
	h.PrintInfo()
}

func main() {
	stu := Student{Person{"张三"}, 96}
	tea := Teacher{Person{"李四"}, "数学"}

	//调用同一个函数,通过传入不同参数,得到不同结果,多态,多种形态
	WhoSay(&stu)
	WhoSay(&tea)
}

  关于接口的定义,以及使用接口实现多态,大家都比较熟悉了,但是多态有什么好处呢?现在还是以开始提出的计算器案例给大家讲解一下,在开始我们已经实现了一个加减功能的计算器,但是有人感觉太麻烦了,因为实现加法,就要定义加法操作的类(结构体),实现减法就要定义减法的类(结构体),所以这个人实现了一个比较简单的加减法的计算器,如下所示:

  1.使用面向对象的思想实现一个加减功能的计算器,可能有人感觉非常简单,代码如下:

type Operation struct {
}

func (p *Operation) GetResult(num1, num2 float64, operate string) float64 {
	var result float64
	switch operate {
	case "+":
		result = num1 + num2
	case "-":
		result = num1 - num2

	}
	return result
}

func main() {
	var operation Operation
	res := operation.GetResult(10, 20, "+")
	fmt.Println(res)

}

  我们定义了一个类(结构体),然后为该类创建了一个方法,封装了整个计算器功能,以后要使用直接使用该类(结构体)创建对象就可以了。这就是面向对象总的封装性。

  也就是说,当你写完这个计算器后,交给你的同事,你的同事要用,直接创建对象,然后调用GetResult()方法就可以, 根本不需要关心该方法是怎样实现的。

 

  2.大家仔细观察上面的代码,有什么问题吗?

  现在让你在该计算器中,再增加一个功能,例如乘法,应该怎么办呢?你可能会说很简单啊,直接在GetResult( )方法的switch中添加一个case分支就可以了。

  问题是:在这个过程中,如果你不小心将加法修改成了减法怎么办?或者说,对加法运算的规则做了修改怎么办?举例子说明:

  你可以把该程序方法想象成公司中的薪资管理系统。如果公司决定对薪资的运算规则做修改,由于所有的运算规则都在Operation类中的GetResult()方法中,所以公司只能将该类的代码全部给你,你才能进行修改。这时,你一看自己作为开发人员工资这么低,心想“TMD,老子累死累活才给这么点工资,这下有机会了”。直接在自己工资后面加了3000:num1+num2+3000

 

  所以说,我们应该将 加减等运算分开,不应该全部糅合在一起,这样你修改加的时候,不会影响其它的运算规则:

  具体实现如下:

type Operation struct {
	num1 float64
	num2 float64
}

type GetResulter interface {
	GetResult() float64
}

type AddOperation struct {
	Operation
}

func (add *AddOperation) GetResult() float64 {
	return add.num1 + add.num2
}

type SubOperation struct {
	Operation
}

func (sub *SubOperation) GetResult() float64 {
	return sub.num1 - sub.num2
}

//多态
func Result(i GetResulter) float64 {
	return i.GetResult()
}

  现在已经将各个操作分开了,并且这里我们还定义了一个父类(结构体),将公共的成员放在该父类中。如果现在要修改某项运算规则,只需将对应的类和方法发给你,进行修改就可以了。

  这里的实现虽然将各个运算分开了,但是与我们第一次实现的还是有点区别。我们第一次实现的加减计算器也是将各个运算分开了,但是没有定义接口。那么该接口的意义是什么呢?继续看下面的问题。

 

  3.现在怎样调用呢?

  这就是一开始给大家提出的问题,如果调用的时候,直接创建加法操作的对象,调用对应的方法,那么后期要改成减法呢?需要做大量的修改,所以问题解决的方法如下:

//创建一个类负责对象的创建
type OperationFactory struct {
}

func (f *OperationFactory) CreatOption(num1, num2 float64, option string) float64 {
	var result float64
	switch option {
	case "+":
		add := AddOperation{Operation{num1, num2}}
		result = Result(&add)
	case "-":
		sub := SubOperation{Operation{num1,num2}}
		result = Result(&sub)
	}
	return result
}

  创建了一个类OperationFactory,在改类中添加了一个方法CreateOption( )负责创建对象,如果输入的是“+”,创建AddOperation的对象,然后调用Result( )方法,将对象的地址传递到该方法中,所以变量i指的就是AddOperation,接下来在调用GetResult( )方法,实际上调用的是AddOperation类实现的GetResult( )方法。

  同理如果传递过来的是“-”,流程也是一样的。

  所以,通过该程序,大家能够体会出多态带来的好处。

  4.最后调用

func main() {
	var opfactory OperationFactory
	res := opfactory.CreatOption(10, 20, "+")
	fmt.Println(res)
}

  这时会发现调用,非常简单,如果现在想计算减法,只要将"+",修改成"-"就可以。也就是说,除去了main( )函数与具体运算类的依赖。

  当然程序经过这样设计以后:如果现在修改加法的运算规则,只需要修改AddOperation类中对应的方法,不需要关心其它的类。

  如果现在要增加“乘法” 功能,应该怎样进行修改呢?

  第一:定义乘法的类,完成乘法运算。

  第二:在OperationFactory类中CreateOption( )方法中添加相应的分支。但是这样做并不会影响到其它的任何运算。

 

  在使用面向对象思想解决问题时,一定要先分析,定义哪些类,哪些接口,哪些方法。把这些分析定义出来,然后在考虑具体实现。

  最后完整代码如下:

type Operation struct {
	num1 float64
	num2 float64
}

type Resulter interface {
	GetResult() float64
}

type AddOperation struct {
	Operation
}

func (add *AddOperation) GetResult() float64 {
	return add.num1 + add.num2
}

type SubOperation struct {
	Operation
}

func (sub *SubOperation) GetResult() float64 {
	return sub.num1 - sub.num2
}

type MulOperation struct {
	Operation
}

func (mul *MulOperation) GetResult() float64 {
	return mul.num1 * mul.num2
}

type DivOperation struct {
	Operation
}

func (div *DivOperation) GetResult() float64 {
	return div.num1 / div.num2
}

//实现多态
func Result(i Resulter) float64 {
	return i.GetResult()
}

type OperationFactory struct {
}

func (p *OperationFactory) CreateOption(num1, num2 float64, option string) float64 {
	var result float64
	switch option {
	case "+":
		add := AddOperation{Operation{num1, num2}}
		result = Result(&add)
	case "-":
		sub := SubOperation{Operation{num1, num2}}
		result = Result(&sub)
	case "*":
		mul := MulOperation{Operation{num1, num2}}
		result = Result(&mul)
	case "/":
		div := DivOperation{Operation{num1, num2}}
		result = Result(&div)
	}
	return result
}

func main() {
	var opfactory OperationFactory
	res := opfactory.CreateOption(3, 4, "/")
	fmt.Println(res)
}

1.7 空接口

  空接口(interface{})不包含任何的方法,正因为如此,所有的类型都实现了空接口,因此空接口可以存储任意类型的数值

  例如

func main() {
	//空接口万能类型,可以保存任意类型的值
	var i interface{}
	fmt.Printf("i = %v\n", i)
	fmt.Printf("%T\n", i)

	i = "abc"
	fmt.Printf("i = %v\n", i)
	fmt.Printf("%T\n", i)
}

   结果如下:

i = <nil>
<nil>
i = abc
string

   空接口默认值为nil,默认数据类型是nil;接受完数据后,类型会变成数据对应的数据类型。

  当函数可以接受任意的对象实例时,我们会将其声明为interface{},最典型的例子是标准库fmt中PrintXXX系列的函数,例如:

  func Printf(format string, a ...interface{}) 

  func Println(a ...interface{})

  如果自己定义函数,可以如下:

    func Test(arg ...interface{}) {

     }

  Test( )函数可以接收任意个数,任意类型的参数

  现在有一个问题,由空接口接收的值可以进行运算吗?我们看一下代码:

func main() {
	//空接口万能类型,可以保存任意类型的值
	var i interface{}
	i = 100
	fmt.Printf("%T\n", i)

	fmt.Println(i + 100)
}

  执行时会报错:

invalid operation: i + 100 (mismatched types interface {} and int)

由此可知,由空接口接收的值是不能参与运算的,要想能参与运算,需要用到等会儿会提到的类型断言。

1.8 类型断言

  类型断言就是判断一个变量是否是某一数据类型的变量 。类型断言的语法是:value,status := element.(T);这里value就是变量的值,status是一个bool类型,element是interface变量,T是断言的数据类型。如果element里面确实存储了T类型的数据,则status就为true,value就会保存对应的值;否则status就为false,value就为T类型的默认值。由空接口接收的数据,如果想要参与运算,一定要进行类型断言。

  具体案例如下:

type Student struct {
	id   int
	name string
}

func main() {
	i := make([]interface{}, 3)
	i[0] = 1                   //int
	i[1] = "hello"             //string
	i[2] = Student{1001, "张三"} //Student

	for index, data := range i {
		if value, status := data.(int); status {
			fmt.Printf("i[%d] 类型为int,内容为%d\n", index, value)
		} else if value, status := data.(string); status {
			fmt.Printf("i[%d] 类型为string,内容为%s\n", index, value)
		} else if value, status := data.(Student); status {
			fmt.Printf("i[%d] 类型为Student,内容为%+v\n", index, value)
		}
	}

}

结果如下:

i[0] 类型为int,内容为1
i[1] 类型为string,内容为hello
i[2] 类型为Student,内容为{id:1001 name:张三}

猜你喜欢

转载自www.cnblogs.com/dacaigouzi1993/p/11100463.html