一.面向对象
(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 ", stu)
制定初始化成员:
//指定成员初始化,没有初始化的整型自动赋值为0,字符串为空 stu := Student{score: 99.7} fmt.Printf("stu = %+v ", stu)
接下来还可以对Person中指定的成员进行初始化:
//指定成员初始化,没有初始化的整型自动赋值为0,字符串为空 stu := Student{Person{name: "李四"}, 97} fmt.Printf("stu = %+v ", 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 ", 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 ", 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 ", 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 ", 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 ", 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 ", 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 ", *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 ", 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 ", 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 ", 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 ", 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 ", 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同学。 ", 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。 ", stu.name, sum, sum/3) }
3:完成方法的调用
func main() { var stu Student stu.SayHello("张三", "男", 25) stu.SumAndAvg(96, 97, 98) fmt.Printf("stu :%+v ", 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 ", 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。 ", 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年了。 ",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 ", *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 ", &p, p) } func (p *Person) SetINfoPointer() { fmt.Printf("SetInfoPointer: %p,%v ", p, p) } func main() { p := Person{"李四", "男", 27} fmt.Printf("main: %p,%v ", &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分。 ", stu.name, stu.score) } type Teacher struct { name string subject string } //Teacher实现了此方法 func (tea *Teacher) sayhi() { fmt.Printf("教师%是教%s的。 ", 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分。 ", stu.name, stu.score) } type Teacher struct { Person subject string } //Teacher实现了该方法 func (tea *Teacher) PrintInfo() { fmt.Printf("教师%s是教%s的。 ", 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 ", i) fmt.Printf("%T ", i) i = "abc" fmt.Printf("i = %v ", i) fmt.Printf("%T ", 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 ", 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 ", index, value) } else if value, status := data.(string); status { fmt.Printf("i[%d] 类型为string,内容为%s ", index, value) } else if value, status := data.(Student); status { fmt.Printf("i[%d] 类型为Student,内容为%+v ", index, value) } } }
结果如下:
i[0] 类型为int,内容为1 i[1] 类型为string,内容为hello i[2] 类型为Student,内容为{id:1001 name:张三}
用switch语句完成如下:
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 { switch value := data.(type) { case int: fmt.Printf("i[%d] 类型是int,内容为%v ", index, value) case string: fmt.Printf("i[%d] 类型是string,内容为%v ", index, value) case Student: fmt.Printf("i[%d] 类型是Student,内容为%+v ", index, value) } } }