Go第 10 章 :面向对象编程(上)
10.1 结构体
10.1.1 看一个问题
10.1.2 使用现有技术解决
- 单独的定义变量解决
代码演示:
- 使用数组解决 代码演示:
10.1.3 现有技术解决的缺点分析
- 使用变量或者数组来解决养猫的问题,不利于数据的管理和维护。因为名字,年龄,颜色都是属于一只猫,但是这里是分开保存。
- 如果我们希望对一只猫的属性(名字、年龄,颜色)进行操作(绑定方法), 也不好处理。
- 引出我们要讲解的技术-》结构体。
10.1.4 一个程序就是一个世界,有很多对象(变量)
10.1.5 Golang 语言面向对象编程说明
-
- Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对 象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。
-
- Golang 没有类(class),Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可 以理解 Golang 是基于 struct 来实现 OOP 特性的。
-
- Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函 数、隐藏的 this指针等等
-
- Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不 一样,比如继承 :Golang 没有 extends 关键字,继承是通过匿名字段来实现。
-
- Golang 面向对象(OOP)很优雅,OOP 本身就是语言类型系统(type system)的一部分,通过接口 (interface)关联,耦合性低,也非常灵活。后面同学们会充分体会到这个特点。也就是说在 Golang 中面 向接口编程是非常重要的特性。
10.1.6 结构体与结构体变量(实例/对象)的关系示意图
对上图的说明
- 将一类事物的特性提取出来(比如猫类), 形成一个新的数据类型, 就是一个结构体。
- 通过这个结构体,我们可以创建多个变量(实例/对象)
- 事物可以猫类,也可以是 Person , Fish 或是某个工具类。。。
10.1.7 快速入门-面向对象的方式(struct)解决养猫问题
10.1.8 结构体和结构体变量(实例)的区别和联系
通过上面的案例和讲解我们可以看出:
- 结构体是自定义的数据类型,代表一类事物.
- 结构体变量(实例)是具体的,实际的,代表一个具体变量
10.1.9 结构体变量(实例)在内存的布局(重要!)
10.1.10 如何声明结构体
10.1.11 字段/属性
- 基本介绍
- 从概念或叫法上看: 结构体字段 = 属性 = field (即授课中,统一叫字段)
- 字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。比如我们前面定义猫结构体 的 Name string 就是属性
- 注意事项和细节说明
- 字段声明语法同变量,示例:字段名 字段类型
- 字段的类型可以为:基本类型、数组或引用类型
- 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:
布尔类型是 false ,数值是 0 ,字符串是 “”。
数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0, 0, 0]
指针,slice,和 map 的零值都是 nil ,即还没有分配空间。
案例演示:
- 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体 是值类型。
案例:
10.1.12 创建结构体变量和访问结构体字段
-
方式 1-直接声明
案例演示: var person Person
前面我们已经说了。 -
方式 2-{}
案例演示: var person Person = Person{}
-
方式 3-&
案例: var person *Person = new (Person)
- 方式 4-{}
案例: var person *Person = &Person{}
说明:
- 第 3 种和第 4 种方式返回的是 结构体指针。
- 结构体指针访问字段的标准方式应该是:(*结构体指针).字段名 ,比如 (*person).Name = “tom”
- 但 go 做了一个简化,也支持 结构体指针.字段名, 比如 person.Name = “tom”。更加符合程序员 使用的习惯,go 编译器底层 对 person.Name 做了转化 (*person).Name。
10.1.13 struct 类型的内存分配机制
10.1.14 结构体使用注意事项和细节
- 结构体的所有字段在内存中是连续的
- 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类 型)
- 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
- struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是序 列化和反序列化。
10.2 方法
10.2.1 基本介绍
在某些情况下,我们要需要声明(定义)方法。比如 Person 结构体:除了有一些字段外( 年龄,姓名…),Person 结构体还有一些行为比如:可以说话、跑步…,通过学习,还可以做算术题。这时就要用方法 才能完成。
Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型, 都可以有方法,而不仅仅是 struct。
10.2.2 方法的声明和调用
对上面的总结
- test 方法和 Person 类型绑定
- test 方法只能通过 Person 类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
- func (p Person) test() {}… p 表示哪个 Person 变量调用,这个 p 就是它的副本, 这点和函数传参非 常相似。
- p 这个名字,有程序员指定,不是固定, 比如修改成 person 也是可以
10.2.3 方法快速入门
- 给 Person 结构体添加 speak 方法,输出 xxx 是一个好人
- 给 Person 结构体添加 jisuan 方法,可以计算从 1+…+1000 的结果, 说明方法体内可以函数一样, 进行各种运算
- 给 Person 结构体 jisuan2 方法,该方法可以接收一个数 n,计算从 1+…+n 的结果
- 给 Person 结构体添加 getSum 方法,可以计算两个数的和,并返回结果
- 方法的调用
10.2.4 方法的调用和传参机制原理:(重要!)
说明:
方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做 实参也传递给方法。下面我们举例说明。
案例 1:
画出前面 getSum方法的执行过程+说明
说明:
- 在通过一个变量去调用方法时,其调用机制和函数一样
- 不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝)
在方法栈中的对象 p是main 中的对象p的一个副本,是一个单独实例存在于getSum栈中
10.2.5 方法的声明(定义)
func (recevier type) methodName(参数列表) (返回值列表){
方法体
return 返回值
}
- 参数列表:表示方法输入
- recevier type : 表示这个方法和 type 这个类型进行绑定,或者说该方法作用于 type 类型
- receiver type : type 可以是结构体,也可以其它的自定义类型
- receiver : 就是 type 类型的一个变量(实例),比如 :Person 结构体 的一个变量(实例)
- 返回值列表:表示返回的值,可以多个
- 方法主体:表示为了实现某一功能代码块
- return 语句不是必须的。
10.2.6 方法的注意事项和细节
- 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
- 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
- Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型, 都可以有方法,而不仅仅是 struct, 比如 int , float32 等都可以有方法
- 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母 大写,可以在本包和其它包访问。[讲解]
- 如果一个类型实现了 String()这个方法,那么 fmt.Println 默认会调用这个变量的 String()进行输出. 新版本的golang不可使用
10.2.7 方法的课堂练习题
- 编写结构体(MethodUtils),编程一个方法,方法不需要参数,在方法中打印一个 10*8 的矩形,
在 main方法中调用该方法。
type MethodUtils struct{
//..
}
func (m MethodUtils) Print(){
for i:=0;i<9;i++{
for j:=0;j<7;j++{
fmt.Print("*")
}
fmt.Println()
}
}
func main() {
//var i integer =10
//i.print()
//i.change()
//fmt.Println("i=",i)
//fmt.Println(i)
var m MethodUtils
m.Print()
}
- 编写一个方法,提供 m 和 n 两个参数,方法中打印一个 m*n 的矩形
- 编写一个方法算该矩形的面积(可以接收长 len,和宽 width), 将其作为方法返回值。在 main 方法中调用该方法,接收返回的面积值并打印。
- 编写方法:判断一个数是奇数还是偶数
- 根据行、列、字符打印 对应行数和列数的字符,比如:行:3,列:2,字符*,则打印相应的效果
- 定义小小计算器结构体(Calcuator),实现加减乘除四个功能
实现形式 1:分四个方法完成:
实现形式 2:用一个方法搞定
type MethodUtils struct {
Num1 float64
Num2 float64
}
func (mu *MethodUtils) getRes(operator byte)float64{
res := 0.0
switch operator{
case '+':
res = mu.Num1 + mu.Num2
case '-':
res = mu.Num1 - mu.Num2
case '*':
res = mu.Num1 * mu.Num2
case '/':
res = mu.Num1 / mu.Num2
default:
fmt.Println("运算符输入错误")
}
return res
}
func main() {
//m:=MethodUtils{1,2}
//var m MethodUtils = MethodUtils{1,2}
//var m *MethodUtils = new(MethodUtils)
//(*m).Num2=1
//(*m).Num1=2
//m.Num2=3
//m.Num1=4
var m *MethodUtils = &MethodUtils{1,2}
s := m.getRes('-')
fmt.Println(s)
}
10.2.8 方法的课后练习题
package main
import "fmt"
type MethidUtils struct{
num1 int
}
func (mu *MethidUtils)multiplication( ){
for i:=1;i<=mu.num1;i++{
for j:=1;j<=i;j++{
fmt.Printf("%v*%v=%v \t",j,i,i*j)
if i == j {
fmt.Println()
}
}
}
}
func main() {
var m *MethidUtils
fmt.Println("请输入乘法表")
fmt.Scanln(&m.num1)
m.multiplication()
}
package main
import "fmt"
//type MethidUtils struct {
// num1 int
//}
type MethodUtils struct {
}
//func (mu *MethidUtils) multiplication() {
// for i := 1; i <= mu.num1; i++ {
// for j := 1; j <= i; j++ {
// fmt.Printf("%v*%v=%v \t", j, i, i*j)
// if i == j {
// fmt.Println()
// }
// }
//
// }
//}
func (mu *MethodUtils) exchange(array [3][3]int) {
var array2 [3][3]int
//遍历数组并进行转置
for i := 0; i < len(array); i++ {
for j := 0; j < len(array[i]); j++ {
array2[j][i] = array[i][j]
}
}
fmt.Println("转置后的矩阵为:")
//遍历数组
for i := 0; i < len(array2); i++ {
for j := 0; j < len(array2[i]); j++ {
fmt.Print(array2[i][j], " ")
}
fmt.Println()
}
}
func main() {
//var m *MethidUtils
//fmt.Println("请输入乘法表")
//fmt.Scanln(&m.num1)
//
//m.multiplication()
var n *MethodUtils
var arr = [3][3]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}
fmt.Println("转置前的矩阵为:")
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr[i]); j++ {
fmt.Printf("%v ", arr[i][j])
}
fmt.Println()
}
n.exchange(arr)
}
10.2.9 方法和函数区别
-
调用方式不一样
函数的调用方式: 函数名(实参列表)
方法的调用方式: 变量.方法名(实参列表) -
对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
-
对于方法(如 struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反 过来同样也可以
总结:
- 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
- 如果是和值类型,比如 (p Person) , 则是值拷贝, 如果和指针类型,比如是 (p *Person) 则是地址拷贝。
10.3 面向对象编程应用实例
10.3.1 步骤
- 声明(定义)结构体,确定结构体名
- 编写结构体的字段
- 编写结构体的方法
10.3.2 学生案例:
- 编写一个 Student 结构体,包含 name、gender、age、id、score 字段,分别为 string、string、int、 int、float64 类型。
- 结构体中声明一个 say 方法,返回 string 类型,方法返回信息中包含所有字段值。
- 在 main 方法中,创建 Student 结构体实例(变量),并访问 say 方法,并将调用结果打印输出。
- 走代码
package main
import "fmt"
type Student struct {
name string
gender string
age int
id uint64
score float64
}
func (s *Student) say() string {
var information string
information = fmt.Sprintf("学生的姓名为%v,性别为%v,年龄为:%v,id号为:%v,分数为:%v", s.name, s.gender, s.age, s.id, s.score)
return information
}
func main() {
var student *Student = &Student{"tianyi", "男", 18, 8848, 100.0}
//var student = Student{
// name:"tom",
// gender:"male",
// age : 18,
// id:8848,
// score:100.0,
//}
info := student.say()
fmt.Println(info)
}
10.3.3 小狗案例 [学员课后练习]
- 编写一个 Dog 结构体,包含 name、age、weight 字段
- 结构体中声明一个 say 方法,返回 string 类型,方法返回信息中包含所有字段值。
- 在 main 方法中,创建 Dog 结构体实例(变量),并访问 say 方法,将调用结果打印输出
10.3.4 盒子案例
- 编程创建一个 Box 结构体,在其中声明三个字段表示一个立方体的长、宽和高,长宽高要从终 端获取
- 声明一个方法获取立方体的体积。
- 创建一个 Box 结构体变量,打印给定尺寸的立方体的体积
- 走代码
10.3.5 景区门票案例
- 一个景区根据游人的年龄收取不同价格的门票,比如年龄大于 18,收费 20 元,其它情况门票免 费.
- 请编写 Visitor 结构体,根据年龄段决定能够购买的门票价格并输出
- 代码:
package main
import "fmt"
type Vistor struct {
Name string
Age int
}
func (v *Vistor) showPrice(){
if v.Age >=90 ||v.Age<=8{
fmt.Println("考虑到安全,别玩了")
return
}else if v.Age>18{
fmt.Printf("游客的名字为%v 年龄为%v 收费20\n",v.Name,v.Age)
}else{
fmt.Printf("游客的名字为%v 年龄为%v free\n",v.Name,v.Age)
}
}
func main() {
var v Vistor
for {
fmt.Println("请输入你的名字")
fmt.Scanln(&v.Name)
if v.Name=="n"{
fmt.Println("退出程序")
break
}
fmt.Println("请输入你的年龄")
fmt.Scanln(&v.Age)
v.showPrice()
}
}
10.4 创建结构体变量时指定字段值
说明
Golang 在创建结构体实例(变量)时,可以直接指定字段的值
方式 1
方式 2
10.5 工厂模式
10.5.1 说明
Golang 的结构体没有构造函数,通常可以使用工厂模式来解决这个问题
10.5.2 看一个需求
10.5.4 思考题
同学们思考一下,如果 model 包的 student 的结构体的字段 Score 改成 score,我们还能正常访问
吗?又应该如何解决这个问题呢?[老师给出思路,学员自己完成]