【Golang第8章:面向对象编程】Go语言的结构体是什么,怎么声明;Golang方法的调用和声明;go语言面向对象实例,go语言工厂模式;golang面向对象的三大特性:继承、封装、多态
介绍
这个是在B站上看边看视频边做的笔记,这一章是Glang面向对象编程
这一章内容较多,内容有Go语言的结构体是什么,怎么声明;Golang方法的调用和声明;go语言面向对象实例,go语言工厂模式;golang面向对象的三大特性:继承、封装、多态,面向对象编程应用实例,golang的接口介绍,类型断言
配套视频自己去B站里面搜【go语言】,最高的播放量就是
里面的注释我写的可能不太对,欢迎大佬们指出╰(°▽°)╯
文章目录
- 介绍
- (八)、面向对象编程
- 1.看一个问题
- 2.使用现有技术解决
- 3.现有技术解决的缺点分析
- 4.Golang 语言面向对象编程说明
- 一、结构体
- 1.快速入门
- 2.结构体变量(实例)在内存的布局(重要!)
- 3.如何声明结构体
- 4.字段/属性
- 5.创建结构体变量和访问结构体字段
- 6.struct 类型的内存分配机制
- 7.结构体使用注意事项和细节
- 二、方法
- 1.基本介绍
- 2.方法的声明和调用
- 3.方法快速入门
- 4.方法的调用和传参机制原理:(重要!)
- 5.方法的声明(定义)
- 6.方法的注意事项和细节
- 7.方法的练习题
- 练习题
- 8.方法和函数区别
- 三、面向对象编程应用实例
- 1.步骤
- 2.案例1
- 3.案例2
- 4.案例3
- 5.案例4
- 四、创建结构体变量时指定字段值
- 五、工厂模式
- 1.说明
- 2.工厂模式案例
- 3.思考题
- 六、面向对象编程思想-抽象
- 1.抽象的介绍
- 七、面向对象编程三大特性-封装
- 1.基本介绍
- 2.封装介绍
- 3.封装的理解和好处
- 4.如何体现封装
- 5.封装的实现步骤
- 6.快速入门案例
- 7.练习
- 八、面向对象编程三大特性-继承
- 1.引出继承的必要性
- 2.继承基本介绍和示意图
- 3.嵌套匿名结构体的基本语法
- 4.快速入门案例
- 5.继承的深入讨论
- 6.练习
- 7.面向对象编程-多重继承
- 九、接口(interface)
- 1.基本介绍
- 2.接口快速入门
- 3.基本语法
- 4.接口使用的应用场景
- 5.注意事项和细节
- 6.接口编程的最佳实践
- 7.实现接口vs 继承
- 十、面向对象编程三大特性-多态
- 1.基本介绍
- 2.快速入门
- 3.接口体现多态的两种形式
- 4.类型断言
- 1.基本介绍
- 2.类型断言的最佳实践
- 章节目录
(八)、面向对象编程
1.看一个问题
2.使用现有技术解决
package mainfunc 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 mainimport "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 mainimport "fmt"//定义一个Cat结构体,将Cat的各个字段/属性信息,放入到Cat结构体进行管理 type Cat struct {Name string //字符串模式是空Age int //int类型默认为0Color string //字符串模式是空Hobby string }func main() {/*张老太养了两只猫猫:一只名字叫小白,今年3岁,白色。还有一只叫小花,今年100岁,花色。请编写一个程序,当用户输入小猫的名字时,就显示该猫的名字,年龄,颜色。如果用户输入的小猫名错误,则显示张老太没有这只猫猫。*///使用struct结构体来完成案例var cat1 Cat //创建一个cat的变量fmt.Println(cat1)//给结构体赋值cat1.Name = "小白"cat1.Age = 3cat1.Color = "白色"fmt.Println("猫的信息如下:")fmt.Println("Nmae =", cat1.Name)fmt.Println("Age =", cat1.Age)fmt.Println("Color =", cat1.Color) }
通过上面的案例和讲解我们可以看出:
- 结构体和结构体变量(实例)的区别和联系
- 结构体是自定义的数据类型,代表一类事物.
- 结构体变量(实例)是具体的,实际的,代表一个具体变量
2.结构体变量(实例)在内存的布局(重要!)
3.如何声明结构体
基本语法
type 结构体名称 struct {field1 typefield2 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 mainimport "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 mainimport "fmt"type Monster struct {Name string Age int }func main() {//不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改, //不影响另外一个,结构体是值类型 var monster1 Monster monster1.Name = "牛魔王" monster1.Age = 500monster2 := monster1 //这里因为是值拷贝 结构体是值类型,默认为拷贝 monster2.Name = "红孩儿" //不会影响monster1fmt.Println("monster1=", monster1) //monster1= {牛魔王 500} fmt.Println("monster2=", monster2) //monster2= {红孩儿 500} }
上图内存示意图:
5.创建结构体变量和访问结构体字段
方式1-直接声明
案例演示:
var person Person
//方式1var p1 Personp1.Name = "tom"p1.Age = 23fmt.Println(p1)
方式2-{}
案例演示:
var person Person = Person{}
//方式2p2 := Person{"mary", 20}fmt.Println(p2)
方式3-&
案例演示:
var person *Person = new (Person)
//方式3var p3 *Person = new(Person) //因为p3是一个指针,因此标准的给字段方式是赋值(*p3).Name = "smith" //标准写法(*p3).Age = 30fmt.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 mainimport "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}出入给reghtDownfmt.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 地址=0xc00000c258r2.leftUp 本身地址=0xc00005a250 r2.reghtDown 本身地址=0xc00005a258 r2.leftUp 指向地址=0xc0000140c0 r2.reghtDown 指向地址=0xc0000140d0
对应的分析图:
结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
package mainimport "fmt"type A struct {Num int }type B struct {Num int }//结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型) func main() {var a A //a的类型是结构体Avar b B //b的类型是结构体Bfmt.Println(a, b) //{0} {0}a = A(b) //对b进行强制转换,转换为结构体A,之所以能进行强转,是因为结构体的类型字段是一致的(名字、格式、类型)fmt.Println(a) }
- 结构体进行type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
struct 的每个字段上,可以写上一个tag, 该tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
序列化的使用场景:
package mainimport ("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 mainimport "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.Namep.test() //调用方法 test() name = tom}
对上面的总结
test 方法和Person 类型绑定
test 方法只能通过Person 类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
func (p Person) test() {}
… p 表示哪个Person 变量调用,这个p 就是它的副本, 这点和函数传参非常相似。p 这个名字,有程序员指定,不是固定, 比如修改成person 也是可以
3.方法快速入门
给Person 结构体添加speak 方法,输出xxx 是一个好人
package mainimport "fmt"type Person struct {Name string }func (p Person) speak() {fmt.Println(p.Name, "是一个好人") }func main() {var p Person //必须先声明pp.Name = "tom"p.speak() }
给Person 结构体添加jisuan 方法,可以计算从1+…+1000 的结果, 说明方法体内可以函数一样,进行各种运算
package mainimport "fmt"type Person struct {Name string }//给Person 结构体添加jisuan 方法,可以计算从1+..+1000 的结果, 说明方法体内可以函数一样,进行各种运算 func (p Person) jisuan() {res := 0for i := 0; i <= 1000; i++ {res += i}fmt.Println(p.Name, "计算的结果是=", res) }func main() {var p Person //必须先声明pp.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 := 0for i := 0; i <= n; i++ {res += i}fmt.Println(p.Name, "计算的结果是=", res) } func main() {var p Person //必须先声明pp.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 //必须先声明pp.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 mainimport "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 Circlec.radius = 4.0res := 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 mainimport "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 Circlec.radius = 5.0res2 := (&c).area2() //标准写法fmt.Println(res2)//编译器底层做了优化(&c).area2() 等价c.area()//因为编译器会自动的给加上&cres1 := c.area2()fmt.Println(res1)}
Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct, 比如int , float32 等都可以有方法
package mainimport "fmt"/* Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型, 都可以有方法,而不仅仅是struct, 比如int , float32 等都可以有方法 */type integer intfunc (i integer) print() {fmt.Println("i =", i) }//编写一个方法, 可以改变i的值 func (i *integer) change() {*i = *i + 1 }func main() {var i integer = 10i.print() //i = 10i.change()fmt.Println("i =", i) //i = 11 }
方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。
如果一个类型实现了
String()
这个方法,那么fmt.Println
默认会调用这个变量的String()
进行输出package mainimport "fmt"type Student struct {Name stringAge int }func (stu *Student) String() string { //赋值Student的指针给stustr := 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 mainimport "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 MethodUtilsmu.Print()
}
- 编写一个方法,提供m 和n 两个参数,方法中打印一个m*n 的矩形
package mainimport "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 MethodUtilsmu.Print(8, 10)
}
编写一个方法算该矩形的面积(可以接收长len,和宽width), 将其作为方法返回值。在main方法中调用该方法,接收返回的面积值并打印。
编写方法:判断一个数是奇数还是偶数
根据行、列、字符打印对应行数和列数的字符,比如:行:3,列:2,字符*,则打印相应的效果
定义小小计算器结构体(Calcuator),实现加减乘除四个功能
实现形式1:分四个方法完成
实现形式2:用一个方法搞定
练习题
8.方法和函数区别
调用方式不一样
函数的调用方式: 函数名(实参列表)
方法的调用方式: 变量.方法名(实参列表)
对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
package mainimport "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 mainimport "fmt"/* 1) 编写一个Student 结构体,包含name、gender、age、id、score 字段,分别为string、string、int、int、float64 类型。 2) 结构体中声明一个say 方法,返回string 类型,方法返回信息中包含所有字段值。 3) 在main 方法中,创建Student 结构体实例(变量),并访问say 方法,并将调用结果打印输出。 */type Student struct {name stringgender stringage intid intscore 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 mainimport "fmt"type Stu struct {Name stringAge 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 mainimport "fmt"type Stu struct {Name stringAge 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 modeltype Student struct {Name string... }
因为这里的Student 的首字母S 是大写的,如果我们想在其它包创建Student 的实例(比如main 包),引入model 包后,就可以直接创建Student 结构体的变量(实例)。
但是问题来了,如果首字母是小写的,比如是type student struct {....}
就不不行了,怎么办 —> 工厂模式来解决.
2.工厂模式案例
使用工厂模式实现跨包创建结构体实例(变量)的案例:
如果model 包的结构体变量首字母大写,引入后,直接使用, 没有问题
student.go
package modeltype Student struct {Name stringScore float64 }
main.go
package mainimport ("demo/28demo/02/model""fmt" )func main() {//创建要给Student实例var stu = model.Student{Name: "小明",Score: 86.25,}fmt.Println(stu) }
如果model 包的结构体变量首字母小写,引入后,不能直接使用, 可以工厂模式解决
student.go
package modeltype student struct {Name stringScore float64 }//因为student结构体首字母是小写,因此是只能在mode1使用 //我们通过工厂模式来解决func NewStudent(n string, s float64) *student { //定义一个公开的方法,使用一个方法来接收。并把值返回return &student{Name: n,Score: s,} }
main.go
package mainimport ("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 modeltype student struct {Name stringscore float64
}//如果score字段首字母小写,则,在其它包不可以直接方法,我们可以提供一个方法
func (s *student) GetScore() float64 {return s.score
}
package mainimport ("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 mainimport "fmt"//定义一个结构体
type Account struct {AccountNo stringPwd stringBalance 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 += moneyfmt.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 -= moneyfmt.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 stringvar password stringvar input stringfmt.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 modelimport "fmt"type person struct {Name stringage 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 mainimport ("demo/29demo/model""fmt" )func main() {p := model.NwePerson("小明") //声明model并把小明赋值给结构体person的namefmt.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 modelimport "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 mainimport ("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 mainimport "fmt"//编写一个学生考试系统//小学生考试
type Pupil struct {Name stringAge intScore 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 stringAge intScore 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 stringPrice int
}
type Book struct {Goods //这里就是嵌套匿名结构体GoodsWriter string
}
4.快速入门案例
案例
我们对extends01.go 改进,使用嵌套匿名结构体的方式来实现继承特性,请大家注意体会这样编程的好处
代码
package mainimport "fmt"type Student struct {Name stringAge intScore 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() {//当我们对结构体嵌入了匿名结构体后使用方法会发生变化//方式1pupil := &Pupil{}pupil.Student.Name = "tom"pupil.Student.Age = 10pupil.Tesing()pupil.Student.SteScore(75)pupil.Student.ShowInfo()graduate := &Graduate{}graduate.Student.Name = "mary"graduate.Student.Age = 20graduate.Tesing()graduate.Student.SteScore(68)graduate.Student.ShowInfo()fmt.Println()//方式2......pupil.Name = "tom~"pupil.Age = 100pupil.Tesing()pupil.SteScore(750)pupil.ShowInfo()graduate.Name = "mary~"graduate.Age = 200graduate.Tesing()graduate.SteScore(680)graduate.ShowInfo()}
5.继承的深入讨论
结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。【举例说明】
匿名结构体字段访问可以简化
package mainimport "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 Bb.A.Name = "tom"b.A.age = 19b.A.SayOk()b.A.hello()//上面写法可以简化b.Name = "smith"b.age = 20b.SayOk()b.hello()
}
对上面的代码小结
(1) 当我们直接通过b 访问字段或方法时,其执行流程如下比如
b.Name
(2) 编译器会先看b 对应的类型有没有Name, 如果有,则直接调用B 类型的Name 字段
(3) 如果没有就去看B 中嵌入的匿名结构体A 有没有声明Name 字段,如果有就调用,如果没有继续查找…如果都找不到就报错
- 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分【举例说明】
package mainimport "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 {AName string
}func (b *B) SayOk() {fmt.Println("B SayOk", b.Name)
}///func main() {var b Bb.Name = "jack" //使用就近原则,导入jackb.A.Name = "scott" //使用匿名结构体全路径b.age = 100b.SayOk() //使用就近原则b.A.SayOk() //使用匿名结构体全路径b.hello()
}
- 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。【举例说明】
package mainimport "fmt"type A struct {Name stringAge int
}type B struct {Name stringScore float64
}//C同时嵌入了2个不同的结构体,此时就必须明确指定匿名结构体名字
type C struct {AB
}func main() {var c C//如果c没有Name字段,而A和B有Name, 这时就必须通过指定置名结构体名字来区分//所以 c.Name 就会包编译错误, 这个规则对方法也是一 样的!c.A.Name = "tom"fmt.Println(c.A.Name)}
如果一个struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
package mainimport "fmt"type A struct {Name stringAge int }type D struct {a A //有名结构体 组合关系}func main() {如果D中是一个有名结构体,则访问有名结构体的字段时,就必须带上有名结构体的名字var d D//d.Name = "jack" //直接去d里面找Name,发现没有叫做Name的就直接报错,d.a.Name = "jack" //因为A是有名结构体,需要完整的路径}
嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
package mainimport "fmt"type Goods struct {Name stringPrice float64 }type Brend struct {Name stringAddrees string }type Tv1 struct {GoodsBrend }type Tv2 struct { //使用指针类型*Goods*Brend }func main() {//嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值tv1 := Tv1{Goods{"电视机001", 5999.09}, Brend{"海尔", "山东"}} //方式1fmt.Println(tv1)tv2 := Tv1{ //方式2Goods{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 mainimport "fmt"type Monster struct {Name stringAge int
}type E struct {Monsterint //匿名字段时基本数据类型n int //如果有了1个int,后面int必须有名称
}func main() {var e Ee.Name = "牛魔王"e.Age = 500e.int = 20e.n = 10fmt.Println(e) //{{牛魔王 500} 20 10}
}
说明
- 如果一个结构体有 int 类型的匿名字段,就不能第二个。
- 如果需要有多个 int 的字段,则必须给 int 字段指定名
7.面向对象编程-多重继承
如一个 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方 法,从而实现了多重继承。
案例
通过一个案例来说明多重继承使用
多重继承细节说明
如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。【案例演示】
为了保证代码的简洁性,建议大家尽量不使用多重继承
九、接口(interface)
1.基本介绍
按顺序,我们应该讲解多态,但是在讲解多态前,我们需要讲解接口(interface),因为在Golang 中多态特性主要是通过接口来体现的。
2.接口快速入门
这样的设计需求在Golang 编程中也是会大量存在的,前面说过,一个程序就是一个世界,在现实世界存在的情况,在程序中也会出现。我们用程序来模拟一下前面的应用场景。
快速入门案例
package mainimport "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 mainimport "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 = stustu.Say()a.Say() }
接口中所有的方法都没有方法体,即都是没有实现的方法。
在Golang 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口。
一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。
一个自定义类型可以实现多个接口
package mainimport "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 和BInterfacevar monster Monstervar a AInterface = monstervar b BInterface = monstera.Say()b.Hello() }
Golang 接口中不能有任何变量
一个接口(比如A 接口)可以继承多个别的接口(比如B,C 接口),这时如果要实现A 接口,也必须将B,C 接口的方法也全部实现。
package maintype BInterface interface {test01() }type CInterface interface {test02() }type AInterface interface { //如果要实现A接口,必须把A接口所有的反法全部实现BInterfaceCInterfacetest03() }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 Stuvar a AInterface = stuvar b BInterface = stua.test01()b.test01() }
interface 类型默认是一个指针(引用类型),如果没有对interface 初始化就使用,那么会输出nil
空接口
interface{}
没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量赋给空接口。package mainimport "fmt"type T interface{} //定义一个空接口type Stu struct { //定义一个结构体 }func main() {var nume float64 = 8.0//方式1var stu Stu //调用结构体var t1 T = stu //声明位空接口类型并调用结构体t1 = numefmt.Println(t1)//方式2var t2 interface{} = stu //定义一个空接口类型,并传给t2t2 = numefmt.Println(t2) }
6.接口编程的最佳实践
实现对Hero 结构体切片的排序:
sort.Sort(data Interface)
package mainimport ("fmt""math/rand""sort" )// 实现对Hero 结构体切片的排序: sort.Sort(data Interface)//1.声明Hero结构体 type Hero struct {Name stringAge 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//生成英雄herofor 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.Sortsort.Sort(heros)//看看排序后的顺序for _, v := range heros {fmt.Println(v)} }
接口编程练习
//1.声明Student 结构体 type Student struct{Name stringAge intScore float64 } //将Student 的切片,安Score 从大到小排序!!
7.实现接口vs 继承
大家听到现在,可能会对实现接口和继承比较迷茫了, 这个问题,那么他们究竟有什么区别呢
- 接口是对继承的补充
代码
package mainimport ("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 mainimport "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 mainimport "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 Computerfor _, v := range usbArr {computer.Working(v)fmt.Println()} }
4.类型断言
1.基本介绍
类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言,具体的如下:
对上面代码的说明:
在进行类型断言时,如果类型不匹配,就会报panic, 因此进行类型断言时,要确保原来的空接口指向的就是断言的类型.
如何在进行断言时,带上检测机制,如果成功就ok,否则也不要报panic
package mainimport "fmt"func main() {//带检测的类型断言var x interface{}var b float32 = 1.1x = by, ok := x.(float64) //返回ok是否为ture或falseif ok { //等同于ok == turefmt.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 mainimport "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 Computerfor _, v := range usbArr {computer.Working(v)fmt.Println()} }
写一函数,循环判断传入参数的类型:
package mainimport "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.2var n2 float64 = 1.43var n3 int32 = 43var n4 string = "tom"n5 := "北京"n6 := 123TyeepJudge(n1, n2, n3, n4, n5, n6) }
在前面代码的基础上,增加判断Student 类型和*Student 类型
package mainimport "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.2var n2 float64 = 1.43var n3 int32 = 43var n4 string = "tom"n5 := "北京"n6 := 123stu1 := Student{} //2.自定义类型stu2 := &Student{} //3.指针类型TyeepJudge(n1, n2, n3, n4, n5, n6, stu1, stu2) //4.传入参数 }
章节目录
【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第11章:单元测试】GO语言单元测试
【Golang第12章:goroutine协程与channel管道】GO语言goroutine协程和channel管道的基本介绍、goroutine协
【Golang第8章:面向对象编程】Go语言的结构体是什么,怎么声明;Golang方法的调用和声明;go语言面向对象实例,go语言工厂模式;golang面向对象的三大特性:继承、封装、多态相关推荐
- 面向对象之三大特性:继承,封装,多态
python面向对象的三大特性:继承,封装,多态. 1. 封装: 把很多数据封装到⼀个对象中. 把固定功能的代码封装到⼀个代码块, 函数, 对象, 打包成模块. 这都属于封装的思想. 具体的情况具体分 ...
- Swift学习 OOP三大特性:继承、多态、封装
先看个例子 从上面的例子可以总结那么一句话:"学生是人".也就是Student类继承People类.简而言之,学生是人,这句话是说得通的,但是"人是学生"这句 ...
- c语言字符数组赋值_C语言关于结构体字符成员元素赋值的方法
C语言部分有些容易忘记的内容,这样贴在这里方便大家学习和查阅 本章节里面有结构体和数组长度的问题. 欢迎大家来点评 #include #include //结构体 struct student{ in ...
- 面向对象的三大特性————继承,多态
1,继承:继承是一种创建新类的方式,在python中,新建的类可以继承一个或多个父类,父类又可称为基类或超类,新建的类称派生类或子类. 一个类可以被多个类继承,一个类可以继承多个父类. 没有继承父类默 ...
- C语言结构体自动初始化实现,C语言中结构体(struct)的几种初始化方法
本文给大家总结的struct数据有3种初始化方法 1.顺序 2.C风格的乱序 3.C++风格的乱序 下面通过示例代码详细介绍这三种初始化方法. 1)顺序 这种方法很常见,在一般的介绍C的书中都有介绍. ...
- c++ 结构体初始化_单片机C语言 - 基于结构体的面向对象编程技巧
单片机C语言 - 基于结构体的面向对象编程技巧 一.面向对象 面向对象是软件开发方法,是相对于面向过程来讲的.通过把数据与方法组织为一个整体来看待,从更高的层次来进行系统建模,更贴近事物的自然运行模式 ...
- 【Go语言】【13】再谈GO语言的结构体
本文从如下四个方面再领着大家认识结构体 匿名结构体和匿名成员的结构体 值传递和引用传递 再谈嵌套结构体 面向对象 1.匿名结构体和匿名成员的结构体 如上篇所述,一个结构体需要先声明,再初始化,最后把初 ...
- c语言如何将值赋给结构体指针,C语言给结构体指针赋值
<C语言给结构体指针赋值>由会员分享,可在线阅读,更多相关<C语言给结构体指针赋值(6页珍藏版)>请在人人文库网上搜索. 1.指向结构体的指针 在C语言中几乎可以创建指向任何类 ...
- Java继承_Hachi君浅聊Java三大特性之 封装 继承 多态
Hello,大家好~我是你们的Hachi君,一个来自某学院的资深java小白.最近利用暑假的时间,修得满腔java语言学习心得.今天小宇宙终于要爆发了,决定在知乎上来一场根本停不下来的Hachi君个人 ...
最新文章
- 第八章 泛型程序设计
- linux两个文件修改主机名
- 熟悉的亲切-老外婆教做的豌豆蔬菜汤
- mysql數據庫的增刪改查_MySQL數據庫之基礎增刪改查操作
- MySQL设置表的字符编码为utf-8
- S5PV210之GPIO模拟I2c时序之pcf8591与at24xx linux3.0.8驱动
- 知乎超高赞:见识多的人,平时都在看些什么?
- 监督学习 | 线性回归 之正则线性模型原理及Sklearn实现
- Python面向对象基础一
- OpenGL:绘制太阳地球例子
- 视频PPT互动问答丨数据驱动的业务实践专题
- 利用UTL_FILE包实现文件I/O操作
- 3 天开发物联网应用!腾讯云 IoT 超级小程序来了
- python开发环境anaconda3_使用Anaconda3配置多版本Python虚拟开发环境
- 系统学习NLP(八)--中文分词整理
- 一步步学习SPD2010--第十四章节--在Web页面使用控件(2)--使用标准ASP.NET服务器控件...
- 图书速读 | 一分钟读完《如何成为学习高手》
- ros中odometry数据生成方式与分发去向
- C. DZY Loves Fibonacci Numbers(线段树fibonacci)
- printf 函数使用 可变参数函数实现原理