基本介绍
Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一 样,下面我们一一为同学们进行详细的讲解 Golang 的三大特性是如何实现的。
封装介绍
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其 它包只有通过被授权的操作(方法),才能对字段进行操作
封装的理解和好处
- 隐藏实现细节
- 提可以对数据进行验证,保证安全合理(Age)
如何体现封装
- 对结构体中的属性进行封装
- 通过方法,包实现封装
封装的实现步骤
func (var 结构体类型名) SetXxx(参数列表) (返回值列表) {//加入数据验证的业务逻辑 var.字段 = 参数}
func (var 结构体类型名) GetXxx() {return var.age;}
特别说明:在 Golang 开发中并没有特别强调封装,这点并不像 Java. 所以提醒学过 java 的朋友, 不用总是用 java 的语法特性来看待 Golang, Golang 本身对面向对象的特性做了简化的.
快速入门案例
看一个案例 请大家看一个程序(person.go),不能随便查看人的年龄,工资等隐私,并对输入的年龄进行合理的验证。设计: model 包(person.go) main 包(main.go 调用 Person 结构体)
代码实现
model/person.go
main/main.go
面向对象编程三大特性-继承
看一个问题,引出继承的必要性
一个小问题,看个学生考试系统的程序 extends01.go,提出代码复用的问题
code
package mainimport ("fmt")type Pupil struct{Name stringAge intScore int
}func (p *Pupil) ShowInfo() {fmt.Printf("学生姓名=%v 年龄=%v 成绩=%v\n" , (*p).Name, (*p).Age, (*p).Score)
}func (p *Pupil) SetScore(score int) {p.Score = score
}func (*Pupil) testing(){fmt.Printf("小学生正在考试\n")
}type Graduate struct{Name stringAge intScore int
}func (p *Graduate) ShowInfo() {fmt.Printf("学生姓名=%v 年龄=%v 成绩=%v\n" , (*p).Name, (*p).Age, (*p).Score)
}func (p *Graduate) SetScore(score int) {p.Score = score
}func (p *Graduate) testing(){fmt.Printf("大学数正在考试\n")
}//大学数,研究生。。、func main(){var pupil = &Pupil{Name : "tom",Age : 10,}pupil.testing()pupil.SetScore(90)pupil.ShowInfo()var da = &Graduate{Name : "ada",Age : 20,}da.testing()da.SetScore(50)da.ShowInfo()
}
对上面代码的小结
- Pupil 和 Graduate 两个结构体的字段和方法几乎,但是我们却写了相同的代码, 代码复用性不强
- 出现代码冗余,而且代码不利于维护,同时也不利于功能的扩展。
- 解决方法-通过继承方式来解决
继承基本介绍和示意图
继承可以解决代码复用,让我们的编程更加靠近人类思维。 当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体(比如刚才的Student),在该结构体中定义这些相同的属性和方法。
其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个 Student 匿名结构体即可。 [画 出示意图]
也就是说:在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访 问匿名结构体的字段和方法,从而实现了继承特性。
嵌套匿名结构体的基本语法
type Goods struct { Name string Price int
}type Book struct {Goods //这里就是嵌套匿名结构体 Goods Writer string
}
快速入门案例
我们对 extends01.go 改进,使用嵌套匿名结构体的方式来实现继承特性,请大家注意体会这样编程的好处
package mainimport ("fmt")type Student struct{Name stringAge intScore int
}func (stu *Student) ShowInfo() {fmt.Printf("学生姓名=%v 年龄=%v 成绩=%v\n" , (*stu).Name, (*stu).Age, (*stu).Score)
}func (stu *Student) SetScore(score int) {stu.Score = score
}//小学生
type Pupil struct{Student//嵌入student匿名结构体
}type Graduate struct{Student
}func (*Pupil) testing(){fmt.Printf("小学生正在考试\n")
}func (p *Graduate) testing(){fmt.Printf("大学数正在考试\n")
}//大学数,研究生。。、func main(){//当嵌入匿名结构体后,使用方法会发生变换pupil := &Pupil{}pupil.Student.Name = "tom~"pupil.Student.Age = 10pupil.testing()pupil.Student.SetScore(70)pupil.Student.ShowInfo()graduate := &Pupil{}graduate.Student.Name = "davade~"graduate.Student.Age = 20graduate.testing()graduate.Student.SetScore(90)graduate.Student.ShowInfo()
}
继承给编程带来的便利
- 代码的复用性提高了
- 代码的扩展性和维护性提高了
继承的深入讨论
-
结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法, 都可以使用。【举例说明】
-
匿名结构体字段访问可以简化,如图
对上面的代码小结
(1) 当我们直接通过 b 访问字段或方法时,其执行流程如下比如 b.Name
(2) 编译器会先看 b 对应的类型有没有 Name, 如果有,则直接调用 B 类型的 Name 字段
(3) 如果没有就去看 B 中嵌入的匿名结构体 A 有没有声明 Name 字段,如果有就调用,如果没有 继续查找…如果都找不到就报错. -
当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问 匿名结构体的字段和方法,可以通过匿名结构体名来区分【举例说明】
-
结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身 没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。【举例说明】
-
如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合 的结构体的字段或方法时,必须带上结构体的名字
-
嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
例题:
结构体的匿名字段是基本数据类型,如何访问, 下面代码输出什么
说明
- 如果一个结构体有 int 类型的匿名字段,就不能第二个。
- 如果需要有多个 int 的字段,则必须给 int 字段指定名字
面向对象编程-多重继承
-
多重继承说明
如一个 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方 法,从而实现了多重继承。 -
案例演示
通过一个案例来说明多重继承使用
多重继承细节说明
-
如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来 区分。【案例演示】
-
为了保证代码的简洁性,建议大家尽量不使用多重继承
接口(interface)
基本介绍
按顺序,我们应该讲解多态,但是在讲解多态前,我们需要讲解接口(interface),因为在 Golang 中 多态 特性主要是通过接口来体现的。
为什么有接口
接口快速入门
这样的设计需求在 Golang 编程中也是会大量存在的,我曾经说过,一个程序就是一个世界,在现实世 界存在的情况,在程序中也会出现。我们用程序来模拟一下前面的应用场景。
package main import ("fmt"
)//声明/定义一个接口
type Usb interface {
//声明了两个没有实现的方法Start() Stop()
}type Phone struct {}//让 Phone 实现 Usb 接口的方法
func (p Phone) Start() { fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop() { fmt.Println("手机停止工作。。。")
}type Camera struct {}//让 Camera 实现 Usb 接口的方法
func (c Camera) Start() { fmt.Println("相机开始工作。。。")
}func (c Camera) Stop() { fmt.Println("相机停止工作。。。")
}//计算机
type Computer struct {}//编写一个方法 Working 方法,接收一个 Usb 接口类型变量
//只要是实现了 Usb 接口 (所谓实现 Usb 接口,就是指实现了 Usb 接口声明所有方法)
func (c Computer) Working(usb Usb) {
//usb 变量会根据传入的实参,来判断到底是 Phone,还是 Camera
//通过 usb 接口变量来调用 Start 和 Stop 方法usb.Start() usb.Stop()
}func main() {//测试//先创建结构体变量 computer := Computer{} phone := Phone{} camera := Camera{}//关键点 computer.Working(phone) computer.Working(camera)
}
接口概念的再说明
interface 类型可以定义一组方法,但是这些不需要实现。并且 interface 不能包含任何变量。到某个 自定义类型(比如结构体 Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)。
基本语法
小结说明:
- 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的 多态和高内聚低偶合的思想。
- Golang 中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个 变量就实现这个接口。因此,Golang 中没有 implement 这样的关键字
接口使用的应用场景
注意事项和细节
- 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)
-
接口中所有的方法都没有方法体,即都是没有实现的方法。
-
在 Golang 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现 了该接口。
-
一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
-
只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。
-
一个自定义类型可以实现多个接口
-
Golang 接口中不能有任何变量
-
一个接口(比如 A 接口)可以继承多个别的接口(比如 B,C 接口),这时如果要实现 A 接口,也必 须将 B,C 接口的方法也全部实现。
-
interface 类型默认是一个指针(引用类型),如果没有对 interface 初始化就使用,那么会输出 nil
-
空接口 interface{} 没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量
赋给空接口。
课堂练习
实现对 Hero 结构体切片的排序: sort.Sort(data Interface)
package mainimport ("fmt"//"time""math/rand""sort"
)type Hero struct{Name stringAge int
}//Hero结构体切片
type HeroSlice []Hero//实现interface接口
func (hs HeroSlice) Len() int{return len(hs)
}//less方法绝对使用什么标准进行排序
//1.按照年龄进行排序
func (hs HeroSlice) Less(i int,j int) bool{return hs[i].Age < hs[j].Age
}//
func (hs HeroSlice) Swap(i int, j int) {temp := hs[i]hs[i] = hs[j]hs[j] = temp
}//实现对 Hero 结构体切片的排序: sort.Sort(data Interface)func main(){var intslice = []int{0,-1,10,7,98}sort.Ints(intslice)fmt.Println(intslice)//对结构体切片进行排序//测试var heros HeroSlicefor i := 0; i <10; i++{hero := Hero{Name : fmt.Sprintf("英雄~%d", rand.Intn(100)),Age : rand.Intn(100),}heros = append(heros,hero)}for _ , v := range heros{fmt.Println(v)}fmt.Println("排序后")sort.Sort(heros)for _ , v := range heros{fmt.Println(v)}}
实现接口 vs 继承
大家听到现在,可能会对实现接口和继承比较迷茫了, 这个问题,那么他们究竟有什么区别呢
代码说明:
对上面代码的小结
- 当 A 结构体继承了 B 结构体,那么 A 结构就自动的继承了 B 结构体的字段和方法,并且可以直 接使用
- 当 A 结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此我 们可以认为:实现接口是对继承机制的补充.
实现接口可以看作是对 继承的一种补充
接口和继承解决的解决的问题不同
- 继承的价值主要在于:解决代码的复用性和可维护性。
- 接口的价值主要在于:设计,设计好各种规范(方法),让其它自定义类型去实现这些方法。
接口比继承更加灵活 Person Student BirdAble LittleMonkey
接口比继承更加灵活,继承是满足 is - a 的关系,而接口只需满足 like - a 的关系。
接口在一定程度上实现代码解耦