【Go笔记】第 10 章 面向对象(上)

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

# 10.1 结构体

# 10.1.1 看一个问题

张老太养了两只猫猫:一只名字叫小白,今年3岁,白色。还有一 只叫小花,今年100岁,花色。请编写一个程序,当用户输入小猫 的名字时,就显示该猫的名字,年龄,颜色。如果用户输入的 小猫名错误,则显示张老太没有这只猫猫。

# 10.1.2 使用现有技术解决

  1. 单独的定义变量解决 代码演示:
	//1.使用变量的处理  
	var cat1Name string = "小白"  
	var catlAge int = 3  
	var cat1Color string = "白色"  
	var cat2Name string = "小花"  
	var cat2Age int = 100  
	var cat2Color string = "花色"
1
2
3
4
5
6
7
  1. 使用数组解决 代码演示:
//2.使用数组解决  
var catNames [2]string = [...]string{"小白", "小花"}  
var catAges [2]int = [...]int{3, 109}  
var catcolors [2]string = [...]string{"白色", "花色"}  
//..map[string]string
1
2
3
4
5

# 10.1.3 现有技术解决的缺点分析

  1. 使用变量或者数组来解决养猫的问题,不利于数据的管理和维护。因为名字,年龄,颜色都是属于一只猫,但是这里是分开保存。
  2. 如果我们希望对一只猫的属性(名字、年龄,颜色)进行操作(绑定方法), 也不好处理。
  3. 引出我们要讲解的技术-》 结构体。

# 10.1.4 一个程序就是一个世界,有很多对象(变量)

# 10.1.5 Golang 语言面向对象编程说明

  1. Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。
  2. Golang 没有类(class),Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解 Golang 是基于 struct 来实现 OOP 特性的。
  3. Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等等
  4. Golang 仍然有面向对象编程的 继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承 :Golang 没有 extends 关键字,继承是通过匿名字段来实现。
  5. Golang 面向对象(OOP)很优雅,OOP 本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。后面同学们会充分体会到这个特点。也就是说在 Golang 中面向接口编程是非常重要的特性。

# 10.1.6 结构体与结构体变量(实例/对象)的关系示意图

🏷 对上图的说明

  1. 将一类事物的特性提取出来(比如猫类), 形成一个新的数据类型, 就是一个结构体。
  2. 通过这个结构体,我们可以创建多个变量(实例/对象)
  3. 事物可以猫类,也可以是 Person , Fish 或是某个工具类。。。

🏷 上图说明 注意:从猫结构体到变量,就是创建一个Ct结构体变量,也可以说是定义一个Cat结构体变量 当然:上面的猫也可是鱼、狗、人。

# 10.1.7 快速入门-面向对象的方式(struct)解决养猫问题

🏷 代码演示

package main  
  
import "fmt"  
  
func main() {  
   //定义一个Cat结构体,将cat的各个字段/属性信息,放入到cat结构体进行管理  
   type Cat struct {  
      Name  string  
      Age   int  
      Color string  
      Hobby string  
   }  
  
   //创建一个cat的变量  
   var cat1 Cat //var a int  
   cat1.Name = "小白"  
   cat1.Age = 3  
   cat1.Color = "白色"  
   cat1.Hobby = "吃·))><<"  
   fmt.Println("cati=", cat1)  
   fmt.Println("猫猫的信息如下:")  
   fmt.Println("name=", cat1.Name)  
   fmt.Println("Age=", cat1.Age)  
   fmt.Println("color=", cat1.Color)  
   fmt.Println("hobby=", cat1.Hobby)  
}
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

# 10.1.8 结构体和结构体变量(实例)的区别和联系

通过上面的案例和讲解我们可以看出:

  1. 结构体是自定义的数据类型,代表一类事物.
  2. 结构体变量(实例)是具体的,实际的,代表一个具体变量

# 10.1.9 结构体变量(实例)在内存的布局(重要!)

# 10.1.10 如何声明结构体

🏷基本语法 type 结构体名称 struct { field1 type field2 type }

🏷举例: type Student struct { Name string //字段 Age int //字段 Score float32 }

# 10.1.11 字段/属性

🏷基本介绍

  1. 从概念或叫法上看: 结构体字段 = 属性 = field (即授课中,统一叫字段)
  2. 字段是结构体的一个组成部分,一般是基本数据类型、 数组,也可是 引用类型。比如我们前面定义猫结构体 的 Name string 就是属性

🏷 注意事项和细节说明

  1. 字段声明语法同变量,示例:字段名 字段类型
  2. 字段的类型可以为:基本类型、数组或引用类型
  3. 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样: 布尔类型是 false ,数值是 0 ,字符串是 ""。 数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0, 0, 0] 指针,slice ,和 map 是 的零值都是 nil ,即还没有分配空间。 案例演示:
package main  
  
import "fmt"  
  
//如果结构体的字段类型是:指针,slice,和map的零值都是ni1,即还没有分配空间  
//如果需要使用这样的字段,需要先make,才能使用.  
type Person struct {  
   Name   string  
   Age    int  
   Scores [5]float64  
   ptr    *int              //指针  
   slice  []int             //切片  
   map1   map[string]string //map  
}  
  
func main() {  
   //定义结构体变量  
   var p1 Person  
   fmt.Println(p1)  
  
   if p1.ptr == nil {  
      fmt.Println("ok1")  
   }  
  
   if p1.slice == nil {  
      fmt.Println("ok2")  
   }  
  
   if p1.map1 == nil {  
      fmt.Println("ok3")  
   }  
  
   //使用s1ice,再次说明,一定要make  
   p1.slice = make([]int, 10)  
   p1.slice[0] = 100 //ok  
  
   //使用map, 一定要先make  
   p1.map1 = make(map[string]string)  
  
   p1.map1["key1"] = "tom~"  
   fmt.Println(p1)  
}
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
  1. 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体是值类型。 案例:
type Monster struct{
	Name string
	Age int
}

//不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,
//不影响另外一个,结构体是值类型
var monster1 Monster
monsterl.Name ="牛魔主"
monster1.Age = 500

monster2 := monster1 //结构体是值类型,款认为值拷贝
monster2.Name="青牛精"
fmt.Println("monster:1=",monster1) //monster1={牛魔王5oo】
fmt.Println("monster2=",monster2) //monster2={青牛精50o]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

画出上面代码的内存示意图:

# 10.1.12 创建结构体变量和访问结构体字段

🏷 方式 1-直接声明 案例演示: var person Person 前面我们已经说了。

🏷 方式 2-{} 案例演示: var person Person = Person{}

//1方式2
p2 :Person{"mary",20}
//p2.Name "tom
//p2.Age=18
fmt.Println(p2)
1
2
3
4
5

🏷 方式 3-& 案例: var person *Person = new (Person)

//方式3-&  
//案例:var person*Person=new(Person)  
var p3 *Person = new(Person)  
//因为p3是一个指针,因此标准的给字段赋值方式  
//(*p3).Name="smith”也可以这样写p3.Name="smith"  
//原因:go的设计者为了程序员使用方便,底层会对p3.Name="smith”进行处理  
//会给p3加上取值运算(*p3).Name="smith"  
(*p3).Name = "smith"  
p3.Name = "john" //  
(*p3).Age = 30  
p3.Age = 100  
fmt.Println(*p3)
1
2
3
4
5
6
7
8
9
10
11
12

🏷 方式 4-{} 案例: var person *Person = &Person{}

//方式4-0  
//案例:var person*Person=&Person{  
//下面的语句,也可以直接给字符赋值  
//var person *Person &Person{"mary",60}  
var person *Person = &Person{}  
//因为person是一个指针,因此标准的访问字段的方法  
//(*person).Name "scott"  
//go的改计有为丁在开页使用方便,也可以person.Name =”"scott"  
//原因和上面一样,底层会对person.Name = "scott”进行处理,会加上(*person)  
(*person).Name = "scott"  
person.Name = "scott~"  
(*person).Age = 88  
person.Age = 10  
fmt.Println(*person)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

🏷 说明:

  1. 第 3 种和第 4 种方式返回的是 结构体指针。
  2. 结构体指针访问字段的标准方式应该是:(*结构体指针).字段名 ,比如 (*person).Name = "tom"
  3. 但 go 做了一个简化,持 也支持 结构体指针. 字段名, 比如 person.Name = "tom"。更加符合程序员 使用的习惯,go 层 编译器底层 对 对 person.Name 化 做了转化 (*person).Name

# 10.1.13 struct 类型的内存分配机制

🏷 看一个思考题 我们定义一个Person结构体(包括名字,年龄)

//我们看看下面一段代码,输出什么内容?
var p1 Person
p1.Age=10
p1.Name="小明"

var p2 Person p1
fmt.Println(p2.Age)
p2.Name ="tom"
fmt.Printf("p2.Name=%v p1.Name=%v",p2.Name,p1.Name)
1
2
3
4
5
6
7
8
9

输出的结果是: p2.Name = tom p1.Name = 小明

🏷 基本说明

🏷 结构体在内存中示意图

🏷 看下面代码,并分析原因

package main  
  
import "fmt"  
  
type Person struct {  
   Name string  
   Age  int  
}  
  
func main() {  
   var p1 Person  
   p1.Age = 10  
   p1.Name = "小明"  
   var p2 *Person = &p1 // 这里是关键-->画出示意图  
   fmt.Println((*p2).Age)  
   fmt.Println(p2.Age)  
   p2.Name = "tom~"  
   fmt.Printf("p2.Name=%v p1.Name=%v \n", p2.Name, p1.Name)    //tom~tom~  
   fmt.Printf("p2.Name=%v p1.Name=%v \n", (*p2).Name, p1.Name) //tom~tom~  
  
   fmt.Printf("p1的地址%p\n", &p1)  
   fmt.Printf("p2的地址%pp2的值%pln", &p2, p2)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

输出的结果是:

10
10
p2.Name=tom~ p1.Name=tom~
p2.Name=tom~ p1.Name=tom~
p1的地址0xc000004078
p2的地址0xc000006028p2的值0xc000004078ln
1
2
3
4
5
6

上面代码对应的内存图的分析:

🏷 看下面代码,并分析原因

# 10.1.14 结构体使用注意事项和细节

  1. 结构体的所有字段在 内存中是连续的
package main  
  
import "fmt"  
  
//结构体  
type Point struct {  
   x int  
   y int  
}  
  
//结构体  
type Rect struct {  
   leftup, rightDown Point  
}  
  
//结构体  
type Rect2 struct {  
   leftup, rightDown *Point  
}  
  
func main() {  
   r1 := Rect{Point{1, 2}, Point{3, 4}}  
   //r1有四个int,在内存中是连续分布  
   //打印地址  
   fmt.Printf("r1.leftup.x 地址=%p r1.leftup.y地址=%p r1.rightDown.x地址=%p r1.rightDown.y=%p", &r1.leftup.x, &r1.leftup.y, &r1.rightDown.x, &r1.rightDown.y)  
  
   //r2有两个*Point类型,这个两个*Point类型的本身地址也是连续的,  
   //但是他们指向的地址不一定是连续  
   r2 := Rect2{&Point{10, 20}, &Point{30, 40}}  
  
   //打印地址  
   fmt.Printf("r2.leftup=%p r2.rightDown本身地址=%p\n", &r2.leftup, &r2.rightDown)  
   //他们指向的地址不一定是连续..·, 这个要看系统在运行时是如何分配  
   fmt.Printf("r2.leftup=%p r2.rightDown指向地址=%p\n", r2.leftup, r2.rightDown)  
}
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

对应的分析图:

  1. 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
package main  
  
import "fmt"  
  
//结构体  
type Point struct {  
   x int  
   y int  
}  
  
type A struct {  
   Num int  
}  
  
type B struct {  
   Num int  
}  
  
func main() {  
   var a A  
   var b B  
   a = A(b)  
  
   fmt.Println(a, b)  
}
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
  1. 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转

  2. struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是 序 列化和反序列化。 🏷 序列化的使用场景:

🏷 举例:

package main  
  
import (  
   "encoding/json"  
   "fmt")  
  
type Monster struct {  
   Name  string `json:"name"` //`json:"name"a就是struct tag  
   Age   int    `json:"age"`  
   Skill string `json:"skill"`  
}  
  
func main() {  
   //1.创建一个Monster变量  
   monster := Monster{"牛魔王", 580, "芭蕉扇~"}  
   //2.将monster变量序列化为json格式字串  
   //json.Marshal函数中使用反射,这个讲解反射时,我会详细介绍  
   jsonstr, err := json.Marshal(monster)  
   if err != nil {  
      fmt.Println("json处理错误", err)  
   }  
   fmt.Println("jsonstr", string(jsonstr))  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 10.2 方法

# 10.2.1 基本介绍

在某些情况下,我们要需要声明(定义)方法。比如 Person 结构体:除了有一些字段外( 年龄,姓名..),Person 结构体还有一些行为比如:可以说话、跑步..,通过学习,还可以做算术题。这时就要用方法 才能完成。

Golang 中的方法是 作用在指定的数据类型上的(即:和指定的数据类型绑定),因此 自定义类型, 都可以有方法,而不仅仅是 struct。

# 10.2.2 方法的声明和调用

typeAstruct { Num int } func (aA) test() { fmt.Println(a.Num) }

🏷对上面的语法的说明

  1. func (aA) test() {} 表示 A 结构体有一方法,方法名为 test
  2. (aA) 体现 test 方法是和 A 类型绑定的

🏷举例说明

package main  
  
import "fmt"  
  
type Person struct {  
   Name string  
}  
  
//给Person类型绑定一方法  
func (p Person) test() {  
   fmt.Println("test() name = ", p.Name)  
}  
  
func main() {  
   var p Person  
   p.Name = "tom"  
   p.test() //调用方法  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

🏷 对上面的总结

  1. test 方法和 Person 类型绑定
  2. test 方法只能通过 Person 类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
//下面的使用方式都是错误的
var dog Dog
dog.test() //xx
test() //xxx
1
2
3
4
  1. func (p Person) test() {}... p 表示哪个 Person 变量调用,这个 p 就是它的副本, 这点和函数传参非常相似。
  2. p 这个名字,有程序员指定,不是固定, 比如修改成 person 也是可以
//给Person类型绑定一方法
func (Person Person) test(){
	person.Name = "jack"
	fmt.Println("test() name=",person.Name) //输出jack
}
1
2
3
4
5

# 10.2.3 方法快速入门

  1. 给 Person 结构体添加 speak 方法,输出 xxx 是一个好人
//给Person 结构体添加speak 方法,输出xxx 是一个好人
func (p Person) speak(){
	fmt.Printl(p.Name,"是一个goodman~")
}
1
2
3
4
  1. 给 Person 结构体添加 jisuan 方法,可以计算从 1+..+1000 的结果, 说明方法体内可以函数一样,进行各种运算
//给Person结构体添加jisuan方法,可以计算从1+..+1o00的结果,
//说明方法体内可以函数一样,进行各种运算
func (p Person)jisuan(){
	res := 0
	for i:=1;i<=1000;i++{
		res += i
	}
	fmt.Println(p.Name,"计算的结果是",res)
}
1
2
3
4
5
6
7
8
9
  1. 给 Person 结构体 jisuan2 方法,该方法可以接收一个数 n,计算从 1+..+n 的结果
//给Person:结构体jisuan.2方法,该方法可以接收一个参数n,计算从1+.+n的结果
func (p Person)jisuan2(n int){
	res := 0
	for i := 1;i<=n;i++{
		res += i
	}
	fmt.Println(p.Name,"计算的结果是=",res)
}
1
2
3
4
5
6
7
8
  1. 给 Person 结构体添加 getSum 方法,可以计算两个数的和,并返回结果
//给Person结构体添加getsum方法,可以计算两个数的和,并返回结果
func (p Person) getsum(n1 int,n2 int)int{
	return n1 n2
}
1
2
3
4
  1. 方法的调用
//调用方法
p.speak()
p.jisuan()
p.jisuan2(20)
res := p.getsum(10,20)
fmt.Println("res=",res)
1
2
3
4
5
6

🏷 说明: 方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法。下面我们举例说明。

🏷 案例 1: 画出前面 getSum 方法的执行过程+说明

说明:

  1. 在通过一个变量去调用方法时,其调用机制和函数一样
  2. 不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地质拷贝)

🏷 案例 2 请编写一个程序,要求如下:

  1. 声明一个结构体 Circle, 字段为 radius
  2. 声明一个方法 area 和 Circle 绑定,可以返回面积。
  3. 提示:画出 area 执行过程+说明

# 10.2.5 方法的声明(定义)

func (recevier type) methodName(参数列表) (返回值列表){ 方法体 return 返回值 }

  1. 参数列表:表示方法输入
  2. recevier type : 表示这个方法和 type 这个类型进行绑定,或者说该方法作用于 type 类型
  3. receiver type : type 可以是结构体,也可以其它的自定义类型
  4. receiver : 就是 type 类型的一个变量(实例),比如 :Person 结构体 的一个变量(实例)
  5. 返回值列表:表示返回的值,可以多个
  6. 方法主体:表示为了 实现某一功能代码块
  7. return 语句不是必须的。

# 10.2.6 方法的注意事项和细节

  1. 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式

  2. 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理

  3. Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct, 比如 int , float32 等都可以有方法

package main  
  
import "fmt"  
  
type integer int  
  
func (i integer) print() {  
   fmt.Println("i=", i)  
}  
  
//编写一个方法,可以改变1的值  
func (i *integer) change() {  
   *i = *i + 1  
}  
  
func main() {  
   var i integer = 10  
   i.print()  
   i.change()  
   fmt.Println("i=", i)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  1. 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。[讲解]
  2. 如果一个类型实现了 String()这个方法,那么 fmt.Println 默认会调用这个变量的 String()进行输出
package main  
  
import "fmt"  
  
type student struct {  
   Name string  
   Age  int  
}  
  
//给*student实现方法string()  
func (stu *student) string() string {  
   str := fmt.Sprintf("Name=[%v]Age=[%v]", stu.Name, stu.Age)  
   return str  
}  
  
func main() {  
   //定义一个student变量  
   stu := student{  
      Name: "tom",  
      Age:  20,  
   }  
   //如果你实现了*student类型的 string方法,就会自动调用  
   fmt.Println(&stu)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 10.2.7 方法的课堂练习题

  1. 编写结构体(MethodUtils),编程一个方法,方法不需要参数,在方法中打印一个 10 * 8 的矩形,在 main 方法中调用该方法.

package main  
  
import "fmt"  
  
type Methodutils struct {  
   //字段..  
  
}  
  
//给Methoduti1s编写方法  
func (mu Methodutils) Print() {  
   for i := 1; i <= 10; i++ {  
      for j := 1; j <= 8; j++ {  
         fmt.Print("*")  
      }  
      fmt.Println()  
   }  
}  
  
func main() {  
   /*  
      1)编写结构体(Methoduti1s),编程一个方法,方法不需要参数,  
      在方法中打印一个10*8的矩形,在main方法中调用该方法。  
   */   var mu Methodutils  
   mu.Print()  
}
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
  1. 编写一个方法,提供 m 和 n 两个参数,方法中打印一个 m * n 的矩形

func (mu Methodutils) Print2(m int, n int) {  
   for i := 1; i <= m; i++ {  
      for j := 1; j <= n; j++ {  
         fmt.Print("*")  
      }  
      fmt.Println()  
   }  
}
1
2
3
4
5
6
7
8
9
  1. 编写一个方法算该矩形的面积(可以接收长 len,和宽 width), 将其作为方法返回值。在 main方法中调用该方法,接收返回的面积值并打印。

func (mu Methodutils) area(len float64, width float64) float64 {  
   return len * width  
}
1
2
3
4
  1. 编写方法:判断一个数是奇数还是偶数

func (mu Methodutils) judgeNum(num int) {  
   if num%2 == 0 {  
      fmt.Println(num, "是偶数...")  
   } else {  
      fmt.Println(num, "是奇数...")  
   }  
}
1
2
3
4
5
6
7
8
  1. 根据行、列、字符打印 对应行数和列数的字符,比如:行:3,列:2,字符*,则打印相应的效果

func (mu Methodutils) Print3(n int, m int, key string) {  
   for i := 1; i <= n; i++ {  
      for j := 1; j <= m; j++ {  
         fmt.Print(key)  
      }  
      fmt.Println()  
   }  
}
1
2
3
4
5
6
7
8
9
  1. 定义小小计算器结构体(Calcuator),实现加减乘除四个功能 实现形式 1:分四个方法完成: 实现形式 2:用一个方法搞定
//实现形式 1
type calcuator struct {  
   Numl float64  
   Num2 float64  
}  
  
func (calcuator *calcuator) getsum() float64 {  
   return calcuator.Numl + calcuator.Num2  
}  
  
func (calcuator *calcuator) getsub() float64 {  
   return calcuator.Numl - calcuator.Num2  
}

//实现形式 2
func (calcuator *calcuator) getRes(operator byte) float64 {  
   res := 0.0  
   switch operator {  
   case '+':  
      res = calcuator.Numl + calcuator.Num2  
   case '-':  
      res = calcuator.Numl - calcuator.Num2  
   case '*':  
      res = calcuator.Numl * calcuator.Num2  
   case '/':  
      res = calcuator.Numl / calcuator.Num2  
   default:  
      fmt.Println("运算符输入有误..")  
   }  
  
   return res  
}

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

# 10.2.9 方法和函数区别

  1. 调用方式不一样 函数的调用方式: 函数名(实参列表) 方法的调用方式: 变量.方法名(实参列表)

  2. 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然


package main  
  
import "fmt"  
  
type Person struct {  
   Name string  
}  
  
//函数  
//对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然  
func test0l(p Person) {  
   fmt.Println(p.Name)  
}  
func test02(p *Person) {  
   fmt.Println(p.Name)  
}  
  
func main() {  
   p := Person{"tom"}  
   test0l(p)  
   test02(&p)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  1. 对于方法(如 struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以

//对于方法(如struct的方法),  
//接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以  
func (p Person) teste3() {  
   p.Name = "jack"  
   fmt.Println("teste3()=", p.Name) //jack  
}  
  
func (p *Person) teste4() {  
   p.Name = "mary"  
   fmt.Println("test03()=", p.Name) //mary  
}

func main() {  
   p := Person{"tom"}  
   test0l(p)  
   test02(&p)  
  
   p.test03()  
   fmt.Println("main()p.name=", p.Name) // tom  
   (&p).test03()                        //从形式上是传入地址, 但是本质仍然是值拷贝  
   fmt.Println("main()p.name=", p.Name) //tom  
   (&p).test04()  
   fmt.Println("main()p.name=", p.Name) //mary  
   p.test04()                           //等价(&p).test4,从形式上是传入值类型,但是本质仍然是地址拷贝  
}
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. 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
  2. 如果是和值类型,比如 (p Person) , 则是值拷贝, 如果和指针类型,比如是 (p * Person) 则是地址拷贝。

# 10.3 面向对象编程应用实例

# 10.3.1 步骤

  1. 声明(定义)结构体,确定结构体名
  2. 编写结构体的字段
  3. 编写结构体的方法

# 10.3.2 学生案例:

  1. 编写一个 Student 结构体,包含 name、gender、age、id、score 字段,分别为 string、string、int、 int、float64 类型。
  2. 结构体中声明一个 say 方法,返回 string 类型,方法返回信息中包含所有字段值。
  3. 在 main 方法中,创建 Student 结构体实例(变量),并访问 say 方法,并将调用结果打印输出。
  4. 走代码
package main  
  
import (  
   "fmt"  
)  
  
/*  
学生案例:  
编写一个 Student 结构体,包含 name、gender、age、id、score 字段,分别为 string、string、int、int、  
float64 类型。  
结构体中声明一个 say 方法,返回 string 类型,方法返回信息中包含所有字段值。  
在 main 方法中,创建 Student 结构体实例(变量),并访问 say 方法,并将调用结果打印输出。  
*/  
type Student struct {  
   name   string  
   gender string  
   age    int  
   id     int  
   score  float64  
}  
  
func (student *Student) say() string {  
   infoStr := fmt.Sprintf("student 的信息 name=[%v] gender=[%v], age=[%v] id=[%v] score=[%v]",  
      student.name, student.gender, student.age, student.id, student.score)  
   return infoStr  
}  
func main() {  
   //测试  
   //创建一个 Student 实例变量  
   var stu = Student{  
      name:   "tom",  
      gender: "male",  
      age:    18,  
      id:     1000,  
      score:  99.98,  
   }  
   fmt.Println(stu.say())  
}
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

# 10.3.3 小狗案例 [学员课后练习]

  1. 编写一个 Dog 结构体,包含 name、age、weight 字段
  2. 结构体中声明一个 say 方法,返回 string 类型,方法返回信息中包含所有字段值。
  3. 在 main 方法中,创建 Dog 结构体实例(变量),并访问 say 方法,将调用结果打印输出。

# 10.3.4 盒子案例

  1. 编程创建一个 Box 结构体,在其中声明三个字段表示一个立方体的长、宽和高,长宽高要从终 端获取
  2. 声明一个方法获取立方体的体积。
  3. 创建一个 Box 结构体变量,打印给定尺寸的立方体的体积
  4. 走代码
package main  
  
import "fmt"  
  
/*  
1)编程创建一个B0x结构体,在其中声明三个字段表示一个立方体的长、宽和高,长宽高要从终端获取  
2)声明一个方法获取立方体的体积。  
3)创建一个B0x结构体变量,打印给定尺寸的立方体的体积  
*/  
type Box struct {  
   len    float64  
   width  float64  
   height float64  
}  
  
//声明一个方法辣取立方体的体积  
  
func (box *Box) getvolumn() float64 {  
   return box.len * box.width * box.height  
}  
  
func main() {  
   //测试代码  
   var box Box  
   box.len = 1.1  
   box.width = 2.0  
   box.height = 3.0  
   volumn := box.getvolumn()  
   fmt.Printf("体积为=%.2f", volumn)  
}
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

# 10.3.5 景区门票案例

  1. 一个景区根据游人的年龄收取不同价格的门票,比如年龄大于 18,收费 20 元,其它情况门票免 费.
  2. 请编写 Visitor 结构体,根据年龄段决定能够购买的门票价格并输出
  3. 代码:

package main  
  
import "fmt"  
  
type Visitor struct {  
   Name string  
   Age  int  
}  
  
func (visitor *Visitor) showprice() {  
   if visitor.Age > 90 || visitor.Age <= 8 {  
      fmt.Println("考虑到安全,就不要玩了")  
      return  
   }  
   if visitor.Age > 18 {  
      fmt.Printf("游客的名字为%v年龄为%v收费20元\n", visitor.Name, visitor.Age)  
   } else {  
  
      fmt.Printf("游客的名字为%v年龄为%v免费\n", visitor.Name, visitor.Age)  
   }  
}  
  
func main() {  
   var v Visitor  
   for {  
      fmt.Println("请输入你的名字")  
      fmt.Scanln(&v.Name)  
  
      if v.Name == "n" {  
         fmt.Println("退出程序..")  
         break  
      }  
      fmt.Println("请输入你的年龄")  
      fmt.Scanln(&v.Age)  
      v.showprice()  
   }  
  
}
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

# 10.4 创建结构体变量时指定字段值

🏷 说明 Golang 在创建结构体实例(变量)时,可以直接指定字段的值

🏷 方式 1


package main  
  
import "fmt"  
  
type Stu struct {  
   Name string  
   Age  int  
}  
  
func main() {  
   //方式一  
   //在创建结构体变量时,就直接会自动字段的值  
   var stu1 = Stu{"小明", 19} // stu1 -> 结构体数据空间  
   stu2 := Stu{"小明", 20}  
  
   //再创建结构体变量时,把字段名和字段值写在一起,这种写法,就不依赖字段的定义顺序  
   var stu3 = Stu{  
      Name: "jack",  
      Age:  20,  
   }  
  
   stu4 := Stu{  
      Age:  30,  
      Name: "mary",  
   }  
  
   fmt.Println(stu1, stu2, stu3, stu4)  
  
}
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

🏷 方式 2

//方式2,返回结构体的指针类型(!!!)  
var stu5 *Stu = &Stu{"小王", 29} //stu5->  地址-->   结构体数据[xxxx,xxxx]  
  
stu6 := &Stu{"小王~", 39}  
//在创建结构体指针变量时,把字段名和字段值写在一起,这种写法,就不依赖字段的定义顺序,  
var stu7 = &Stu{  
   Name: "小李",  
   Age:  49,  
}  
  
stu8 := &Stu{  
   Age:  59,  
   Name: "小李",  
}  
fmt.Println(*stu5, *stu6, *stu7, *stu8) //
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 10.5 工厂模式

# 10.5.1 说明

Golang 的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。

# 10.5.2 看一个需求

一个结构体的声明是这样的: package model

type Student struct { Name string... }

因为这里的 Student 的首字母 S 是大写的,如果我们想在其它包创建 Student 的实例(比如 main 包),引入 model 包后,就可以直接创建 Student 结构体的变量(实例)。 但是问题来了 , 如果首字母是小写的 ,比如 是 type student struct {....} 就不行了,怎么办---> 工厂模式来解决.

# 10.5.3 工厂模式来解决问题

🥼 使用工厂模式实现跨包创建结构体实例(变量)的案例: 如果 model 包的 结构体变量首字母大写,引入后,直接使用, 没有问题

如果 model 包的 结构体变量首字母小写,引入后,不能直接使用, 可以 工厂模式解决, 看老师演 示, 代码: student.go

package model  
  
/定义一个结构体  
type student struct {  
   Name  string  
   Score float64  
}  
  
//因为student:结构体首字母是小写,因此是只能在model使用  
//我们通过工厂模式来解决  
func Newstudent(n string, s float64) *student {  
   return &student{  
      Name:  n,  
      Score: s,  
   }  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

main.go

package main  
  
import (  
   "fmt"  
   model "go_test/day41/models"  
)  
  
func main() {  
   //创建要给student实例  
   //var stu model.student{  
   //Name :"tom",   //score:78.9,   //}   //定student结构体是首字母小写,我们可以通过工厂模式来解决  
  
   var stu = model.Newstudent("tom~", 88.8)  
   fmt.Println(*stu) //&{....}  
   fmt.Println("name=", stu.Name, "score=", stu.Score)  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 10.5.4 思考题

同学们思考一下,如果 model 包的 student 的结构体的字段 Score 改成 score,我们还能正常访问 吗?又应该如何解决这个问题呢?[老师给出思路,学员自己完成] 🏷 解决方法如下: