【Golang第8章:面向对象编程】Go语言的结构体是什么,怎么声明;Golang方法的调用和声明;go语言面向对象实例,go语言工厂模式;golang面向对象的三大特性:继承、封装、多态
介绍
这个是在B站上看边看视频边做的笔记,这一章是Glang面向对象编程
这一章内容较多,内容有Go语言的结构体是什么,怎么声明;Golang方法的调用和声明;go语言面向对象实例,go语言工厂模式;golang面向对象的三大特性:继承、封装、多态,面向对象编程应用实例,golang的接口介绍,类型断言
配套视频自己去B站里面搜【go语言】,最高的播放量就是
里面的注释我写的可能不太对,欢迎大佬们指出╰(°▽°)╯
文章目录
(八)、面向对象编程
1.看一个问题
2.使用现有技术解决
package main
func main() {
/*
张老太养了两只猫猫:一只名字叫小白,今年3岁,白色。还有一
只叫小花,今年100岁,花色。请编写一个程序,当用户输入小猫
的名字时,就显示该猫的名字,年龄,颜色。如果用户输入的
小猫名错误,则显示张老太没有这只猫猫。
*/
//1.使用变量处理
// var cat1Name string = "小白"
// var cat1Age int = 3
// var cat1Color string = "白色"
// var cat2Name string = "小花"
// var cat2Age int = 100
// var cat2Color string = "花色"
//2.使用数组处理
// var catNames [2]string = [...]string{"小白", "小花"}
// var catAge [2]int = [...]int{3, 100}
// var catColor [2]string = [...]string{"白色", "花色"}
//3.使用map解决
}
3.现有技术解决的缺点分析
- 使用变量或者数组来解决养猫的问题,不利于数据的管理和维护。因为名字,年龄,颜色都是属于一只猫,但是这里是分开保存。
- 如果我们希望对一只猫的属性(名字、年龄,颜色)进行操作(绑定方法), 也不好处理。
- 引出我们要讲解的技术–> 结构体。
4.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 中面向接口编程是非常重要的特性。
一、结构体
对上图的说明
- 将一类事物的特性提取出来(比如猫类), 形成一个新的数据类型, 就是一个结构体。
- 通过这个结构体,我们可以创建多个变量(实例/对象)
- 事物可以猫类,也可以是Person , Fish 或是某个工具类。。。
package main
import "fmt"
type monster struct {
Name string `json:"name"`
Age int `json:"age"`
Skill []string `json:"skill"`
}
func main() {
monster := monster{
Name: "哈哈",
Age: 100,
}
// monster.Skill = make([]string, 2, 6)
monster.Skill = append(monster.Skill[:], "武器1")
monster.Skill = append(monster.Skill[:], "武器2")
monster.Skill = append(monster.Skill[:], "武器3")
// monster.Skill[1] = "武器2"
fmt.Println(monster)
}
输出
{哈哈 100 [武器1 武器2 武器3]}
1.快速入门
-
面向对象的方式(struct结构体)解决养猫问题
package main import "fmt" //定义一个Cat结构体,将Cat的各个字段/属性信息,放入到Cat结构体进行管理 type Cat struct { Name string //字符串模式是空 Age int //int类型默认为0 Color string //字符串模式是空 Hobby string } func main() { /* 张老太养了两只猫猫:一只名字叫小白,今年3岁,白色。还有一 只叫小花,今年100岁,花色。请编写一个程序,当用户输入小猫 的名字时,就显示该猫的名字,年龄,颜色。如果用户输入的 小猫名错误,则显示张老太没有这只猫猫。 */ //使用struct结构体来完成案例 var cat1 Cat //创建一个cat的变量 fmt.Println(cat1) //给结构体赋值 cat1.Name = "小白" cat1.Age = 3 cat1.Color = "白色" fmt.Println("猫的信息如下:") fmt.Println("Nmae =", cat1.Name) fmt.Println("Age =", cat1.Age) fmt.Println("Color =", cat1.Color) }
通过上面的案例和讲解我们可以看出:
- 结构体和结构体变量(实例)的区别和联系
- 结构体是自定义的数据类型,代表一类事物.
- 结构体变量(实例)是具体的,实际的,代表一个具体变量
2.结构体变量(实例)在内存的布局(重要!)
3.如何声明结构体
-
基本语法
type 结构体名称 struct { field1 type field2 type }
-
举例:
type Student struct { Name string //字段 Age int //字段 Score float32 }
4.字段/属性
-
基本介绍
- 从概念或叫法上看: 结构体字段= 属性= field (即授课中,统一叫字段)
- 字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。比如我们前面定义猫结构体的Name string 就是属性
-
注意事项和细节说明
-
字段声明语法同变量,示例:字段名字段类型
-
字段的类型可以为:基本类型、数组或引用类型
-
在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:
布尔类型是false ,数值是0 ,字符串是""。
数组类型的默认值和它的元素类型相关,比如
score [3]int
则为[0, 0, 0]指针,slice,和map 的零值都是nil ,即还没有分配空间。
案例演示:
package main import "fmt" //指针,slice,和map 的零值都是nil ,即还没有分配空间。 //如果需要使用这样的字段,需要先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) //{ 0 [0 0 0 0 0] <nil> [] map[]} if p1.ptr == nil { fmt.Println("ptr为空") } if p1.slice == nil { fmt.Println("slice为空") } if p1.map1 == nil { fmt.Println("map1为空") } //使用slice,一定要make p1.slice = make([]int, 10) p1.slice[0] = 100 fmt.Println(p1.slice) //[100 0 0 0 0 0 0 0 0 0] //使用map,一定要make p1.map1 = make(map[string]string) p1.map1["key1"] = "1" fmt.Println(p1.map1) //map[key1:1] fmt.Println(p1) //{ 0 [0 0 0 0 0] <nil> [100 0 0 0 0 0 0 0 0 0] map[key1:1]} }
- 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体是值类型。
package main import "fmt" type Monster struct { Name string Age int } func main() { //不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改, //不影响另外一个,结构体是值类型 var monster1 Monster monster1.Name = "牛魔王" monster1.Age = 500 monster2 := monster1 //这里因为是值拷贝 结构体是值类型,默认为拷贝 monster2.Name = "红孩儿" //不会影响monster1 fmt.Println("monster1=", monster1) //monster1= {牛魔王 500} fmt.Println("monster2=", monster2) //monster2= {红孩儿 500} }
上图内存示意图:
-
5.创建结构体变量和访问结构体字段
-
方式1-直接声明
案例演示:
var person Person
//方式1 var p1 Person p1.Name = "tom" p1.Age = 23 fmt.Println(p1)
-
方式2-{}
案例演示:
var person Person = Person{}
//方式2 p2 := Person{"mary", 20} fmt.Println(p2)
-
方式3-&
案例演示:
var person *Person = new (Person)
//方式3 var p3 *Person = new(Person) //因为p3是一个指针,因此标准的给字段方式是赋值 (*p3).Name = "smith" //标准写法 (*p3).Age = 30 fmt.Println(*p3)
-
方式4-{}
案例演示:
var person *Person = &Person{}
//方式4 //也可以直接给值 //var person *Person = &Person{"jak", 30} var person *Person = &Person{} //因为person是一个指针,因此标准的访问字段的方法是 //(*person).Name = "scott" (*person).Name = "scott" //标准写法 person.Name = "scott" //简化写法 (*person).Age = 16 //标准写法 person.Age = 16 //简化写法 fmt.Println(*person)
-
说明:
- 第3 种和第4 种方式返回的是结构体指针。
- 结构体指针访问字段的标准方式应该是:
(*结构体指针).字段名
,比如(*person).Name = "tom"
- 但go 做了一个简化,也支持结构体指针.字段名, 比如
person.Name = "tom"
。更加符合程序员使用的习惯,go 编译器底层对person.Name
做了转化(*person).Name
。
6.struct 类型的内存分配机制
- 看一个思考题
输出的结果是: p2.Name = tom p1.Name = 小明
- 看下面代码,并分析原因
原因:
- 看下面代码,并分析原因
7.结构体使用注意事项和细节
-
结构体的所有字段在内存中是连续的
package main import "fmt" //结构体 type Point struct { //声明一个结构体 x int //声明类型 y int } type Rect struct { //声明一个结构体 leftUp, reghtDown Point //把Point分别传入给leftUp和reghtDown } type Rect2 struct { leftUp, reghtDown *Point //把Point的指针地分别传入给leftUp和reghtDown } func main() { //r1有4个int,在内存中是连续分布 //打印地址 r1 := Rect{Point{1, 2}, Point{3, 4}} //调用结构体Rect,并把Point{1, 2}传入给leftUp,Point{3, 4}出入给reghtDown fmt.Printf("r1.leftUp.x 地址=%p\nr1.leftUp.y 地址=%p\n", &r1.leftUp.x, &r1.leftUp.y) fmt.Printf("r1.reghtDown.x 地址=%p\nr1.reghtDown.y 地址=%p\n", &r1.reghtDown.x, &r1.reghtDown.y) fmt.Println() //r2有两个*Point类型, 这个两个*point类型的本身地址也是连续的, //但是他们指向的地址不一定是连续 r2 := Rect2{&Point{1, 2}, &Point{3, 4}} //本身地址 fmt.Printf("r2.leftUp 本身地址=%p\nr2.reghtDown 本身地址=%p\n", &r2.leftUp, &r2.reghtDown) //指向地址不一定连续, fmt.Printf("r2.leftUp 指向地址=%p\nr2.reghtDown 指向地址=%p\n", r2.leftUp, r2.reghtDown) }
输出:
r1.leftUp.x 地址=0xc00000c240 r1.leftUp.y 地址=0xc00000c248 r1.reghtDown.x 地址=0xc00000c250 r1.reghtDown.y 地址=0xc00000c258 r2.leftUp 本身地址=0xc00005a250 r2.reghtDown 本身地址=0xc00005a258 r2.leftUp 指向地址=0xc0000140c0 r2.reghtDown 指向地址=0xc0000140d0
对应的分析图:
-
结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
package main import "fmt" type A struct { Num int } type B struct { Num int } //结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型) func main() { var a A //a的类型是结构体A var b B //b的类型是结构体B fmt.Println(a, b) //{0} {0} a = A(b) //对b进行强制转换,转换为结构体A,之所以能进行强转,是因为结构体的类型字段是一致的(名字、格式、类型) fmt.Println(a) }
- 结构体进行type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
-
struct 的每个字段上,可以写上一个tag, 该tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
-
序列化的使用场景:
package main import ( "encoding/json" "fmt" ) type Monster struct { Name string `json:"name"` //`json:"name"`就是struct结构体的标签,使用json进行序列化时会输出标签 Age int `json:"age"` Skill string `json:"skill"` } func main() { //1.创建一个Monster变量 monster := Monster{"牛魔王", 500, "野蛮冲撞"} //2.将monster变量序列化为json格式字符串 //内置函数json.Marshal函数中使用反射 jsonMonster, err := json.Marshal(monster) if err != nil { fmt.Println("转换错误", err) } fmt.Println("jsonStr", jsonMonster) //jsonStr [123 34 110 ......... 158 34 125] //需要转换为string类型 fmt.Println("jsonStr", string(jsonMonster)) // jsonStr {"name":"牛魔王","age":500,"skill":"野蛮冲撞"} }
-
二、方法
1.基本介绍
在某些情况下,我们要需要声明(定义)方法。
比如Person 结构体:除了有一些字段外( 年龄,姓名…)
Person 结构体还有一些行为比如:可以说话、跑步…,通过学习,还可以做算术题。
这时就要用方法才能完成。
Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct。
2.方法的声明和调用
type A struct {
Num int
}
func (a A) test() {
fmt.Println(a.Num)
}
-
对上面的语法的说明
func (a A) test() {}
表示A 结构体有一方法,方法名为test(a A)
体现test 方法是和A 类型绑定的
-
举例:
package main import "fmt" type Person struct { Name string } //给Person类型绑定一个方法 func (p Person) test() { //把结构Person绑定给p,并调用test()函数,此时test()为方法 fmt.Println("test() name =", p.Name) //由于绑定过Person结构体 } func main() { var p Person //声明p的类型 p.test() //调用方法 test() name= p.Name = "tom" //tom赋值给p.Name p.test() //调用方法 test() name = tom }
-
对上面的总结
-
test 方法和Person 类型绑定
-
test 方法只能通过Person 类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
-
func (p Person) test() {}
… p 表示哪个Person 变量调用,这个p 就是它的副本, 这点和函数传参非常相似。 -
p 这个名字,有程序员指定,不是固定, 比如修改成person 也是可以
-
3.方法快速入门
-
给Person 结构体添加speak 方法,输出xxx 是一个好人
package main import "fmt" type Person struct { Name string } func (p Person) speak() { fmt.Println(p.Name, "是一个好人") } func main() { var p Person //必须先声明p p.Name = "tom" p.speak() }
-
给Person 结构体添加jisuan 方法,可以计算从1+…+1000 的结果, 说明方法体内可以函数一样,进行各种运算
package main import "fmt" type Person struct { Name string } //给Person 结构体添加jisuan 方法,可以计算从1+..+1000 的结果, 说明方法体内可以函数一样,进行各种运算 func (p Person) jisuan() { res := 0 for i := 0; i <= 1000; i++ { res += i } fmt.Println(p.Name, "计算的结果是=", res) } func main() { var p Person //必须先声明p p.Name = "tom" p.speak() p.jisuan() //tom 计算的结果是= 500500 }
-
给Person 结构体jisuan2 方法,该方法可以接收一个数n,计算从1+…+n 的结果
type Person struct { Name string } //给Person 结构体jisuan2 方法,该方法可以接收一个数n,计算从1+..+n 的结果 func (p Person) jisuan2(n int) { res := 0 for i := 0; i <= n; i++ { res += i } fmt.Println(p.Name, "计算的结果是=", res) } func main() { var p Person //必须先声明p p.Name = "tom" p.jisuan2(10) //tom 计算的结果是= 55 }
-
给Person 结构体添加getSum 方法,可以计算两个数的和,并返回结果
type Person struct { Name string } //给Person 结构体添加getSum 方法,可以计算两个数的和,并返回结果 func (p Person) getSum(n1 int, n2 int) int { return n1 + n2 } func main() { var p Person //必须先声明p p.Name = "tom" res := p.getSum(10, 20) fmt.Println(res) //30 }
4.方法的调用和传参机制原理:(重要!)
-
说明:
方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法。下面我们举例说明。
-
案例1:
画出前面getSum 方法的执行过程+说明
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LhFSHfUb-1669362409965)(assets/image-20221017140607810.png)]
说明:
- 在通过一个变量去调用方法时,其调用机制和函数一样
- 不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地质拷贝)
-
案例2
请编写一个程序,要求如下:
- 声明一个结构体Circle, 字段为radius
- 声明一个方法area 和Circle 绑定,可以返回面积。
- 提示:画出area 执行过程+说明
package main import "fmt" //1) 声明一个结构体Circle, 字段为radius //2) 声明一个方法area 和Circle 绑定,可以返回面积。 //3) 提示:画出area 执行过程+说明 type Circle struct { radius float64 } func (c Circle) area() float64 { return 3.14 * c.radius * c.radius //圆面积=圆周率×半径×半径 } func main() { //创建一个Circle变量 var c Circle c.radius = 4.0 res := c.area() fmt.Println(res) //50.24 }
5.方法的声明(定义)
func (recevier type) methodName(参数列表) (返回值列表){
方法体
return 返回值
}
- 参数列表:表示方法输入
recevier type
: 表示这个方法和type 这个类型进行绑定,或者说该方法作用于type 类型receiver type
: type 可以是结构体,也可以其它的自定义类型receiver
: 就是type 类型的一个变量(实例),比如:Person 结构体的一个变量(实例)- 返回值列表:表示返回的值,可以多个
- 方法主体:表示为了实现某一功能代码块
return
语句不是必须的。
6.方法的注意事项和细节
-
结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
-
如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
package main import "fmt" type Circle struct { radius float64 } //为了提高效率,通常我们方法和结构体的指针类型绑定 func (c *Circle) area2() float64 { //因为c是指针,因此我们标准的访问其字段的方式是(*c).redius //标准写法 return 3.14 * (*c).radius * (*c).radius //圆面积=圆周率×半径×半径 return 3.14 * c.radius * c.radius } func main() { //创建一个Circle变量 var c Circle c.radius = 5.0 res2 := (&c).area2() //标准写法 fmt.Println(res2) //编译器底层做了优化(&c).area2() 等价c.area() //因为编译器会自动的给加上&c res1 := c.area2() fmt.Println(res1) }
-
Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct, 比如int , float32 等都可以有方法
package main import "fmt" /* Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型, 都可以有方法,而不仅仅是struct, 比如int , float32 等都可以有方法 */ type integer int func (i integer) print() { fmt.Println("i =", i) } //编写一个方法, 可以改变i的值 func (i *integer) change() { *i = *i + 1 } func main() { var i integer = 10 i.print() //i = 10 i.change() fmt.Println("i =", i) //i = 11 }
-
方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。
-
如果一个类型实现了
String()
这个方法,那么fmt.Println
默认会调用这个变量的String()
进行输出package main import "fmt" type Student struct { Name string Age int } func (stu *Student) String() string { //赋值Student的指针给stu str := fmt.Sprintf("Name=[%v] Age=[%v]", (*stu).Name, (*stu).Age) return str } func main() { //定义一个Student变量 stu := Student{ Name: "tom", Age: 20, } fmt.Println(stu) //不使用指针输出,使用默认方式输出 {tom 20} fmt.Println(&stu) //使用地址进行输出 Name=[tom] Age=[20] }
7.方法的练习题
- 编写结构体(MethodUtils),编程一个方法,方法不需要参数,在方法中打印一个10*8 的矩形,在main 方法中调用该方法。
package main
import "fmt"
//编写结构体(MethodUtils),编程一个方法,方法不需要参数,在方法中打印一个10*8 的矩形,
//在main 方法中调用该方法。
type MethodUtils struct {
}
//给MethodUtils编写方法
func (mu MethodUtils) Print() {
for j := 0; j < 10; j++ {
for i := 0; i < 8; i++ {
fmt.Printf("*")
}
fmt.Println()
}
}
func main() {
var mu MethodUtils
mu.Print()
}
- 编写一个方法,提供m 和n 两个参数,方法中打印一个m*n 的矩形
package main
import "fmt"
//编写结构体(MethodUtils),编程一个方法,方法不需要参数,在方法中打印一个10*8 的矩形,
//在main 方法中调用该方法。
type MethodUtils struct {
}
//给MethodUtils编写方法
func (mu MethodUtils) Print(n1 int, n2 int) {
for j := 0; j < n1; j++ {
for i := 0; i < n2; i++ {
fmt.Printf("*")
}
fmt.Println()
}
}
func main() {
var mu MethodUtils
mu.Print(8, 10)
}
-
编写一个方法算该矩形的面积(可以接收长len,和宽width), 将其作为方法返回值。在main方法中调用该方法,接收返回的面积值并打印。
-
编写方法:判断一个数是奇数还是偶数
-
根据行、列、字符打印对应行数和列数的字符,比如:行:3,列:2,字符*,则打印相应的效果
-
定义小小计算器结构体(Calcuator),实现加减乘除四个功能
实现形式1:分四个方法完成
实现形式2:用一个方法搞定
练习题
8.方法和函数区别
-
调用方式不一样
函数的调用方式: 函数名(实参列表)
方法的调用方式: 变量.方法名(实参列表)
-
对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
package main import "fmt" type Person struct { Name string } func test01(p Person) { fmt.Println(p.Name) } func test02(p *Person) { fmt.Println(p.Name) } func main() { p := Person{"tom"} test01(p) test02(&p) }
-
对于方法(如struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
- 总结:
- 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
- 如果是和值类型,比如
(p Person)
, 则是值拷贝, 如果和指针类型,比如是(p *Person)
则是地址拷贝。
三、面向对象编程应用实例
1.步骤
- 声明(定义)结构体,确定结构体名
- 编写结构体的字段
- 编写结构体的方法
2.案例1
-
编写一个Student 结构体,包含name、gender、age、id、score 字段,分别为string、string、int、int、float64 类型。
-
结构体中声明一个say 方法,返回string 类型,方法返回信息中包含所有字段值。
-
在main 方法中,创建Student 结构体实例(变量),并访问say 方法,并将调用结果打印输出。
package main import "fmt" /* 1) 编写一个Student 结构体,包含name、gender、age、id、score 字段,分别为string、string、int、int、float64 类型。 2) 结构体中声明一个say 方法,返回string 类型,方法返回信息中包含所有字段值。 3) 在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()) }
3.案例2
- 编写一个Dog 结构体,包含name、age、weight 字段
- 结构体中声明一个say 方法,返回string 类型,方法返回信息中包含所有字段值。
- 在main 方法中,创建Dog 结构体实例(变量),并访问say 方法,将调用结果打印输出。
4.案例3
-
编程创建一个Box 结构体,在其中声明三个字段表示一个立方体的长、宽和高,长宽高要从终端获取
-
声明一个方法获取立方体的体积。
-
创建一个Box 结构体变量,打印给定尺寸的立方体的体积
5.案例4
-
一个景区根据游人的年龄收取不同价格的门票,比如年龄大于等于18,收费20 元,其它情况门票免费.
-
请编写Visitor 结构体,根据年龄段决定能够购买的门票价格并输出
四、创建结构体变量时指定字段值
-
说明
Golang 在创建结构体实例(变量)时,可以直接指定字段的值
-
方式1
package main import "fmt" type Stu struct { Name string Age int } func main() { //方式1 //在创建结构体变量时,就直接指定字段的值 var stu1 = Stu{"小明", 19} stu2 := Stu{"小米", 20} //在创建结构体变量时,把字段名和字段值写在一起 var stu3 = Stu{ Name: "jack", Age: 18, } var stu4 = Stu{ Name: "mary", Age: 21, } fmt.Println(stu1, stu2, stu3, stu4) //{小明 19} {小米 20} {jack 18} {mary 21} }
-
方式2
package main import "fmt" type Stu struct { Name string Age int } func main() { //方式2 返回结构体的指针类型(!! ! var stu5 = &Stu{"小王", 29} stu6 := &Stu{"小花", 28} //在创建结构体指针变量时,把字段名和字段值写在一起,这种写法,就不依赖字段的定义顺序. var stu7 = &Stu{ Name: "小李", Age: 39, } stu8 := &Stu{ Name: "小李", Age: 39, } fmt.Println(stu5, stu6, stu7, stu8) //&{小王 29} &{小花 28} &{小李 39} &{小李 39} //&表示是地址符,使用*对原值进行取值 fmt.Println(*stu5, *stu6, *stu7, *stu8) //{小王 29} {小花 28} {小李 39} {小李 39} }
五、工厂模式
1.说明
Golang 的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。
-
看一个需求
一个结构体的声明是这样的:
package model type Student struct { Name string... }
因为这里的Student 的首字母S 是大写的,如果我们想在其它包创建Student 的实例(比如main 包),引入model 包后,就可以直接创建Student 结构体的变量(实例)。
但是问题来了,如果首字母是小写的,比如是type student struct {....}
就不不行了,怎么办 —> 工厂模式来解决.
2.工厂模式案例
-
使用工厂模式实现跨包创建结构体实例(变量)的案例:
如果model 包的结构体变量首字母大写,引入后,直接使用, 没有问题
student.go
package model type Student struct { Name string Score float64 }
main.go
package main import ( "demo/28demo/02/model" "fmt" ) func main() { //创建要给Student实例 var stu = model.Student{ Name: "小明", Score: 86.25, } fmt.Println(stu) }
-
如果model 包的结构体变量首字母小写,引入后,不能直接使用, 可以工厂模式解决
student.go
package model type student struct { Name string Score float64 } //因为student结构体首字母是小写,因此是只能在mode1使用 //我们通过工厂模式来解决 func NewStudent(n string, s float64) *student { //定义一个公开的方法,使用一个方法来接收。并把值返回 return &student{ Name: n, Score: s, } }
main.go
package main import ( "demo/28demo/02/model" "fmt" ) func main() { //因为student结构体是首字母小写,我们可以通过工厂模式来解决 var stu = model.NewStudent("tom", 88.8) fmt.Println(*stu) fmt.Printf("name=%v score=%v", stu.Name, stu.Score) }
3.思考题
思考一下,如果model 包的student 的结构体的字段Score 改成score,我们还能正常访问吗?又应该如何解决这个问题呢?
package model
type student struct {
Name string
score float64
}
//如果score字段首字母小写,则,在其它包不可以直接方法,我们可以提供一个方法
func (s *student) GetScore() float64 {
return s.score
}
package main
import (
"demo/28demo/02/model"
"fmt"
)
func main() {
//因为student结构体是首字母小写,我们可以通过工厂模式来解决
var stu = model.NewStudent("tom", 88.8)
fmt.Println(*stu)
fmt.Printf("name=%v score=%v", stu.Name, stu.GetScore())
}
六、面向对象编程思想-抽象
1.抽象的介绍
我们在前面去定义一个结构体时候,实际上就是把一类事物的共有的属性(字段)和行为(方法)提取出来,形成一个物理模型(结构体)。这种研究问题的方法称为抽象。
package main
import "fmt"
//定义一个结构体
type Account struct {
AccountNo string
Pwd string
Balance float64
}
//方法
//1.存款
func (account *Account) Deposite(useraccount string, pwd string, money float64) {
// if useraccount != account.AccountNo {
// fmt.Println("你输入的账号不正确")
// return
// }
// //看下输入的密码是否正确
// if pwd != account.Pwd {
// fmt.Println("你输入的密码不正确")
// return
// }
if money <= 0 {
fmt.Println("你输入的金额不正确")
return
}
account.Balance += money
fmt.Println("存款成功")
fmt.Printf("你的账号为=%v,你的余额为=%v\n", account.AccountNo, account.Balance)
}
//2.取款
func (account *Account) WithDraw(useraccount string, pwd string, money float64) {
// if useraccount != account.AccountNo {
// fmt.Println("你输入的账号不正确")
// return
// }
// if pwd != account.Pwd {
// fmt.Println("你输入的密码不正确")
// return
// }
if money <= 0 || money > account.Balance { //取款小于等于0,你要取款的钱大于你存的钱
fmt.Println("你输入的金额不正确")
return
}
account.Balance -= money
fmt.Println("取款成功")
fmt.Printf("你的账号为=%v,你的余额为=%v\n", account.AccountNo, account.Balance)
}
//3.查询余额
func (account *Account) Query(useraccount string, pwd string) {
// if useraccount != account.AccountNo {
// fmt.Println("你输入的账号不正确")
// return
// }
// if pwd != account.Pwd {
// fmt.Println("你输入的密码不正确")
// return
// }
fmt.Printf("你的账号为=%v,你的余额为=%v\n", account.AccountNo, account.Balance)
}
func main() {
account := Account{
AccountNo: "icbc",
Pwd: "123456",
Balance: 100.0,
}
var user string
var password string
var input string
fmt.Println("请输入账号:")
fmt.Scanln(&user) //用户输入
fmt.Println("请输入密码:")
fmt.Scanln(&password)
if user != account.AccountNo || password != account.Pwd {
fmt.Println("您输入的账号或密码有误")
return
} else {
fmt.Printf("请输入您要办理的业务:\n1.存款\n2.取款\n3.查看余额\n")
fmt.Scanln(&input)
switch input {
case "1", "存款", "1.存款":
account.Deposite(user, password, 123)
case "2", "取款", "2.取款":
account.WithDraw(user, password, 12.66)
case "3", "查看余额", "3.查看余额":
account.Query(user, password)
default:
fmt.Println("输入有误,请重新输入")
}
fmt.Println()
}
}
七、面向对象编程三大特性-封装
1.基本介绍
Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP 语言不一样,下面我们进行详细的讲解Golang 的三大特性是如何实现的。
2.封装介绍
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作
3.封装的理解和好处
- 隐藏实现细节
- 提可以对数据进行验证,保证安全合理(Age)
4.如何体现封装
- 对结构体中的属性进行封装
- 通过方法,包实现封装
5.封装的实现步骤
-
将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似private)
-
给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
-
提供一个首字母大写的Set 方法(类似其它语言的public),用于对属性判断并赋值
func (var 结构体类型名) SetXxx(参数列表) (返回值列表) { //加入数据验证的业务逻辑 var.字段= 参数 }
-
提供一个首字母大写的Get 方法(类似其它语言的public),用于获取属性的值
func (var 结构体类型名) GetXxx() { return var.age; }
- 特别说明:在Golang 开发中并没有特别强调封装,这点并不像Java. 所以提醒学过java 的朋友,不用总是用java 的语法特性来看待Golang, Golang 本身对面向对象的特性做了简化的.
6.快速入门案例
-
案例
请大家看一个程序
(person.go)
,不能随便查看人的年龄,工资等隐私,并对输入的年龄进行合理的验证。设计:model
包(person.go)
main 包(main.go 调用Person 结构体)
person.go
package model import "fmt" type person struct { Name string age int //其他包不能直接访问 年龄 sal float64 //其他包不能直接访问 薪水 } //写一个工厂模式的函数,相当于构造函数 func NwePerson(name string) *person { //让小写的结构体可以被其他包引用 return &person{ Name: name, } } //为了访问age和sal 我们编写一 对SetXxx的方法和Getxx的方法 func (p *person) SetAge(age int) { //使用指针赋值给结构体中的年龄 存入值 if age > 0 && age < 150 { p.age = age } else { fmt.Println("年龄范围不正确") } } func (p *person) GetAge() int { //Get查看这个值 ,直接返回 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 { //Get查看sal这个值并直接返回给GetSal() return p.sal }
main.go
package main import ( "demo/29demo/model" "fmt" ) func main() { p := model.NwePerson("小明") //声明model并把小明赋值给结构体person的name fmt.Println(*p) //使用*取原值进行输出 p.SetAge(18) //set存入age值 p.SetSal(5000) fmt.Println(*p) fmt.Printf("name=%v age=%v sal=%v", p.Name, p.GetAge(), p.GetSal()) //调用方法并输出 }
7.练习
-
要求
-
创建程序,在
model
包中定义Account
结构体:在main
函数中体会Golang 的封装性。 -
Account
结构体要求具有字段:账号(长度在6-10 之间)、余额(必须>20)、密码(必须是六 -
通过SetXxx 的方法给Account 的字段赋值。
-
在main 函数中测试
-
-
代码
account.go
package model import "fmt" type account struct { user string //长度在6-10之间 pwd string //必须是6位 balance float64 //必须>20 } //小写的结构体可以被其他包引用 func NewAccount(user string, pwd string, balance float64) *account { fmt.Println(len(user)) if len(user) < 6 || len(user) > 10 { fmt.Println("输入的范围有误,长度必须在6-10之间") return nil } if len(pwd) != 6 { fmt.Println("输入的范围有误,长度必须是6位") return nil } if balance < 20 { fmt.Println("输入的范围有误,余额(必须>20)") return nil } return &account{ user: user, pwd: pwd, balance: balance, } } // func (a *account) SetUser(user string) { length := len(user) if 6 <= length && length <= 10 { a.user = user } else { fmt.Println("输入的范围有误,长度必须在6-10之间") } } func (a *account) SetPwd(pwd string) { length := len(pwd) if length == 6 { a.pwd = pwd } else { fmt.Println("输入的范围有误,长度必须是6位") } } func (a *account) SetBalance(balance float64) { if balance >= 20 { a.balance = balance } else { fmt.Println("输入的范围有误,余额(必须>20)") } } func (a *account) GetUser() string { return a.user } func (a *account) GetPwd() string { return a.pwd } func (a *account) GetBalance() float64 { return a.balance }
main.go
package main import ( "demo/29demo/01/model" "fmt" ) /* 1) 创建程序,在`model` 包中定义`Account`结构体:在`main` 函数中体会Golang 的封装性。 2) `Account` 结构体要求具有字段:账号(长度在6-10 之间)、余额(必须>20)、密码(必须是六 3) 通过SetXxx 的方法给Account 的字段赋值。 4) 在main 函数中测试 */ func main() { account := model.NewAccount("小红帽", "123456", 123) if account != nil { fmt.Println("创建成功", *account) } else { fmt.Println("创建失败") } account.SetUser("小明") fmt.Println(*account) fmt.Println(account.GetUser()) }
八、面向对象编程三大特性-继承
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) SteScore(score int) {
p.Score = score
}
func (p *Pupil) Tesing() {
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) SteScore(score int) {
p.Score = score
}
func (p *Graduate) Tesing() {
fmt.Println("大学生正在考试中")
}
func main() {
//测试
var pupil = &Pupil{
Name: "tom",
Age: 10,
}
pupil.Tesing()
pupil.SteScore(89)
pupil.ShowInfo()
var graduate = &Graduate{
Name: "mary",
Age: 20,
}
graduate.Tesing()
graduate.SteScore(76)
graduate.ShowInfo()
}
对上面代码的小结
- Pupil 和Graduate 两个结构体的字段和方法几乎,但是我们却写了相同的代码, 代码复用性不强
- 出现代码冗余,而且代码不利于维护,同时也不利于功能的扩展。
- 解决方法-通过继承方式来解决
2.继承基本介绍和示意图
继承可以解决代码复用,让我们的编程更加靠近人类思维。
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体(比如刚才的Student),在该结构体中定义这些相同的属性和方法。
其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个Student 匿名结构体即可。[示意图]
在Golang 中,如果一个struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。
3.嵌套匿名结构体的基本语法
type Goods struct {
Name string
Price int
}
type Book struct {
Goods //这里就是嵌套匿名结构体Goods
Writer string
}
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) SteScore(score int) { stu.Score = score } /// /// type Pupil struct { Student //嵌入Student匿名结构体 } type Graduate struct { Student //嵌入Student匿名结构体 } //保留特有的方法 func (p *Pupil) Tesing() { fmt.Println("小学生正在考试中") } func (p *Graduate) Tesing() { fmt.Println("大学生正在考试中") } /// func main() { //当我们对结构体嵌入了匿名结构体后使用方法会发生变化 //方式1 pupil := &Pupil{} pupil.Student.Name = "tom" pupil.Student.Age = 10 pupil.Tesing() pupil.Student.SteScore(75) pupil.Student.ShowInfo() graduate := &Graduate{} graduate.Student.Name = "mary" graduate.Student.Age = 20 graduate.Tesing() graduate.Student.SteScore(68) graduate.Student.ShowInfo() fmt.Println() //方式2...... pupil.Name = "tom~" pupil.Age = 100 pupil.Tesing() pupil.SteScore(750) pupil.ShowInfo() graduate.Name = "mary~" graduate.Age = 200 graduate.Tesing() graduate.SteScore(680) graduate.ShowInfo() }
5.继承的深入讨论
-
结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。【举例说明】
-
匿名结构体字段访问可以简化
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.age)
}
type B struct {
A
}
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) 当我们直接通过b 访问字段或方法时,其执行流程如下比如
b.Name
(2) 编译器会先看b 对应的类型有没有Name, 如果有,则直接调用B 类型的Name 字段
(3) 如果没有就去看B 中嵌入的匿名结构体A 有没有声明Name 字段,如果有就调用,如果没有继续查找…如果都找不到就报错
- 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分【举例说明】
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.age)
}
///
type B struct {
A
Name string
}
func (b *B) SayOk() {
fmt.Println("B SayOk", b.Name)
}
///
func main() {
var b B
b.Name = "jack" //使用就近原则,导入jack
b.A.Name = "scott" //使用匿名结构体全路径
b.age = 100
b.SayOk() //使用就近原则
b.A.SayOk() //使用匿名结构体全路径
b.hello()
}
- 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。【举例说明】
package main
import "fmt"
type A struct {
Name string
Age int
}
type B struct {
Name string
Score float64
}
//C同时嵌入了2个不同的结构体,此时就必须明确指定匿名结构体名字
type C struct {
A
B
}
func main() {
var c C
//如果c没有Name字段,而A和B有Name, 这时就必须通过指定置名结构体名字来区分
//所以 c.Name 就会包编译错误, 这个规则对方法也是一 样的!
c.A.Name = "tom"
fmt.Println(c.A.Name)
}
-
如果一个struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
package main import "fmt" type A struct { Name string Age int } type D struct { a A //有名结构体 组合关系 } func main() { 如果D中是一个有名结构体,则访问有名结构体的字段时,就必须带上有名结构体的名字 var d D //d.Name = "jack" //直接去d里面找Name,发现没有叫做Name的就直接报错, d.a.Name = "jack" //因为A是有名结构体,需要完整的路径 }
-
嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
package main import "fmt" type Goods struct { Name string Price float64 } type Brend struct { Name string Addrees string } type Tv1 struct { Goods Brend } type Tv2 struct { //使用指针类型 *Goods *Brend } func main() { //嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值 tv1 := Tv1{Goods{"电视机001", 5999.09}, Brend{"海尔", "山东"}} //方式1 fmt.Println(tv1) tv2 := Tv1{ //方式2 Goods{ Name: "电视机002", Price: 5000.09, }, Brend{ Name: "海尔", Addrees: "山东", }, } fmt.Println(tv2) //使用指针 tv3 := Tv2{&Goods{"电视机003", 6999.09}, &Brend{"创维", "河南"}} fmt.Println(*tv3.Goods, *tv3.Brend) tv4 := Tv2{ &Goods{ Name: "电视机004", Price: 4000.99, }, &Brend{ Name: "创维", Addrees: "河南", }, } fmt.Println(*tv4.Goods, *tv4.Brend) }
6.练习
结构体的匿名字段是基本数据类型,如何访问, 下面代码输出什么
package main
import "fmt"
type Monster struct {
Name string
Age int
}
type E struct {
Monster
int //匿名字段时基本数据类型
n int //如果有了1个int,后面int必须有名称
}
func main() {
var e E
e.Name = "牛魔王"
e.Age = 500
e.int = 20
e.n = 10
fmt.Println(e) //{{牛魔王 500} 20 10}
}
说明
- 如果一个结构体有 int 类型的匿名字段,就不能第二个。
- 如果需要有多个 int 的字段,则必须给 int 字段指定名
7.面向对象编程-多重继承
-
如一个 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方 法,从而实现了多重继承。
-
案例
通过一个案例来说明多重继承使用
-
多重继承细节说明
-
如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。【案例演示】
-
为了保证代码的简洁性,建议大家尽量不使用多重继承
-
九、接口(interface)
1.基本介绍
按顺序,我们应该讲解多态,但是在讲解多态前,我们需要讲解接口(interface),因为在Golang 中多态特性主要是通过接口来体现的。
2.接口快速入门
这样的设计需求在Golang 编程中也是会大量存在的,前面说过,一个程序就是一个世界,在现实世界存在的情况,在程序中也会出现。我们用程序来模拟一下前面的应用场景。
-
快速入门案例
package main import "fmt" //声明一个接口 type Usb interface { //声明了2个没有实现的方法 Start() Stop() } //让手机Phone实现Usb接口的方法 type Phone struct { } func (p Phone) Start() { fmt.Println("启动手机...") } func (p Phone) Stop() { fmt.Println("关闭手机...") } //让相机Camera实现Usb接口的方法 type Camera struct { } 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) { //2.接收到phone //通过usb接口变量来调用start和stop方法 usb.Start() //3.寻找关于phone的Start方法 输出:启动手机... usb.Stop() //4.寻找关于phone的Stop方法 输出:关闭手机... // //上面等同于下面这个 // phone := Phone{} // phone.Start() } func main() { //测试,先创建结构体变量 computer := Computer{} phone := Phone{} camera := Camera{} computer.Working(phone) //1.传入phone参数 fmt.Println() computer.Working(camera) }
3.基本语法
interface 类型可以定义一组方法,但是这些不需要实现。并且interface 不能包含任何变量。到某个自定义类型(比如结构体Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)。
- 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。
- Golang 中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有
implement
这样的关键字
4.接口使用的应用场景
5.注意事项和细节
-
接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)
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 //结构体变量 var a AInterface = stu stu.Say() a.Say() }
-
接口中所有的方法都没有方法体,即都是没有实现的方法。
-
在Golang 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口。
-
一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
-
只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。
-
一个自定义类型可以实现多个接口
package main import "fmt" type AInterface interface { //定义一个接口 Say() } type BInterface interface { //定义一个接口 Hello() } type Monster struct { //定义struct结构体 } func (m Monster) Hello() { //定义Hello并调用Monster结构体 fmt.Println("Monster Hello()") } func (m Monster) Say() { fmt.Println("Monster Say()") } func main() { //调用接口 //Monster实现 了AInterface 和BInterface var monster Monster var a AInterface = monster var b BInterface = monster a.Say() b.Hello() }
-
Golang 接口中不能有任何变量
-
一个接口(比如A 接口)可以继承多个别的接口(比如B,C 接口),这时如果要实现A 接口,也必须将B,C 接口的方法也全部实现。
package main type BInterface interface { test01() } type CInterface interface { test02() } type AInterface interface { //如果要实现A接口,必须把A接口所有的反法全部实现 BInterface CInterface test03() } type Stu struct { } //如果需要实现AInterface,就需要将BInterface CInterface的方法都实现 func (stu Stu) test01() {} //缺一不可 func (stu Stu) test02() {} //缺一不可 func (stu Stu) test03() {} //缺一不可 //如果需要实现AInterface,就需要将BInterface CInterface的方法都实现 func main() { var stu Stu var a AInterface = stu var b BInterface = stu a.test01() b.test01() }
-
interface 类型默认是一个指针(引用类型),如果没有对interface 初始化就使用,那么会输出nil
-
空接口
interface{}
没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量赋给空接口。package main import "fmt" type T interface{} //定义一个空接口 type Stu struct { //定义一个结构体 } func main() { var nume float64 = 8.0 //方式1 var stu Stu //调用结构体 var t1 T = stu //声明位空接口类型并调用结构体 t1 = nume fmt.Println(t1) //方式2 var t2 interface{} = stu //定义一个空接口类型,并传给t2 t2 = nume fmt.Println(t2) }
6.接口编程的最佳实践
-
实现对Hero 结构体切片的排序:
sort.Sort(data Interface)
package main import ( "fmt" "math/rand" "sort" ) // 实现对Hero 结构体切片的排序: sort.Sort(data Interface) //1.声明Hero结构体 type Hero struct { Name string Age int } //2.声明一个Hero结构体切片类型 type HeroSlice []Hero //声明Hero切片 //3.interface接口 func (hs HeroSlice) Len() int { return len(hs) //遍历数组的数量,并返回 } //Less方法就是决定你使用什么标准进行排序 //3.1.按Hero年龄进行从小到大进行排序 func (hs HeroSlice) Less(i, j int) bool { return hs[i].Age < hs[j].Age //判断大小,返回bool类型 //修改对Name的排序 //return hs[i].Name < hs[j].Name } //3.2把值进行交换 func (hs HeroSlice) Swap(i, j int) { hs[i], hs[j] = hs[j], hs[i] } func main() { //先定义一个数组/切片 var intSlice = []int{0, -1, 10, 7, 90} //要求对interface进行排序 fmt.Println(intSlice) sort.Ints(intSlice) fmt.Println(intSlice) //======================================== //对结构体切片进行排序 var heros HeroSlice //生成英雄hero for i := 0; i < 10; i++ { hero := Hero{ //调用Hero结构体,传入英雄和英雄的年龄 Name: fmt.Sprintf("英雄%d", rand.Intn(100)), //使用rand.Intn生成0到100的伪随机数 Age: rand.Intn(100), //使用rand.Intn生成0到100的伪随机数 } heros = append(heros, hero) //使用append对切片进行动态追加,并赋值给heros } //看看排序前的顺序 fmt.Println(heros) for _, v := range heros { //使用range对heros进行遍历 fmt.Println(v) } fmt.Println("-----排序后-----") //调用sort.Sort sort.Sort(heros) //看看排序后的顺序 for _, v := range heros { fmt.Println(v) } }
-
接口编程练习
//1.声明Student 结构体 type Student struct{ Name string Age int Score float64 } //将Student 的切片,安Score 从大到小排序!!
7.实现接口vs 继承
-
大家听到现在,可能会对实现接口和继承比较迷茫了, 这个问题,那么他们究竟有什么区别呢
- 接口是对继承的补充
-
代码
package main import ( "fmt" ) / //猴子Monkey结构体 type Monkey struct { Name string } func (ithis *Monkey) Climbing() { //猴子Monkey的方法 fmt.Println(ithis.Name, "生来会爬树") } / / //小猴子LittleMonkey结构体 type LittleMonkey struct { Monkey //继承猴子Monkey } / / //声明鸟的能力BirdAble接口 type BirdAble interface { Flying() //飞翔Flying方法 } //让小猴子LittleMonkey实现鸟的能力BirdAble func (ithis *LittleMonkey) Flying() { fmt.Println(ithis.Name, "通过学习,会飞翔了") } / / //声明鱼的能力FishAble接口 type FishAble interface { Swimming() //游泳Swimming方法 } //让小猴子LittleMonkey实现鱼的能力FishAble func (ithis *LittleMonkey) Swimming() { fmt.Println(ithis.Name, "学会了游泳") } / func main() { //创建一个LittleMonkey实例 monkey := LittleMonkey{ Monkey{ Name: "悟空", }, } monkey.Climbing() //调用Monkey的方法 monkey.Flying() //调用鸟的能力BirdAble接口,实现飞翔Flying方法 monkey.Swimming() //调用鱼的能力BirdAble接口,实现游泳Swimming方法 }
-
对上面代码的小结
- 当A 结构体继承了B 结构体,那么A 结构就自动的继承了B 结构体的字段和方法,并且可以直接使用
- 当A 结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此我们可以认为:实现接口是对继承机制的补充.
-
实现接口可以看作是对继承的一种补充
-
接口和继承解决的解决的问题不同
继承的价值主要在于:解决代码的复用性和可维护性。
接口的价值主要在于:设计,设计好各种规范(方法),让其它自定义类型去实现这些方法。
-
接口比继承更加灵活
接口比继承更加灵活,继承是满足is - a 的关系,而接口只需满足like - a 的关系。
继承:人
Person
继承 学生Student
:学生继承了人接口:鸟的能力
BirdAble
像 小猴子LittleMonkey
:小猴子像鸟一样飞翔
- 接口在一定程度上实现代码解耦
十、面向对象编程三大特性-多态
1.基本介绍
变量(实例)具有多种形态。面向对象的第三大特征,在Go 语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。
2.快速入门
在前面的Usb 接口案例,Usb usb ,既可以接收手机变量,又可以接收相机变量,就体现了Usb 接口多态特性。
package main
import "fmt"
//声明一个接口
type Usb interface {
//声明了2个没有实现的方法
Start()
Stop()
}
//让手机Phone实现Usb接口的方法
type Phone struct {
}
func (p Phone) Start() {
fmt.Println("启动手机...")
}
func (p Phone) Stop() {
fmt.Println("关闭手机...")
}
//让相机Camera实现Usb接口的方法
type Camera struct {
}
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) { //2.接收到phone
//通过usb接口变量来调用start和stop方法
usb.Start() //3.寻找关于phone的Start方法 输出:启动手机...
usb.Stop() //4.寻找关于phone的Stop方法 输出:关闭手机...
// //上面等同于下面这个
// phone := Phone{}
// phone.Start()
}
func main() {
//测试,先创建结构体变量
computer := Computer{}
phone := Phone{}
camera := Camera{}
computer.Working(phone) //1.传入phone参数
fmt.Println()
computer.Working(camera)
}
3.接口体现多态的两种形式
-
多态参数
在前面的Usb 接口案例,Usb usb ,即可以接收手机变量,又可以接收相机变量,就体现了Usb 接口多态。
-
多态数组
演示一个案例:给Usb 数组中,存放Phone 结构体和Camera 结构体变量
案例说明:
package main import "fmt" /// //声明一个接口 type Usb interface { //声明了2个没有实现的方法 Start() Stop() } /// /// //让手机Phone实现Usb接口的方法 type Phone struct { Name string } func (p Phone) Start() { fmt.Println("启动手机...") } func (p Phone) Stop() { fmt.Println("关闭手机...") } func (p Phone) Call() { fmt.Println("可以使用电话...") } /// /// //让相机Camera实现Usb接口的方法 type Camera struct { Name string } func (c Camera) Start() { fmt.Println("启动相机...") } func (c Camera) Stop() { fmt.Println("关闭相机...") } /// type Computer struct { } func (computer Computer) Working(usb Usb) { usb.Start() usb.Stop() //如果usb是指向Phone结构体变量,则还需要调用Call方法 //类型断言 if phone, ok := usb.(Phone); ok { //判断usb的参数是否为phone, phone.Call() //当等于时就执行Call()方法,否则跳过 } } func main() { //定义一个Usb接口数组,可以存放Phone和Camear的结构体变量 //这里就体现出多态数组 var usbArr [3]Usb //声明一个数组,里面可以存放3个元素 fmt.Println(usbArr) //[<nil> <nil> <nil>] //使用接口实现多个数据类型 usbArr[0] = Phone{"vivo"} usbArr[1] = Phone{"小米"} usbArr[2] = Camera{"索尼"} fmt.Println(usbArr) //多态数组[{vivo} {小米} {索尼}] fmt.Println() var computer Computer for _, v := range usbArr { computer.Working(v) fmt.Println() } }
4.类型断言
1.基本介绍
类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言,具体的如下:
-
对上面代码的说明:
在进行类型断言时,如果类型不匹配,就会报panic, 因此进行类型断言时,要确保原来的空接口指向的就是断言的类型.
-
如何在进行断言时,带上检测机制,如果成功就ok,否则也不要报panic
package main import "fmt" func main() { //带检测的类型断言 var x interface{} var b float32 = 1.1 x = b y, ok := x.(float64) //返回ok是否为ture或false if ok { //等同于ok == ture fmt.Println("convert succes") fmt.Printf("y的类型是%T 值是%v", y, y) } else { fmt.Println("convert fail") } fmt.Println("继续执行") }
2.类型断言的最佳实践
-
在前面的Usb 接口案例做改进:
给Phone 结构体增加一个特有的方法call(), 当Usb 接口接收的是Phone 变量时,还需要调用call方法
package main import "fmt" /// //声明一个接口 type Usb interface { //声明了2个没有实现的方法 Start() Stop() } /// /// //让手机Phone实现Usb接口的方法 type Phone struct { Name string } func (p Phone) Start() { fmt.Println("启动手机...") } func (p Phone) Stop() { fmt.Println("关闭手机...") } func (p Phone) Call() { fmt.Println("可以使用电话...") } /// /// //让相机Camera实现Usb接口的方法 type Camera struct { Name string } func (c Camera) Start() { fmt.Println("启动相机...") } func (c Camera) Stop() { fmt.Println("关闭相机...") } /// type Computer struct { } func (computer Computer) Working(usb Usb) { usb.Start() usb.Stop() //如果usb是指向Phone结构体变量,则还需要调用Call方法 //类型断言 if phone, ok := usb.(Phone); ok { //判断usb的参数是否为phone, phone.Call() //当等于时就执行phone.Call()方法,否则跳过 } } func main() { //定义一个Usb接口数组,可以存放Phone和Camear的结构体变量 //这里就体现出多态数组 var usbArr [3]Usb //声明一个数组,里面可以存放3个元素 fmt.Println(usbArr) //[<nil> <nil> <nil>] //使用接口实现多个数据类型 usbArr[0] = Phone{"vivo"} usbArr[1] = Phone{"小米"} usbArr[2] = Camera{"索尼"} fmt.Println(usbArr) //多态数组[{vivo} {小米} {索尼}] fmt.Println() var computer Computer for _, v := range usbArr { computer.Working(v) fmt.Println() } }
-
写一函数,循环判断传入参数的类型:
package main import "fmt" //编写一个函数,可以判断输入的参数是什么类型 func TyeepJudge(items ...interface{}) { //定义一个...根据大小自动扩容的空接口 for index, x := range items { //遍历items ,index为下标,x为值 index++ switch x.(type) { //固定写法,传入遍历后的值对类型进行判断 case bool: fmt.Printf("第%v个参数是bool类型,值是%v\n", index, x) case float32: fmt.Printf("第%v个参数是float32类型,值是%v\n", index, x) case float64: fmt.Printf("第%v个参数是float64类型,值是%v\n", index, x) case int, int16, 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.2 var n2 float64 = 1.43 var n3 int32 = 43 var n4 string = "tom" n5 := "北京" n6 := 123 TyeepJudge(n1, n2, n3, n4, n5, n6) }
-
在前面代码的基础上,增加判断Student 类型和*Student 类型
package main import "fmt" //1.自定义一个类型 type Student struct{} //编写一个函数,可以判断输入的参数是什么类型 func TyeepJudge(items ...interface{}) { //定义一个...根据大小自动扩容的空接口 for index, x := range items { //遍历items ,index为下标,x为值 index++ switch x.(type) { //固定写法,传入遍历后的值对类型进行判断 case bool: fmt.Printf("第%v个参数是bool类型,值是%v\n", index, x) case float32: fmt.Printf("第%v个参数是float32类型,值是%v\n", index, x) case float64: fmt.Printf("第%v个参数是float64类型,值是%v\n", index, x) case int, int16, int32, int64: fmt.Printf("第%v个参数是 整数 类型,值是%v\n", index, x) case string: fmt.Printf("第%v个参数是string类型,值是%v\n", index, x) case Student: //可以直接进行判断 fmt.Printf("第%v个参数是Student类型,值是%v\n", index, x) case *Student: //可以直接进行判断 fmt.Printf("第%v个参数是*Student类型,值是%v\n", index, x) default: fmt.Printf("第%v个参数类型不确定,值是%v\n", index, x) } } } func main() { var n1 float32 = 1.2 var n2 float64 = 1.43 var n3 int32 = 43 var n4 string = "tom" n5 := "北京" n6 := 123 stu1 := Student{} //2.自定义类型 stu2 := &Student{} //3.指针类型 TyeepJudge(n1, n2, n3, n4, n5, n6, stu1, stu2) //4.传入参数 }
![](https://img-blog.csdnimg.cn/614deab842d548d0a302692cb08e740c.jpeg)
章节目录
【Golang第1~3章:基础】如何安装golang、第一个GO程序、golang的基础
【Golang第4章:函数】Golang包的引用,return语句、指针、匿名函数、闭包、go函数参数传递方式,golang获取当前时间
【Golang第5章:数组与切片】golang如何使用数组、数组的遍历和、使用细节和内存中的布局;golang如何使用切片,切片在内存中的布局
【Golang第6章:排序和查找】golang怎么排序,golang的顺序查找和二分查找,go语言中顺序查找二分查找介绍和案例
【Golang第7章:map】go语言中map的基本介绍,golang中map的使用案例,go语言中map的增删改查操作,go语言对map的值进行排序
【Golang第8章:面向对象编程】Go语言的结构体是什么,怎么声明;Golang方法的调用和声明;go语言面向对象实例,go语言工厂模式;golang面向对象的三大特性:继承、封装、多态
【Golang第9章:项目练习】go项目练习家庭收支记账软件项目、go项目练习客户管理系统项目
【Golang第10章:文件操作】GO语言的文件管理,go语言读文件和写文件、GO语言拷贝文件、GO语言判断文件是否存在、GO语言Json文件格式和解析
【Golang第12章:goroutine协程与channel管道】GO语言goroutine协程和channel管道的基本介绍、goroutine协