Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一 样,下面我们一一为同学们进行详细的讲解 Golang 的三大特性是如何实现的。
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其 它包只有通过被授权的操作(方法),才能对字段进行操作
封装的理解和好处
如何体现封装
将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)
给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值
func (var 结构体类型名) SetXxx(参数列表) (返回值列表) {
//加入数据验证的业务逻辑
var.字段 = 参数
}
提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值
func (var 结构体类型名) GetXxx() {
return var.age;
}
特别说明:在 Golang 开发中并没有特别强调封装,这点并不像 Java. 所以提醒学过 java 的朋友, 不用总是用 java 的语法特性来看待 Golang, Golang 本身对面向对象的特性做了简化的.
看一个案例 请大家看一个程序(person.go),不能随便查看人的年龄,工资等隐私,并对输入的年龄进行合理的验证。设计: model 包(person.go) main 包(main.go 调用 Person 结构体)
看一个问题,引出继承的必要性
一个小问题,看个学生考试系统的程序 extends01.go,提出代码复用的问题
code
package main
import
(
"fmt"
)
type Pupil struct{
Name string
Age int
Score 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 string
Age int
Score 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()
}
对上面代码的小结
继承可以解决代码复用,让我们的编程更加靠近人类思维。 当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体(比如刚才的Student),在该结构体中定义这些相同的属性和方法。
其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个 Student 匿名结构体即可。 [画 出示意图]
也就是说:在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访 问匿名结构体的字段和方法,从而实现了继承特性。
嵌套匿名结构体的基本语法
type Goods struct {
Name string Price int
}
type Book struct {
Goods //这里就是嵌套匿名结构体 Goods Writer string
}
快速入门案例
我们对 extends01.go 改进,使用嵌套匿名结构体的方式来实现继承特性,请大家注意体会这样编程的好处
package main
import
(
"fmt"
)
type Student struct{
Name string
Age int
Score 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 = 10
pupil.testing()
pupil.Student.SetScore(70)
pupil.Student.ShowInfo()
graduate := &Pupil{}
graduate.Student.Name = "davade~"
graduate.Student.Age = 20
graduate.testing()
graduate.Student.SetScore(90)
graduate.Student.ShowInfo()
}
匿名结构体字段访问可以简化,如图
对上面的代码小结
(1) 当我们直接通过 b 访问字段或方法时,其执行流程如下比如 b.Name
(2) 编译器会先看 b 对应的类型有没有 Name, 如果有,则直接调用 B 类型的 Name 字段
(3) 如果没有就去看 B 中嵌入的匿名结构体 A 有没有声明 Name 字段,如果有就调用,如果没有 继续查找…如果都找不到就报错.
当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问 匿名结构体的字段和方法,可以通过匿名结构体名来区分【举例说明】
结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身 没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。【举例说明】
如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合 的结构体的字段或方法时,必须带上结构体的名字
例题:
结构体的匿名字段是基本数据类型,如何访问, 下面代码输出什么
说明
基本介绍
按顺序,我们应该讲解多态,但是在讲解多态前,我们需要讲解接口(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 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现 了该接口。
一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
一个接口(比如 A 接口)可以继承多个别的接口(比如 B,C 接口),这时如果要实现 A 接口,也必 须将 B,C 接口的方法也全部实现。
interface 类型默认是一个指针(引用类型),如果没有对 interface 初始化就使用,那么会输出 nil
课堂练习
实现对 Hero 结构体切片的排序: sort.Sort(data Interface)
package main
import
(
"fmt"
//"time"
"math/rand"
"sort"
)
type Hero struct{
Name string
Age 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 HeroSlice
for 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)
}
}
大家听到现在,可能会对实现接口和继承比较迷茫了, 这个问题,那么他们究竟有什么区别呢
接口和继承解决的解决的问题不同
接口比继承更加灵活 Person Student BirdAble LittleMonkey
接口比继承更加灵活,继承是满足 is - a 的关系,而接口只需满足 like - a 的关系。
接口在一定程度上实现代码解耦