【Go笔记】第 11 章 面向对象(下)

9/27/2022 韩顺平尚硅谷golang笔记

# 11.1 VSCode 的使用

# 11.1.1 VSCode 使用技巧和经验

🏷 设置字体 文件->首选项->设置

🏷 快捷键的使用 自定义快捷配置:文件->首选项->键盘快捷方式

🏷 介绍几个常用的快捷键

  1. 删除当前行ctrl+shift+k``[也可以自定义]
  2. 向上/向下复制当前行Shit+At+↓/↑
  3. 补全代码alt+/
  4. 添加注释和取消注释ctrl+/
  5. 快速修复alt+/
  6. 快速格式化代码shit+alt+f
  7. 还有很多其它的快捷键.【参考VScode快捷键大全.doc】

特别说明:

  1. VSC0d的恤捷键不要和输入注冲突,否则不会生效
  2. 一些快捷键需要安装Go插件后才能生效

# 11.2 面向对象编程思想-抽象

# 11.2.1 抽象的介绍

我们在前面去定义一个结构体时候,实际上就是把一类事物的共有的 属性( 字段)和 行为( 方法)提取出来,形成一个 物理模型(结构体)。这种研究问题的方法称为抽象。

###11.2.2 代码实现


package main  
  
import "fmt"  
  
//定义一个结构体 Account  
type Account struct {  
   AccountNo string  
   Pwd       string  
   Balance   float64  
}  
  
//方法  
//1. 存款  
func (account *Account) Deposite(money float64, pwd string) {  
   //看下输入的密码是否正确  
   if pwd != account.Pwd {  
      fmt.Println("你输入的密码不正确")  
      return  
   }  
   //看看存款金额是否正确  
   if money <= 0 {  
      fmt.Println("你输入的金额不正确")  
      return  
   }  
  
   account.Balance += money  
   fmt.Println("存款成功~~")  
}  
  
//取款  
func (account *Account) WithDraw(money float64, pwd string) {  
   //看下输入的密码是否正确  
   if pwd != account.Pwd {  
      fmt.Println("你输入的密码不正确")  
      return  
   }  
   //看看取款金额是否正确  
   if money <= 0 || money > account.Balance {  
      fmt.Println("你输入的金额不正确")  
      return  
   }  
   account.Balance -= money  
   fmt.Println("取款成功~~")  
}  
  
//查询余额  
func (account *Account) Query(pwd string) {  
   //看下输入的密码是否正确  
   if pwd != account.Pwd {  
      fmt.Println("你输入的密码不正确")  
      return  
   }  
   fmt.Printf("你的账号为=%v 余额=%v \n", account.AccountNo, account.Balance)  
}  
func main() {  
   //测试一把  
   account := Account{  
      AccountNo: "gs1111111",  
      Pwd:       "666666",  
      Balance:   100.0,  
   }  
   //这里可以做的更加灵活,就是让用户通过控制台来输入命令...  
   //菜单....  
   account.Query("666666")  
   account.Deposite(200.0, "666666")  
   account.Query("666666")  
   account.WithDraw(150.0, "666666")  
   account.Query("666666")  
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

🏷 对上面代码的要求

  1. 同学们自己可以独立完成
  2. 增加一个控制台的菜单,可以让用户动态的输入命令和选项

# 11.3 面向对象编程三大特性-封装

# 11.3.1 基本介绍

Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一 样,下面我们一一为同学们进行详细的讲解 Golang 的三大特性是如何实现的。

# 11.3.2 封装介绍

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

# 11.3.3 封装的理解和好处

  1. 隐藏实现细节
  2. 提可以对 数据进行验证,保证安全合理(Age)

# 11.3.4 如何体现封装

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

# 11.3.5 封装的实现步骤

  1. 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)
  2. 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
  3. 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值
func (var 结构体类型名) SetXxx(参数列表) (返回值列表) {
	//加入数据验证的业务逻辑
	var.字段 = 参数
}
1
2
3
4
  1. 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值
func (var 结构体类型名) GetXxx() {
	return var.age;
}
1
2
3

特别说明:在 Golang 开发中并没有特别强调封装,这点并不像 Java. 所以提醒学过 java 的朋友,不用总是用 java 的语法特性来看待 Golang,Golang 本身对面向对象的特性做了简化的.

# 11.3.6 快速入门案例

🏷 看一个案例 请大家看一个程序(person.go),不能随便查看 人的年龄, 工资等隐私,并对输入的年龄进行合理的验 证。设计: model 包(person.go) main 包(main.go 调用 Person 结构体)

🏷 代码实现 model/person.go


package model  
  
import "fmt"  
  
type person struct {  
   Name string  
   age  int //其它包不能直接访问.:  
   sal  float64  
}  
  
//写一个工厂模式的函数,相当于构造函数  
func NewPerson(name string) *person {  
   return &person{  
      Name: name,  
   }  
}  
  
//为了访问age和sal我们编写一对SetXxx的方法和GetXxx的方法  
func (p *person) setAge(age int) {  
   if age > 0 && age < 150 {  
      p.age = age  
   } else {  
      fmt.Println("年龄范围不正确.")  
      //给程序员给一个默认值  
   }  
}  
  
func (p *person) GetAge() int {  
   return p.age  
}  
  
func (p *person) setsal(sal float64) {  
   if sal >= 3000 && sal <= 30000 {  
      p.sal = sal  
   } else {  
      fmt.Println("薪水范围不正确..")  
   }  
}  
  
func (p *person) Getsal() float64 {  
   return p.sal  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

main/main.go

package main  
  
import (  
   "fmt"  
   "go_test/day42/model")  
  
func main() {  
   p := model.NewPerson("smith")  
   p.SetAge(18)  
   p.Setsal(5000)  
   fmt.Println(p)  
   fmt.Println(p.Name, "age =", p.GetAge(), "sal =", p.Getsal())  
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 11.3.7 课堂练习(学员先做)

🏷 要求

  1. 创建程序,在 model 包中定义 Account 结构体:在 main 函数中体会 Golang 的封装性。
  2. Account 结构体要求具有字段:账号(长度在 6-10 之间)、余额(必须>20)、密码(必须是六
  3. 通过 SetXxx 的方法给 Account 的字段赋值。(同学们自己完成
  4. 在 main 函数中测试

🏷 代码实现 model/account.go


package model  
  
import (  
   "fmt"  
)  
  
//定义一个结构体 account  
type account struct {  
   accountNo string  
   pwd       string  
   balance   float64  
}  
  
//工厂模式的函数-构造函数  
func NewAccount(accountNo string, pwd string, balance float64) *account {  
   if len(accountNo) < 6 || len(accountNo) > 10 {  
      fmt.Println("账号的长度不对...")  
      return nil  
   }  
   if len(pwd) != 6 {  
      fmt.Println("密码的长度不对...")  
      return nil  
   }  
   if balance < 20 {  
      fmt.Println("余额数目不对...")  
      return nil  
   }  
   return &account{  
      accountNo: accountNo,  
      pwd:       pwd,  
      balance:   balance,  
   }  
}  
  
//方法  
//1. 存款  
func (account *account) Deposite(money float64, pwd string) {  
   //看下输入的密码是否正确  
   if pwd != account.pwd {  
      fmt.Println("你输入的密码不正确")  
      return  
   }  
   //看看存款金额是否正确  
   if money <= 0 {  
      fmt.Println("你输入的金额不正确")  
      return  
   }  
   account.balance += money  
   fmt.Println("存款成功~~")  
}  
  
//取款  
func (account *account) WithDraw(money float64, pwd string) {  
   //看下输入的密码是否正确  
   if pwd != account.pwd {  
      fmt.Println("你输入的密码不正确")  
      return  
   }  
   //看看取款金额是否正确  
   if money <= 0 || money > account.balance {  
      fmt.Println("你输入的金额不正确")  
      return  
   }  
   account.balance -= money  
   fmt.Println("取款成功~~")  
}  
  
//查询余额  
func (account *account) Query(pwd string) {  
   //看下输入的密码是否正确  
   if pwd != account.pwd {  
      fmt.Println("你输入的密码不正确")  
      return  
   }  
   fmt.Printf("你的账号为=%v 余额=%v \n", account.accountNo, account.balance)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

main/main.go


package main  
  
import (  
   "fmt"  
   "go_test/day43/model")  
  
func main() {  
   //创建一个 account 变量  
   account := model.NewAccount("jzh11111", "000", 40)  
   if account != nil {  
      fmt.Println("创建成功=", account)  
   } else {  
      fmt.Println("创建失败")  
   }  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

🏷 说明:在老师的代码基础上 增加如下功能: 通过 SetXxx 的方法给 Account 的字段赋值 通过 GetXxx 方法获取字段的值。(同学们自己完成)

在 main 函数中测试

# 11.4 面向对象编程三大特性-继承

# 11.4.1 看一个问题,引出继承的必要性

一个小问题,看个学生考试系统的程序 extends01.go,提出代码复用的问题

🏷 走一下代码:


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 (p *Pupil) testing() {  
   fmt.Println("小学生正在考试中.....")  
}  
  
//大学生, 研究生。。  
//大学生  
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.Println("大学生正在考试中.....")  
}  
  
//代码冗余.. 高中生....  
func main() {  
   //测试  
   var pupil = &Pupil{  
      Name: "tom",  
      Age:  10,  
   }  
   pupil.testing()  
   pupil.SetScore(90)  
   pupil.ShowInfo()  
   //测试  
   var graduate = &Graduate{  
      Name: "mary",  
      Age:  20,  
   }  
   graduate.testing()  
   graduate.SetScore(90)  
   graduate.ShowInfo()  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

🏷 对上面代码的小结

  1. Pupil 和 Graduate 两个结构体的字段和方法几乎,但是我们却写了相同的代码, 代码复用性不强
  2. 出现代码冗余,而且代码 不利于维护,同时 也不利于功能的扩展。
  3. 解决方法-通过 继承方式来解决

🏷 11.4.2 继承基本介绍和示意图 继承可以解决代码复用,让我们的编程更加靠近人类思维。当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体(比如刚才的Student),在该结构体中定义这些相同的属性和方法。其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个 Student 匿名结构体即可。 [ 画出示意图]

也就是说:在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。

# 11.4.3 嵌套匿名结构体的基本语法

type Goods struct {
	Name string
	Price int
}

type Book struct {
	Goods //这里就是嵌套匿名结构体 Goods
	Writer string
}

1
2
3
4
5
6
7
8
9
10

# 11.4.4 快速入门案例

🏷 案例 我们对 extends01.go 改进,使用嵌套匿名结构体的方式来实现继承特性,请大家注意体会这样编程的好处

🏷 代码实现


package main  
  
import (  
   "fmt"  
)  
  
//编写一个学生考试系统  
type Student struct {  
   Name  string  
   Age   int  
   Score int  
}  
  
//将 Pupil 和 Graduate 共有的方法也绑定到 *Student  
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 匿名结构体  
}  
  
//显示他的成绩  
//这时 Pupil 结构体特有的方法,保留  
func (p *Pupil) testing() {  
   fmt.Println("小学生正在考试中.....")  
}  
  
//大学生, 研究生。。  
//大学生  
type Graduate struct {  
   Student //嵌入了 Student 匿名结构体  
}  
  
//显示他的成绩  
//这时 Graduate 结构体特有的方法,保留  
func (p *Graduate) testing() {  
   fmt.Println("大学生正在考试中.....")  
}  
  
//代码冗余.. 高中生....  
func main() {  
   //当我们对结构体嵌入了匿名结构体使用方法会发生变化  
   pupil := &Pupil{}  
   pupil.Student.Name = "tom~"  
   pupil.Student.Age = 8  
   pupil.testing()  
   pupil.Student.SetScore(70)  
   pupil.Student.ShowInfo()  
   graduate := &Graduate{}  
   graduate.Student.Name = "mary~"  
   graduate.Student.Age = 28  
   graduate.testing()  
   graduate.Student.SetScore(90)  
   graduate.Student.ShowInfo()  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

# 11.4.5 继承给编程带来的便利

  1. 代码的复用性提高了
  2. 代码的扩展性和维护性提高了

# 11.4.6 继承的深入讨论

  1. 结构体可以 使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。【举例说明】

package main  
  
import "fmt"  
  
type A struct {  
   Name string  
   age  int  
}  
  
func (a *A) sayok() {  
   fmt.Println("A Sayok", a.Name)  
}  
  
func (a *A) hello() {  
   fmt.Println("A hello", a.Name)  
}  
  
type B struct {  
   A  
}  
  
func main() {  
   var b B  
   b.A.Name = "tom"  
   b.A.age = 19  
   b.A.sayok()  
   b.A.hello()  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  1. 匿名结构体字段访问可以简化,如图

func main() {  
   var b B  
   b.A.Name = "tom"  
   b.A.age = 19  
   b.A.sayok()  
   b.A.hello()  
  
   //上面的写法可以简化  
   b.Name = "smith"  
   b.age = 20  
   b.sayok()  
   b.hello()  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

对上面的代码小结

  1. 当我们直接通过 b 访问字段或方法时,其执行流程如下比如 b.Name
  2. 编译器会先看 b 对应的类型有没有 Name, 如果有,则直接调用 B 类型的 Name 字段
  3. 如果没有就去看 B 中嵌入的匿名结构体 A 有没有声明 Name 字段,如果有就调用,如果没有继续查找..如果都找不到就报错.
  1. 当 结构体和 匿名结构体有相同的字段或者方法时, 编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分【举例说明】
func main() {  
   var b B  
   b.name = "tom" //这时就近原则,会访问B结构体的name字段  
   //b.A.name就明确指定访问A匿名结构体的字段name  
   b.A.name = "jack"  
   b.Age = 78  
   b.say()  
   //这时就近原则,会访问B结构体的sy函数  
   b.Hello()  
   //b.A.He11o()就明确指定访问A匿名结构体的方法He11o()  
   b.A.Hello()  
}
1
2
3
4
5
6
7
8
9
10
11
12
  1. 结构体嵌入两个(或多个)匿名结构体,如 两个匿名结构体有相同的字段和方法( 同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。【举例说明】

package main  
  
import "fmt"  
  
type A struct {  
   Name string  
   age  int  
}  
  
type B struct {  
   Name  string  
   Score float64  
}  
  
type C struct {  
   A  
   B   
}  
  
//Name string  
func main() {  
   var c C  
   //如果c没有Name字段,而A和B有Name, 这时就必须通过指定置名结构体名字来区分  
   //所以c.Name就会包编译错误,这个规则对方法也是一样的!  
   c.A.Name = "tom" //error  
   fmt.Println("c")  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  1. 如果一个 struct 嵌套了一个有名结构体,这种模式就是 组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字

  2. 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个 匿名结构体字段的值


package main  
  
import "fmt"  
  
type Goods struct {  
   Name  string  
   Price float64  
}  
  
type Brand struct {  
   Name    string  
   Address string  
}  
  
type TV struct {  
   Goods  
   Brand}  
  
type Tv2 struct {  
   *Goods  
   *Brand  
}  
  
func main() {  
   //嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值  
   tv := TV{Goods{"电视机a81", 5086.99}, Brand{"海尔", "山东"}}  
  
   tv2 := TV{  
      Goods{  
         Price: 5088.99,  
         Name:  "电视机a02",  
      },  
      Brand{  
         Name:    "夏普",  
         Address: "北京",  
      },  
   }  
  
   fmt.Println("tv", tv)  
   fmt.Println("tv2", tv2)  
  
   tv3 := Tv2{&Goods{"电视机893", 7800.99}, &Brand{"创维", "河南"}}  
  
   tv4 := Tv2{  
      &Goods{  
         Name:  "电视机884",  
         Price: 9000.99,  
      },  
      &Brand{  
         Name:    "长虹",  
         Address: "四川",  
      }}  
   fmt.Println("tv3", *tv3.Goods, *tv3.Brand)  
   fmt.Println("tv4", *tv4.Goods, *tv4.Brand)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

# 11.4.7 课堂练习

结构体的匿名字段是基本数据类型,如何访问, 下面代码输出什么


package main  
  
import "fmt"  
  
type Monster struct {  
   Name string  
   Age  int  
}  
  
type E struct {  
   Monster  
   int //匿名字段时基本数据类型  
  
   n int  
}  
  
func main() {  
   //演示一下匿名字段时基本数据类型的使用  
   var e E  
   e.Name = "狐狸精"  
   e.Age = 300  
   e.int = 20  
   e.n = 40  
   fmt.Println("e=", e)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

说明

  1. 如果一个结构体有 int 类型的匿名字段,就不能第二个。
  2. 如果需要有多个 int 的字段,则必须给 int 字段指定名字

# 11.4.8 面向对象编程-多重继承

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

🏷 案例演示 通过一个案例来说明多重继承使用


package main  
  
type Goods struct {  
   Name  string  
   Price float64  
}  
  
type Brand struct {  
   Name    string  
   Address string  
}  
  
type TV struct {  
   Goods  
   Brand
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

🏷 多重继承细节说明

  1. 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。【案例演示】

//演示访间Goods的Name  
fmt.Println(tv.Goods.Name)  
fmt.Println(tv.Price)
1
2
3
4
  1. 为了保证代码的简洁性,建议大家尽量不使用多重继承

# 11.5 接口(interface)

# 11.5.1 基本介绍

按顺序,我们应该讲解多态,但是在讲解多态前,我们需要讲解接口(interface),因为在 Golang 中 多态特性主要是通过接口来体现的。

# 11.5.2 为什么有接口

# 11.5.3 接口快速入门

这样的设计需求在 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) //  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

说明: 上面的代码就是一个接口编程的快速入门案例。

# 11.5.4 接口概念的再说明

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

# 11.5.5 基本语法

🏷 小结说明:

  1. 接口里的 所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和 高内聚低偶合的思想。
  2. Golang 中的接口, 不需要 显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中 没有 implement 这样的关键字

🏷 11.5.6 接口使用的应用场景

# 11.5.7 注意事项和细节

  1. 接口本身 不能创建实例,但是 可以指向一个实现了该接口的自定义类型的变量(实例)
package main  
  
import "fmt"  
  
type AInterface interface {  
   say()  
}  
  
type stu struct {  
   Name string  
}  
  
func (stu stu) say() {  
   fmt.Println("stu say()")  
}  
  
func main() {  
   var stu stu //结构体变量,实现了Say()  
   //实现了AInterface  
   var a AInterface = stu  
   a.say()  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  1. 接口中所有的方法都没有方法体,即都是没有实现的方法。
  2. 在 Golang 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口。
  3. 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
  4. 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。

var i integer = 10  
var b AInterface = i  
b.Say() //integer Say i =10
1
2
3
4
  1. 一个自定义类型可以实现多个接口
package main  
  
import "fmt"  
  
type AInterface interface {  
   say()  
}  
  
type BInterface interface {  
   Hello()  
}  
  
type Monster struct {  
}  
  
func (m Monster) Hello() {  
   fmt.Println("Monster Hello()~~")  
}  
  
func (m Monster) say() {  
   fmt.Println("Monster say()~~")  
}  
  
func main() {  
   //Monster实现了AInterface和BInterface  
   var monster Monster  
   var a2 AInterface = monster  
   var b2 BInterface = monster  
   a2.say()  
   b2.Hello()  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  1. Golang 接口中不能有任何变量

type AInterface interface {  
   Name string // 错误  
   Test01()  
   Test02()  
}
1
2
3
4
5
6
  1. 一个接口(比如 A 接口)可以继承多个别的接口(比如 B,C 接口),这时如果要实现 A 接口,也必须将 B,C 接口的方法也全部实现。

package main  
  
import (  
   _ "fmt"  
)  
  
type BInterface interface {  
   test01()  
}  
  
type CInterface interface {  
   test02()  
}  
  
type AInterface interface {  
   BInterface  
   CInterface   test03()  
}  
  
//如果需要实现AInterface,就需要将BInterface cInterface的方法都实现  
type Stu struct {  
   }  
  
func (stu Stu) teste1() {  
  
}  
func (stu Stu) teste2() {  
  
}  
func (stu Stu) test03() {  
  
}  
func main() {  
   var stu Stu  
   var a AInterface = stu //报错  
   a.test01()  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  1. interface 类型默认是一个指针(引用类型),如果没有对 interface 初始化就使用,那么会输出 nil
  2. 空接口 interface{} 没有任何方法, 所以所有类型都实现了空接 口, 即我们可以 把任何一个变量赋给空接口。

  
type T interface {  
}
1
2
3
4

# 11.5.9 接口编程的最佳实践

🏷 实现对 Hero 结构体切片的排序: sort.Sort(data Interface)

✒ 实现对 Hero 结构体切片的排序: sort.Sort(data Interface)


package main  
  
import (  
   "fmt"  
   "math/rand"   "sort")  
  
//1.声明 Hero 结构体  
type Hero struct {  
   Name string  
   Age  int  
}  
  
//2.声明一个 Hero 结构体切片类型  
type HeroSlice []Hero  
  
//3.实现 Interface 接口  
func (hs HeroSlice) Len() int {  
   return len(hs)  
}  
  
//Less 方法就是决定你使用什么标准进行排序  
//1. 按 Hero 的年龄从小到大排序!!  
func (hs HeroSlice) Less(i, j int) bool {  
   return hs[i].Age < hs[j].Age  
   //修改成对 Name 排序  
   //return hs[i].Name < hs[j].Name  
  
}  
func (hs HeroSlice) Swap(i, j int) {  
   //交换  
   // temp := hs[i]  
   // hs[i] = hs[j]   // hs[j] = temp   //下面的一句话等价于三句话  
   hs[i], hs[j] = hs[j], hs[i]  
}  
  
//1.声明 Student 结构体  
type Student struct {  
   Name  string  
   Age   int  
   Score float64  
}  
  
//将 Student 的切片,安 Score 从大到小排序!!  
func main() {  
   //先定义一个数组/切片  
   var intSlice = []int{0, -1, 10, 7, 90}  
   //要求对 intSlice 切片进行排序  
   //1. 冒泡排序...  
   //2. 也可以使用系统提供的方法  
   sort.Ints(intSlice)  
   fmt.Println(intSlice)  
   //请大家对结构体切片进行排序  
   //1. 冒泡排序...  
   //2. 也可以使用系统提供的方法  
   //测试看看我们是否可以对结构体切片进行排序  
   var heroes HeroSlice  
   for i := 0; i < 10; i++ {  
      hero := Hero{  
         Name: fmt.Sprintf("英雄|%d", rand.Intn(100)),  
         Age:  rand.Intn(100),  
      }  
      //将 hero append 到 heroes 切片  
      heroes = append(heroes, hero)  
   }  
   //看看排序前的顺序  
   for _, v := range heroes {  
      fmt.Println(v)  
   }  
   //调用 sort.Sort   sort.Sort(heroes)  
   fmt.Println("-----------排序后------------")  
   //看看排序后的顺序  
   for _, v := range heroes {  
      fmt.Println(v)  
   }  
   i := 10  
   j := 20  
   i, j = j, i  
   fmt.Println("i=", i, "j=", j) // i=20 j = 10  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

🏷 接口编程的课后练习

//1.声明 Student 结构体
type Student struct{
	Name string
	Age int
	Score float64
}
1
2
3
4
5
6

//将 Student 的切片,安 Score 从大到小排序!!

# 11.5.10 实现接口 vs 继承

🏷 大家听到现在,可能会对实现接口和继承比较迷茫了, 这个问题,那么他们究竟有什么区别呢

代码说明:


package main  
  
import "fmt"  
  
//Monkey结构体  
type Monkey struct {  
   Name string  
}  
  
//声明接口  
type BirdAble interface {  
   Flying()  
}  
  
type FishAble interface {  
   Swimming()  
}  
  
func (this *Monkey) climbing() {  
   fmt.Println(this.Name, "生来会爬树.")  
}  
  
//LittleMonkey:结构体  
type LittleMonkey struct {  
   Monkey //继承  
}  
  
//让LittleMonkey实现BirdAble  
func (this *LittleMonkey) Flying() {  
   fmt.Println(this.Name, "通过学习,会飞翔..")  
}  
  
//让LittleMonkey:实现FishAble  
func (this *LittleMonkey) Swimming() {  
   fmt.Println(this.Name, "通过学习,会游泳.")  
}  
  
func main() {  
   //创建一个Litt1 eMonkey实例  
   monkey := LittleMonkey{  
      Monkey{  
         Name: "悟空",  
      },  
   }  
   monkey.climbing()  
   monkey.Flying()  
   monkey.Swimming()  
  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

🏷 对上面代码的小结

  1. 当 A 结构体继承了 B 结构体,那么 A 结构就自动的继承了 B 结构体的字段和方法,并且可以直 接使用
  2. 当 A 结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此我们可以认为:实现接口是对继承机制的补充.

🏷 实现接口可以看作是对 继承的一种补充

🏷 接口和继承解决的解决的问题不同 继承的价值主要在于:解决代码的 复用性和 可维护性。 接口的价值主要在于: 设计,设计好各种规范(方法),让其它自定义类型去实现这些方法。

🏷 接口比继承更加灵活 Person Student BirdAble LittleMonkey 接口比继承更加灵活,继承是满足 is - a 的关系,而接口只需满足 like - a 的关系。

🏷 接口在一定程度上实现 代码解耦

# 11.6 面向对象编程-多态

# 11.6.1 基本介绍

变量(实例)具有多种形态。面向对象的第三大特征,在 Go 语言,多态特征是通过接口实现的。可 以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。

# 11.6.2 快速入门

在前面的 Usb 接口案例,Usb usb ,既可以接收手机变量,又可以接收相机变量,就体现了 Usb 接口 多态特性。[点明]

# 11.6.3 接口体现多态的两种形式

🏷 多态参数 在前面的 Usb 接口案例,Usb usb ,即可以接收手机变量,又可以接收相机变量,就体现了 Usb 接口 多态。

🏷 多态数组 演示一个案例:给 Usb 数组中,存放 Phone 结构体 和 Camera 结构体变量

案例说明:


package main  
  
import (  
   "fmt"  
)  
  
//声明/定义一个接口  
type Usb interface {  
   //声明了两个没有实现的方法  
   Start()  
   Stop()  
}  
type Phone struct {  
   name string  
}  
  
//让 Phone 实现 Usb 接口的方法  
func (p Phone) Start() {  
   fmt.Println("手机开始工作。。。")  
}  
func (p Phone) Stop() {  
   fmt.Println("手机停止工作。。。")  
}  
  
type Camera struct {  
   name string  
}  
  
//让 Camera 实现 Usb 接口的方法  
func (c Camera) Start() {  
   fmt.Println("相机开始工作。。。")  
}  
func (c Camera) Stop() {  
   fmt.Println("相机停止工作。。。")  
}  
func main() {  
   //定义一个 Usb 接口数组,可以存放 Phone 和 Camera 的结构体变量  
   //这里就体现出多态数组  
   var usbArr [3]Usb  
   usbArr[0] = Phone{"vivo"}  
   usbArr[1] = Phone{"小米"}  
   usbArr[2] = Camera{"尼康"}  
   fmt.Println(usbArr)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# 11.7 类型断言

# 11.7.1 由一个具体的需要,引出了类型断言.

# 11.7.2 基本介绍

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


func main() {  
   //类型断言的其它案例  
   var x interface{}  
   var b2 float32 = 1.1  
   x = b2 //空接口,可以接收任意类型  
   //x=>f1oat32[使用类型断言]  
   y := x.(float32)  
   fmt.Printf("y的类型是%T值是=%v", y, y)  
}
1
2
3
4
5
6
7
8
9
10

🏷 对上面代码的说明: 在进行类型断言时,如果类型不匹配,就会报 panic, 因此进行类型断言时,要确保原来的空接口指向的就是断言的类型.

🏷 如何在进行断言时,带上检测机制,如果成功就 ok,否则也不要报 panic


func main() {  
   //类型断言(带检测的)  
   var x interface{}  
   var b2 float32 = 2.1  
   x = b2 //空接口,可以接收任意类型  
   //x=>f1oat32[使用类型断言]  
   //类型断言(带检测的)  
   if y, ok := x.(float32); ok {  
      fmt.Println("convert success")  
      fmt.Printf("y的类型是%T值是=%v", y, y)  
   } else {  
      fmt.Println("convert fail")  
      fmt.Println("继续执行.:.")  
   }   
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 11.7.3 类型断言的最佳实践 1

🏷 在前面的 Usb 接口案例做改进: 给 Phone 结构体增加一个特有的方法 call(), 当 Usb 接口接收的是 Phone 变量时,还需要调用 call 方法, 走代码:


package main  
  
import (  
   "fmt"  
)  
  
//声明/定义一个接口  
type Usb interface {  
   //声明了两个没有实现的方法  
   Start()  
   Stop()  
}  
type Phone struct {  
   name string  
}  
  
//让 Phone 实现 Usb 接口的方法  
func (p Phone) Start() {  
   fmt.Println("手机开始工作。。。")  
}  
func (p Phone) Stop() {  
   fmt.Println("手机停止工作。。。")  
}  
func (p Phone) Call() {  
   fmt.Println("手机 在打电话..")  
}  
  
type Camera struct {  
   name string  
}  
  
//让 Camera 实现 Usb 接口的方法  
func (c Camera) Start() {  
   fmt.Println("相机开始工作。。。")  
}  
func (c Camera) Stop() {  
   fmt.Println("相机停止工作。。。")  
}  
  
type Computer struct {  
}  
  
func (computer Computer) Working(usb Usb) {  
   usb.Start()  
   //如果 usb 是指向 Phone 结构体变量,则还需要调用 Call 方法  
   // 类型断言..[ 注意体会!!!]  
   if phone, ok := usb.(Phone); ok {  
      phone.Call()  
   }  
   usb.Stop()  
}  
func main() {  
   //定义一个 Usb 接口数组,可以存放 Phone 和 Camera 的结构体变量  
   //这里就体现出多态数组  
   var usbArr [3]Usb  
   usbArr[0] = Phone{"vivo"}  
   usbArr[1] = Phone{"小米"}  
   usbArr[2] = Camera{"尼康"}  
   //遍历 usbArr   //Phone 还有一个特有的方法 call(),请遍历 Usb 数组,如果是 Phone 变量,  
   //除了调用 Usb 接口声明的方法外,还需要调用 Phone 特有方法 call. =》类型断言  
   var computer Computer  
   for _, v := range usbArr {  
      computer.Working(v)  
      fmt.Println()  
   }  
   //fmt.Println(usbArr)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

# 11.7.4 类型断言的最佳实践 2

写一函数,循环判断传入参数的类型:


package main  
  
import "fmt"  
  
//编写一个函数,可以判断输入的参数是什么类型  
func TypeJudge(items ...interface{}) {  
   for index, x := range items {  
      switch x.(type) {  
      case bool:  
         fmt.Printf("第%v个参数是bool类型,值是%v\n", index, x)  
      case float32:  
         fmt.Printf("第%v个参数是f1oat32类型,值是%v\n", index, x)  
      case float64:  
         fmt.Printf("第%v个参数是f1oat64类型,值是%v\n", index, x)  
      case int, int32, int64:  
         fmt.Printf("第%v个参数是整数类型,值是%v\n", index, x)  
      case string:  
         fmt.Printf("第%v个参数是string类型,值是%v\n", index, x)  
      default:  
         fmt.Printf("第%v个参数是类型不确定,值是%v\n", index, x)  
      }  
  
   }}  
func main() {  
   var n1 float32 = 1.1  
   var n2 float64 = 2.3  
   var n3 int32 = 30  
   var name string = "tom"  
   address := "北京"  
   n4 := 300  
   TypeJudge(n1, n2, n3, name, address, n4)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 11.7.5 类型断言的最佳实践 3 【学员自己完成】

在前面代码的基础上,增加判断 Student 类型和 * Student 类型


//编写一个函数,可以判断输入的参数是什么类型  
func TypeJudge(items ...interface{}) {  
   for index, x := range items {  
      switch x.(type) {  
      case bool:  
         fmt.Printf("第%v个参数是bool类型,值是%v\n", index, x)  
      case float32:  
         fmt.Printf("第%v个参数是f1oat32类型,值是%v\n", index, x)  
      case float64:  
         fmt.Printf("第%v个参数是f1oat64类型,值是%v\n", index, x)  
      case int, int32, int64:  
         fmt.Printf("第%v个参数是整数类型,值是%v\n", index, x)  
      case string:  
         fmt.Printf("第%v个参数是string类型,值是%v\n", index, x)  
      default:  
         fmt.Printf("第%v个参数是类型不确定,值是%v\n", index, x)  
      }  

   }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20