go语言系列-面向对象编程
面向对象编程
结构体
一个程序就是一个世界,有很多对象(变量)
go也支持面向对象编程(oop),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以说go支持面向对象编程特性是比较准确的
go没有类(class),go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,可以理解go是基于struct来实现oop特性的。
go面向对象编程非常简洁,去掉了传统oop语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等等
go仍然有面向对象编程的继承、封装和多态的特性,只是实现的方式和其它oop语言不一样,比如继承:go没有extends关键字,继承是通过匿名字段来实现
- go面向对象(oop)很优雅,oop本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,非常灵活。后面会充分体会这个特点。也就是说在go中面向接口编程是非常重要的特性
结构体与结构体变量(实例/对象)的关系示意图
*注意:从猫结构体到变量,就是创建一个cat结构体变量,也可以说是定义cat结构体变量*
对上图的说明
将一类事物的特性提取出来(比如猫类),形成一个新的数据类型,就是一个结构体
通过这个结构体,我们可以创建多个变量(实例/对象)
事物可以猫类,也可以是person,fish 或是某个工具类
基本语法 type 结构体名称 struct { field1 type field2 type } type cat struct { name string age int color string hobby string } func main() { //创建一个cat的变量 var cat1 cat cat1.name = "小白" cat1.age = 3 cat1.color = "白色" cat1.hobby = "吃 <·)))><<" fmt.println("cat1 = ", cat1) fmt.println("猫猫的信息如下:") fmt.println("name = ", cat1.name) fmt.println("age = ", cat1.age) fmt.println("color = ", cat1.color) fmt.println("hobby = ", cat1.hobby) } //cat1 = {小白 3 白色 吃 <·)))><<} //猫猫的信息如下: //name = 小白 //age = 3 //color = 白色 //hobby = 吃 <·)))><<
结构体和结构体变量(实例)的区别和联系
结构体是自定义的数据类型,代表一类事物
结构体变量(实例)是具体的,实际的,代表一个具体变量
结构体变量(实例)在内存的布局【重要】
字段/属性
基本介绍
1) 从概念或叫法上看:结构体字段 = 属性 = field
2) 字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。比如我们前面定义猫结构体的name string 就是属性
注意事项和细节说明
-
字段声明语法同变量,示例:字段名 字段类型
-
字段的类型可以为:基本类型、数组或引用类型
-
在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则如下:
布尔类型是false,数值是0,字符串是””
数组类型的默认值和它的元素类型相关,比如score[3]int 则为[0,0,0]
指针,slice和map的零值都是nil,即还没有分配空间
//如果结构体的字段类型是:指针,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) if p1.ptr == nil { fmt.println("ok1") } if p1.slice == nil { fmt.println("ok2") } if p1.map1 == nil { fmt.println("ok3") } //使用slice,再次说明,一定要make p1.slice = make([]int, 10) p1.slice[0] = 100 //使用map,一定要先make p1.map1 = make(map[string]string) p1.map1["key1"] = "tom" fmt.println(p1) } //输出:{ 0 [0 0 0 0 0] <nil> [] map[]} //ok1 //ok2 //ok3 //{ 0 [0 0 0 0 0] <nil> [100 0 0 0 0 0 0 0 0 0] map[key1:tom]}
- 不同结构体变量的字段是独立的,互不影响,一个结构体变量字段的更改,不影响另外一个,结构体是值类型
type monster struct { name string age int } func main(){ var monster1 monster monster1.name = "牛魔王" monster1.age = 500 monster2 := monster1 //结构体是值类型,默认为值拷贝 monster2.name = "青牛精" fmt.println("monster1 = ",monster1) fmt.println("monster2 = ",monster2) } //输出:monster1 = {牛魔王 500} //monster2 = {青牛精 500}
创建结构体变量和访问结构体字段
方式1:直接声明 var person person type cat struct { name string age int color string hobby string } func main() { //创建一个cat的变量 var cat1 cat cat1.name = "小白" cat1.age = 3 cat1.color = "白色" cat1.hobby = "吃 <·)))><<" 方式2:{} var person person = person{} type monster struct { name string age int } func main(){ //{} p := monster{"zisefeizhu",21} fmt.println(p) //输出:{zisefeizhu 21} } 方式3: & var person *person = new (person) type monster struct { name string age int } func main(){ //方式3 //案例:var person *person = new (person) var p3 *monster = new(monster) //因为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) //{john 100} } 方式4: {} var person *person = &person{} type monster struct { name string age int } func main(){ //方式4 - {} //var person *person = &person{} //下面的语句,也可以直接给字符赋值 //var person *person = &person{"mary",60} var person *monster = &monster{} //因为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) //{scott ~ 10} } 说明: 1)第3种和第4种方式返回的是结构体指针 2)结构体指针访问字段的标准方式应该是:(*结构体指针)字段名,比如(*person).name = “tom” 3)但go做了一个简化,也支持 结构体指针.字段名,比如person.name = “tom”。更加符合程序员使用的习惯,go编译器底层对person.name做了转化(*person).name
struct类型的内存分配机制
定义一个person结构体(包括 名字,年龄)
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) p2.name = "tom" fmt.printf("p2.name = %v p1.name = %v",p2.name,p1.name) } //输出:10 //p2.name = tom p1.name = 小明
变量总是存在内存中的,那么结构体变量在内存中究竟是怎样存在的?
画一个图说明:结构体变量在内存中如何存在
看下面代码,分析原因
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) //10 fmt.println(p2.age) //10 p2.name = "tom ~" fmt.printf("p2.name = %v p1.name = %v \n",p2.name,p1.name) //p2.name = tom ~ p1.name = tom ~ fmt.printf("p2.name = %v p1.name = %v \n",(*p2).name,p1.name) //p2.name = tom ~ p1.name = tom ~ fmt.printf("p1的地址%p\n",&p1) //p1的地址0xc00004a420 fmt.printf("p2的地址%p p2的值%p\n",&p2, p2) //p2的地址0xc000080018 p2的值0xc00004a420 fmt.println(p2.age) //10 p2.name = "tom" fmt.printf("p2.name = %v p1.name = %v",p2.name,p1.name) //p2.name = tom p1.name = tom }
结构体使用注意事项和细节
1)结构体的所有字段在内存中连续的
//结构体 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\n" , &r1.leftup.x, &r1.leftup.y, &r1.rightdown.x, &r1.rightdown.y) //r.leftup.x 地址=0xc000052140 r1.leftup.y 地址=0xc000052148 r1.rightdown.x 地址=0xc000052150 r1.rightdown.y 地址=0xc000052158 //r2有两个*point类型,这两个*point类型的本身地址也是连续的 //但是它们指向的地址不一定是连续 r2 := rect2{&point{10,20},&point{30,40}} //打印地址 //打印地址 fmt.printf("r2.leftup 本身地址 = %p r2.rightdown 本身地址 = %p \n", &r2.leftup, &r2.rightdown) //r2.leftup 本身地址 = 0xc0000401c0 r2.rightdown 本身地址 = 0xc0000401c8 //它们指向的地址不一定是连续... 这个要看系统在运行时是如何分配 fmt.printf("r2.leftup 指向地址 = %p r2.rightdown 指向地址 = %p \n", r2.leftup, r2.rightdown) //r2.leftup 指向地址 = 0xc000054090 r2.rightdown 指向地址 = 0xc0000540a0 }
对应的分析图
2)结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
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) //{0} {0} }
- 结构体进行type重新定义(相当于取别名),go认为是新的数据类型,但是相互间可以强转
- struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用就是序列化和反序列化
序列化的常见使用
import ( "encoding/json" "fmt" ) type monster struct { name string `json:"name"` // `json:"name"` 就是struct tag age int `json:"age"` skill string `json:"skill"` } func main() { //1.创建一个monster变量 monster := monster{"牛魔王",500,"芭蕉扇"} //2.将monster变量序列化为json格式字串 // json.marshal 函数中使用反射,这里只是用一下反射,在后面会详细介绍 jsonstr, err := json.marshal(monster) if err != nil { fmt.println("json 处理错误",err) } fmt.println("jsonstr",string(jsonstr)) //jsonstr {"name":"牛魔王","age":500,"skill":"芭蕉扇"} }
方法
在某些情况下,需要声明(定义)方法。比如person结构体:除了有一些字段外(年龄,姓名...),person结构体还有一些行为比如:可以说话、跑步...,通过学习,还可以做算术题。这时就要用方法才能完成
go中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct
方法的声明和调用
type a struct { num int } func (a a)test(){ fmt.println(a.num) } 对上面的语法的说明 func (a a)test() {} 表示a结构体有 - 方法,方法名为test (a a)体现test方法是和a类型绑定的 type person struct { name string } //给person类型绑定 -- 方法 func (p person) test() { fmt.println("test() name = ", p.name) //test() name = tom } func main() { var p person p.name = "tom" p.test() // 调用方法 }
对上面的总结
- test方法和person类型绑定
2)test方法只能通过person类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
-
func (p person)test(){}... p表示哪个person变量调用,这个p就是它的副本,这点和函数传参非常相似
-
p这个名字,由程序员指定,不是固定,比如修改成person也是可以
type person struct { name string } //给person类型绑定 -- 方法 func (person person) test() { fmt.println("test() name = ", person.name) //test() name = tom } func main() { var p person p.name = "tom" p.test() // 调用方法 }
快速入门
- 给person结构体添加speak方法,输出xxx是一个好人
type person struct { name string } //给person结构体添加speak方法,输出xxx是一个好人 func (p person) speak() { fmt.println(p.name,"是一个goodman~") //tom 是一个goodman~ } func main() { var p person p.name = "tom" p.speak() }
- 给person结构体添加jisuan方法,可以计算从1+..+1000的1结果,说明:方法体内可以像函数一样进行各种运算
type person struct { name string } //给person结构题添加jisuan方法,可以计算从1+..+1000的1结果 func (p person) jisuan() { res := 0 for i:=1; i <= 1000; i++ { res += i } fmt.println(p.name,"计算的结构是 = ", res) //tom 计算的结构是 = 500500 } func main() { var p person p.name = "tom" p.jisuan() }
- 给person结构体jisuan2方法,该方法可以接收一个数n,计算从1+..+n的结果
type person struct { name string } func (p person) jisuan2(n int) { res := 0 for i:=1; i <= n; i++ { res += i } fmt.println(p.name,"计算的结构是 = ", res) //tom 计算的结构是 = 210 } func main() { var p person p.name = "tom" p.jisuan2(20) }
- 给person结构体添加getsum方法,可以计算两个数的和,并返回结果
type person struct { name string } func (p person) getsum(n1 int, n2 int) int { return n1 + n2 } func main() { var p person res := p.getsum(10, 20) fmt.println("res = ", res) //res = 30 }
方法的调用和传参机制原理[重要]
方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当作实参也传递给方法。
案例1
画出前面getsum方法的执行过程+说明
-
在通过一个变量去调用方法时,其调用机制和函数一样
-
不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝)
案例2
请编写一个程序,要求如下:
-
声明一个结构体circle,字段为radius
-
声明一个方法area和circle绑定,可以返回面积
-
提示:画出area执行过程+说明
方法的声明(定义)
func (recevier type) methodname (参数列表) (返回值列表) { 方法体 return 返回值 } 1)参数列表:表示方法输入 2)recevier type:表示这个方法和type这个类型进行绑定,或者说该方法作用于type类型 3)recevier type:type可以是结构体,也可以其它的自定义类型 4)recevier:就是type类型的一个变量(实例),比如:person结构体的一个变量(实例) 5)返回值列表:表示返回的值,可以多个 6)方法主体:表示为了实现某一功能代码块 7)return语句不是必须的
方法的注意事项和细节
-
结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
-
如果程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
-
go中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是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.change() fmt.println("i = ", i) } //输出:i = 10 //i = 11
-
方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问
-
如果一个类型实现了string()这个方法,那么fmt.println默认会调用这个变量的string()进行输出
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) } //输出:name = [tom] age = [20]
方法和函数的区别
- 调用方式不一样
函数的调用方式:函数名(实参列表)
方法的调用方式:变量.方法名(实参列表)
- 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
type person struct { name string } //函数 //对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然 func test01(p person) { fmt.println(p.name) //tom } func test02(p *person) { fmt.println(p.name) //tom } func main() { p := person{"tom"} test01(p) test02(&p) }
- 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
type person struct { name string } //3)对于方法(如struct的方法), // 接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以 func (p person) test03() { p.name = "jack" fmt.println("test03() = ",p.name) } func (p *person) test04() { p.name = "mary" - fmt.println("test04() = ",p.name) } func main() { p := person{"tom"} p.test03() fmt.println("main() p.name = ",p.name) (&p).test03() //从形式上传入地址,但是本质仍然是值拷贝 fmt.println("main() p.name = ",p.name) (&p).test04() fmt.println("main() p.name = ",p.name) p.test04() } //test03() = jack //main() p.name = tom //test03() = jack //main() p.name = tom //test04() = mary //main() p.name = mary //test04() = mary
总结
-
不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定
-
如果是和值类型,比如(p person),则是值拷贝,如果和指针类型,比如是(p person)则是地址拷贝
方法练习题
编写结构体(methodutils),编程一个方法,方法不需要参数,在方法中打印一个 10*8的矩形,在main方法中调用该方法
type methodutils struct { //字段... } //给methodutils编写方法 func (mu methodutils) print() { for i := 1; i <= 10; i++ { for j := 1; j <= 8; j++ { fmt.print("*") } fmt.println() } } func main() { var mu methodutils mu.print() } //输出: //******** //******** //******** //******** //******** //******** //******** //******** //******** //********
编写一个方法,提供m 和 n 两个参数,方法中打印一个 m * n 的矩形
type methodutils struct { //字段... } func (mu methodutils) print2(m int, n int) { for i := 1; i <= 10; i++ { for j := 1; j <= 8; j++ { fmt.print("*") } fmt.println() } } func main() { var mu methodutils mu.print2(10,8) } //输出: //******** //******** //******** //******** //******** //******** //******** //******** //******** //********
编写一个方法算该矩形的面积(可以接收长len,和宽width),将其作为方法返回值。在main方法中调用该方法,接收返回的面积值并打印
type methodutils struct { //字段... } func (mu methodutils) area(len float64, width float64) (float64) { return len * width } func main() { var mu methodutils fmt.println("面积 =",mu.area(10,20)) //面积 = 200 }
编写方法:判断一个数是奇数还是偶数
type methodutils struct { //字段... } func (mu *methodutils) judgenum(num int) { if num % 2 == 0 { fmt.println(num,"是偶数..") // 10 是偶数.. } else { fmt.println(num,"是奇数..") } } func main() { var mu methodutils mu.judgenum(10) }
根据行、列、字符打印对应行数和列数的字符,比如:行:3,列:2,字符*,则打印相应的效果
type methodutils struct { //字段... } func (mu *methodutils) print(n int, m int, key string) { for i := 1; i <= n; i++ { for j := 1; j <= m; j++ { fmt.print(key) } fmt.println() } } func main() { var mu methodutils mu.print(5,5,"+") }
定义小小计算器结构体(calcuator),实现加减乘除四个功能
实现形式1:分四个方法完成
实现形式2:用一个方法搞成
//实现形式1 type calcuator struct { num1 float64 num2 float64 } func (calcuator *calcuator) getsum() float64 { return calcuator.num1 + calcuator.num2 } func (calcuator *calcuator) getsub() float64 { return calcuator.num1 - calcuator.num2 } package main import "fmt" //实现形式2 type calcuator struct { num1 float64 num2 float64 } func (calcuator *calcuator) getres(operator byte) float64 { res := 0.0 switch operator { case '+': res = calcuator.num1 + calcuator.num2 case '-': res = calcuator.num1 - calcuator.num2 case '*': res = calcuator.num1 * calcuator.num2 case '/': res = calcuator.num1 / calcuator.num2 default: fmt.println("运算符输入有误...") } return res }
在merhodutils结构体编个方法,从键盘接收整数(1-9),打印对应乘法表
type merhodutils struct { //字段 } func (m merhodutils) jiu(n int) { for i := 1; i <= n; i++ { for j := 1; j <= i; j++ { fmt.printf("%v * %v = %v\t",i,j,i*j) } fmt.println() } } func main() { var mu merhodutils var num int fmt.println("请键入要输入的大于等于1小于等于9的自然数: ") fmt.scanln(&num) mu.jiu(num) } //请键入要输入的大于等于1小于等于9的自然数: //2 //1 * 1 = 1 //2 * 1 = 2 2 * 2 = 4
编写方法,使给定的一个二维数组(3 × 3)转置
面向对象编程应用实例
步骤
-
声明(定义)结构体,确定结构体名
-
编写结构体的字段
-
编写结构体的方法
学生案例
-
编写一个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: "zisefeizhu", gender:"male", age: 18, id: 1000, score: 99.98, } fmt.println(stu.say()) } //输出:student 的信息 name = [zisefeizhu] gender = [male] age = [18] id = [1000] score = [99.98]
小狗案例
-
编写一个dog结构体,包含name、age、weight字段
-
结构体中声明一个say方法,返回string类型,方法返回信息中包含所有字段值
-
在main方法中,创建dog结构体实例(变量),并返回say方法,将调用结果打印输出
type dog struct { name string age int wgight float64 } func (dog *dog) say() string { infostr := fmt.sprintf("dog 的信息 name = [%v] age = [%v] weight = [%v]", dog.name, dog.age, dog.wgight) return infostr } func main() { //创建一个student实例变量 var stu = dog{ name: "xiaohua", age: 18, wgight: 23, } fmt.println(stu.say()) } //输出:dog 的信息 name = [xiaohua] age = [18] weight = [23]
盒子案例
-
编程创建一个box结构体,在其中声明三个字段表示一个立方体的长、宽和高,长宽高要从终端获取
-
声明一个方法获取立方体的体积
-
创建一个box结构体变量,打印给定尺寸的立方体的体积
type box struct { len float64 width float64 height float64 } //声明一个方法获取立方体的体积 func (box *box) getvolum() 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.getvolum() fmt.printf("体积为=%.2f",volumn) } //输出:体积为=6.60
景区门票案例
-
一个景区根据游人的年龄收取不同价格的门票,比如年龄大于18,收费20元,其它情况门票免费
-
请编写visitor结构体,根据年龄段决定能够购买的门票价格并输出
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() } } //输出:请输入你的名字 //zisefeizhu //请输入你的年龄 //20 //游客的名字为 zisefeizhu 年龄为 20 收费20元 //请输入你的名字 //n //退出程序...
创建结构体变量时指定字段值
go在创建结构体实例(变量)时,可以直接指定字段的值
方式1: type stu struct { name string age int } func main() { //方式1 //在创建结构体变量时,就直接指定字段的值 var stu1 = stu{"zisefeizhu",20} // stu1 --> 结构体数据空间 stu2 := stu{"jingxing",20} //在创建结构体变量时,把字段名和字段值写在一起,这种写法,就不依赖字段的定义顺序 var stu3 = stu{ name: "yike", age: 20, } var stu4 = stu{ name: "gengpan", age: 20, } fmt.println(stu1, stu2, stu3, stu4) } //输出:{zisefeizhu 20} {jingxing 20} {yike 20} {gengpan 20} 方式2: type stu struct { name string age int } func main() { //方式2 var stu5 *stu = &stu{"小王", 20} //stu5 --> 地址 --> 结构体数据[xxxx,xxxx] stu6 := &stu{"小紫", 20} //在创建结构体指针变量时。把字段名和字段值写在一起,这种写法,就不依赖字段的定义顺序 var stu7 = &stu{ name: "小林", age: 20, } var stu8 = &stu{ age: 20, name: "小耿", } fmt.println(*stu5, *stu6, *stu7, *stu8) } //输出:{小王 20} {小紫 20} {小林 20} {小耿 20}
工厂模式
go的结构体没有构造函数,通常可以使用工厂模式来解决这个问题
看一个需求
一个结构体的声明是这样的:
pachage model type student struct { name string ... }
因为这里的student的首字母s是大写的,如果我们想在其它包创建student的实例(比如main包),引入model包后,就可以直接创建student结构体的变量(实例)。但是问题来了,如果首字母是小写的,比如是type student struct {...}就不行了,怎么办-->工厂模式来解决>
工厂模式来解决问题
使用工厂模式实现跨包创建结构体实例(变量)的案例:
如果model包的*结构体变**量**首字母大写,引入后,直接使用*,没有问题
![img](file:///c:\users\linkun\appdata\local\temp\ksohtml1372\wps1.jpg)
如果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, } }
main.go
package main import ( "2020-04-04/model" "fmt" ) func main() { var stu = model.newstudent("tom", 21) fmt.println(*stu) //&{...} fmt.println("name = ", stu.name, "score = ", stu.score) } //name = tom score = 21
思考题
如果model包的student 的结构体的字段score 改成score, 我们还能正常访问吗?又应该如何解决这个问题呢?
解决方法如下:
student.go
//定义一个结构体 type student struct { name string score float64 } //因为student结构体首字母是小写,因此只能在model使用 //通过工厂模式来解决 func newstudent(n string, s float64) *student { return &student{ name: n, score: s, } } //如果score字段首字母小写,则,在其它包不可以直接访问,可以提供一个方法 func (s *student) getscore() float64 { return s.score }
main.go
import ( "2020-04-04/model" "fmt" ) func main() { var stu = model.newstudent("tom", 22) fmt.println(*stu) //&{...} fmt.println("name = ", stu.name, "score = ", stu.getscore()) } //{tom 22} //name = tom score = 22
面向对象编程思想-抽象
定义一个结构体的时候,实际上就是把一类事物的共有的属性(字段)和行为(方法)提取出来,形成一个物理模型(结构) 。这种研究问题的方法称为抽象。
快速入门案例
//定义一个结构体account type account struct { accountno string pwd string balance float64 } //方法 //1.存款 func (account *account) deposite(money float64,pwd string){ //看下输入的密码是否正确 if pwd != account.pwd { fmt.println("你输入的密码不正确") return } //看看存款是否正确 if money <= 0 { fmt.println("你输入的金额不正确") return } account.balance += money fmt.println("存款成功!") } //取款 func (account *account) withdraw(money float64, pwd string) { //看一下输入的密码是否正确 if pwd != account.pwd { fmt.println("你输入的密码不正确") return } //看看存款金额是否正确 if money <= 0 || money > account.balance{ fmt.println("你输入的金额不正确") return } account.balance -= money fmt.println("取款成功~") } //查询余额 func (account *account) query(pwd string) { //看一下输入的密码是否正确 if pwd != account.pwd { fmt.println("你输入的密码不正确") return } fmt.printf("你的账号为=%v 余额=%v \n",account.accountno,account.balance) } func main() { var pwd string var balance float64 account := account{ accountno: "1111111", pwd: "666666", balance: 0.0, } fmt.println("请输入密码") fmt.scanln(&pwd) if pwd == account.pwd { fmt.println("请输入金额") fmt.scanln(&balance) account.query(pwd) account.deposite(balance,pwd) account.query(pwd) } } //请输入密码 //666666 //请输入金额 //2000 //你的账号为=1111111 余额=0 //存款成功! //你的账号为=1111111 余额=2000
对上面代码进行修饰:增加一个控制台的菜单,可以让用户动态的输入选项
package main import "fmt" //定义一个结构体account type account struct { accountno string pwd string balance float64 } //方法 //1.存款 func (account *account) deposite(money float64,pwd string){ //看下输入的密码是否正确 if pwd != account.pwd { fmt.println("你输入的密码不正确") return } //看看存款是否正确 if money <= 0 { fmt.println("你输入的金额不正确") return } account.balance += money fmt.println("存款成功!") } //取款 func (account *account) withdraw(money float64, pwd string) { //看一下输入的密码是否正确 if pwd != account.pwd { fmt.println("你输入的密码不正确") return } //看看存款金额是否正确 if money <= 0 || money > account.balance{ fmt.println("你输入的金额不正确") return } account.balance -= money fmt.println("取款成功~") } //查询余额 func (account *account) query(pwd string) { //看一下输入的密码是否正确 if pwd != account.pwd { fmt.println("你输入的密码不正确") return } fmt.printf("你的账号为=%v 余额=%v \n",account.accountno,account.balance) } func main() { var xuanxiang byte var pwd string var balance float64 account := account{ accountno: "1111111", pwd: "666666", balance: 0.0, } fmt.println("请输入密码") fmt.scanln(&pwd) if pwd == account.pwd { for { fmt.println("请输入菜单选项:") fmt.println("1. 存款") fmt.println("2. 取款") fmt.println("3. 余额") fmt.println("4. 退出") fmt.println("请输入菜单选项:") fmt.scanln(&xuanxiang) switch xuanxiang { case 1: fmt.println("请输入金额") fmt.scanln(&balance) account.query(pwd) account.deposite(balance,pwd) account.query(pwd) //fmt.println("请输入金额") //fmt.scanln(&balance) //account.deposite(balance,pwd) case 2: fmt.println("请输入金额") fmt.scanln(&balance) account.query(pwd) account.withdraw(balance,pwd) account.query(pwd) case 3: // account.query(pwd) default: return } } //fmt.println("请输入金额") //fmt.scanln(&balance) //account.query(pwd) //account.deposite(balance,pwd) //account.query(pwd) } } //请输入密码 //666666 //请输入金额 //2000 //你的账号为=1111111 余额=0 //存款成功! //你的账号为=1111111 余额=2000
面向对象编程三大特性-封装
go仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它oop语言不一样
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作
封装的好处
-
隐藏实现细节
-
可以对数据进行验证,保证安全合理(age)
如何体现封装
-
对结构体中的属性进行封装
-
通过方法,包实现封装
封装的实现步骤
-
将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似private)
-
给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
-
提供一个首字母大写的set方法(类似其它语言的public),用于对属性判断并赋值
func(var 结构体类型名) setxxx(参数列表) (返回值列表) { //加入数据验证的业务逻辑 var.字段 = 参数 }
- 提供一个首字母大写的get方法(类似其它语言的public),用于获取属性的值
func (var 结构体类型名) getxxx() { return var.age }
*特别说明*:在go开发中并没有特别强调封装,这点并不像java,所以不用总是用java的语法特性来看待go,go本身对面向对象的特性做了简化的
快速入门案例
编写一个程序(person.go),不能随便查看人的年龄、工资等隐私,并对输入的年龄进行合理的验证。
设计:model包(person.go),main包(main.go调用person结构体)
person.go
type person struct { name string age int //其它包不能直接访问 sal float64 } //写一个工厂模式的函数,相当于构造函数 func newperson(name string) *person { return &person{ name: name, } } //为了访问age和sal 编写一对setxxx的方法和getxxx的方法 func (p *person) setage(age int) { if age > 0 && age < 150 { p.age = age } else { fmt.println("年龄范围不正确..") //给程序员一个默认值 } } func (p *person) getage() int { return p.age } func (p *person) setsal(sal float64) { if sal >= 3000 && sal <= 30000 { p.sal = sal } else { fmt.println("薪水范围不正确...") } } func (p *person) getsal() float64 { return p.sal }
main.go
func main() { p := model.newperson("smith") p.setage(18) p.setsal(5000) fmt.println(p) fmt.println(p.name, "age =", p.getage(), "sal =", p.getsal()) } //输出:&{smith 18 5000} //smith age = 18 sal = 5000
要求
-
创建程序,在model包中定义account结构体:在main函数中体现go的封装性
-
account结构体要求具有字段:账号(长度在6-10之间)、余额(必须>20)、密码(必须是6位数)
-
通过setxxx的方法给account的字段赋值
-
在main函数中测试
account.go
//定义一个结构体account type account struct { accountno string pwd string balance float64 } //工厂模式的函数-构造函数 func newaccount(accountno string, pwd string, balance float64) *account { if len(accountno) < 6 || len(accountno) > 10 { fmt.println("账号的长度不对") return nil } if len(pwd) != 6 { fmt.println("密码的长度不对...") return nil } if balance < 20 { fmt.println("余额数目不对...") return nil } return &account{ accountno: accountno, pwd: pwd, balance: balance, } } //方法 //存款 func (account *account) deposite(money float64, pwd string) { //看下输入的密码是否正确 if pwd != account.pwd { fmt.println("你输入的密码不正确") return } //看看存款金额是否正确 if money <= 0 { fmt.println("你输入的金额不正确") return } account.balance += money fmt.println("存款成功~") } //取款 func (account *account) withdraw(money float64, pwd string) { //看下输入的密码是否正确 if pwd != account.pwd { fmt.println("你输入的密码不正确") return } //看看存款金额是否正确 if money <= 0 || money > account.balance { fmt.println("你输入的金额不正确") return } account.balance -= money fmt.println("取款成功~") } //查询余额 func (account *account) query(pwd string) { //看下输入的密码是否正确 if pwd != account.pwd { fmt.println("你输入的密码不正确") return } fmt.printf("你的账号为 = %v 余额 = %v \n", account.accountno, account.balance) }
main.go
func main() { //创建一个account变量 account := model.newaccount("zisefeizhu","000",40) if account != nil { fmt.println("创建成功 = ", account) } else { fmt.println("创建失败") } } //输出:密码的长度不对... //创建失败
增加如下功能:通过setxxx的方法给account的字段赋值通过getxxx方法获取字段的值
account.go
package model import "fmt" //定义一个结构体account type account struct { accountno string pwd string balance float64 } //工厂模式的函数-构造函数 func newaccount(accountno string, pwd string, balance float64) *account { return &account{ accountno: accountno, pwd: pwd, balance: balance, } } func (accounter *account) setaccountno(accountno string) { if len(accountno) < 6 || len(accountno) > 10 { fmt.println("账号的长度不对") } } func (accounter *account) getaccountno() string { return accounter.accountno } //方法 //存款 func (account *account) deposite(money float64, pwd string) { //看下输入的密码是否正确 if pwd != account.pwd { fmt.println("你输入的密码不正确") return } //看看存款金额是否正确 if money <= 0 { fmt.println("你输入的金额不正确") return } account.balance += money fmt.println("存款成功~") } //取款 func (account *account) withdraw(money float64, pwd string) { //看下输入的密码是否正确 if pwd != account.pwd { fmt.println("你输入的密码不正确") return } //看看存款金额是否正确 if money <= 0 || money > account.balance { fmt.println("你输入的金额不正确") return } account.balance -= money fmt.println("取款成功~") } //查询余额 func (account *account) query(pwd string) { //看下输入的密码是否正确 if pwd != account.pwd { fmt.println("你输入的密码不正确") return } fmt.printf("你的账号为 = %v 余额 = %v \n", account.accountno, account.balance) }
main.go
package main import ( "2020-04-04/model" "fmt" ) func main() { //创建一个account变量 account := model.newaccount("zisefeizhu","000",40) fmt.println(account) fmt.println(account.getaccountno()) } //&{zisefeizhu 000 40} //zisefeizhu
类似改法
面向对象编程三大特性-继承
看一个问题,引出继承的必要性
看一个学生考试系统的程序extend01.go,提出代码复用的问题
//编写一个学生考试系统 //小学生 type pupil struct { name string age int score int } //显示他的成绩 func (p *pupil) showinfo() { fmt.printf("学生名 = %v 年龄 = %v 成绩 = %v\n", p.name, p.age, p.score) } func (p *pupil) setscore(score int) { //业务判断 p.score = score } func (p *pupil) testing() { fmt.println("小学生正在考试中...") } //大学生,研究生... //大学生 type graduate struct { name string age int score int } //显示他的成绩 func (p *graduate) showinfo() { fmt.printf("学生名 = %v 年龄 = %v 成绩 = %v\n", p.name, p.age, p.score) } func (p *graduate) setscore(score int) { //业务判断 p.score = score } func (p *graduate) testing() { fmt.println("大学生正在考试中...") } //代码冗余... 研究生 //代码冗余... 高中生 func main() { //测试 var pupil = &pupil{ name: "tom", age: 10, } pupil.testing() pupil.setscore(90) pupil.showinfo() //测试 var graduate = &graduate{ name: "tom", age: 20, } graduate.testing() graduate.setscore(90) graduate.showinfo() } //输出:小学生正在考试中... //学生名 = tom 年龄 = 10 成绩 = 90 //大学生正在考试中... //学生名 = tom 年龄 = 20 成绩 = 90 对上面代码的小结 1)pupil和graduate两个结构体的字段和方法几乎一样,但是我们却写了相同的代码,代码复用性不强 2)出现代码冗余,而且代码不利于维护,同时也不利于功能的扩展 3)解决方法 - 通过继承方式来解决
继承基本介绍和示意图
继承可以解决代码复用,让编程更加靠近人类思维
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体(比如刚才的student),在该结构体中定义这些相同的属性和方法。
其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个student匿名结构体即可
在go中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性
嵌套匿名结构体的基本语法
type goods struct { name string price int } type book struct { goods //这里就是嵌套匿名结构体goods writer string }
快速入门案例
对extends01.go改进,使用嵌套匿名结构体的方式来实现继承特性,体会继承的好处
//编写一个学生考试系统 //小学生 type student struct { name string age int score int } //将pupil 和 graduate 共有的方法也绑定到 *student func (stu *student) showinfo() { fmt.printf("学生名 = %v 年龄 = %v 成绩 = %v\n", stu.name, stu.age, stu.score) } func (stu *student) setscore(score int) { //业务判断 stu.score = score } //小学生 type pupil struct { student //嵌入了student匿名结构体 } //显示他的成绩 //这时pupil结构体特有的方法,保留 func (p *pupil) testing() { fmt.println("小学生正在考试中...") } //大学生,研究生... //大学生 type graduate struct { student //嵌入了student匿名结构体 } //显示他的成绩 //这时graduate结构体特有的方法,保留 func (p *graduate) testing() { fmt.println("大学生正在考试中...") } //代码冗余... 研究生 //代码冗余... 高中生 func main() { //测试 //当我们对结构体嵌入了匿名结构体使用方法会发生变化 pupil := &pupil{} pupil.student.name = "tom" pupil.student.age = 8 pupil.testing() pupil.setscore(70) pupil.showinfo() //测试 graduate := &graduate{} graduate.student.name = "marry" graduate.student.age = 28 graduate.testing() graduate.setscore(90) graduate.showinfo() } //输出:小学生正在考试中... //学生名 = tom 年龄 = 8 成绩 = 70 //大学生正在考试中... //学生名 = marry 年龄 = 28 成绩 = 90
继承给编程带来的便利
-
代码的复用性提高了
-
代码的扩展性和维护性提高了
继承的深入讨论
1)结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用
type a struct { name string age int } func (a *a) sayok() { fmt.println("a sayok",a.name) } func (a *a) hello() { fmt.println("a hello",a.name) } type b struct { a } func main() { var b b b.a.name = "zisefeizhu" b.a.age = 19 b.a.sayok() b.a.hello() } //输出:a sayok zisefeizhu //a hello zisefeizhu
-
匿名结构体字段访问可以简化
对上面的代码小结
(1)当我们直接通过b访问字段或方法时,其执行流程如下比如b.name
(2)编译器会先看b对应的类型有没有name,如果有,则直接调用b类型的name字段
(3)如果没有就去看b中嵌入的匿名结构体a有没有声明name字段,如果有就调用,如果没有继续查找...如果都找不到就报错
3) 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分 -
结构体嵌入两个(或多个)匿名结构体,如果两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明指定匿名结构体名字,否则编译报错
-
如果一个struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
6 ) 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
type goods struct { name string price float64 } type brand struct { name string address string } type tv struct { goods brand } type tv2 struct { *goods *brand } func main() { tv := tv{ goods{"电视机001", 5000.99},brand{"海尔","山东"},} tv2 := tv{ goods{ price: 5000.99, name: "电视机002", }, brand{ name: "夏普", address: "北京", }, } fmt.println("tv", tv) fmt.println("tv2", tv2) tv3 := tv2{ &goods{"电视机003", 7000.99},&brand{"创维","河南"},} tv4 := tv2{ &goods{ name: "电视机004", price: 9000.99, }, &brand{ name: "长虹", address: "四川", }, } fmt.println("tv3", *tv3.goods, *tv3.brand) fmt.println("tv4", *tv4.goods, *tv4.brand) } //tv {{电视机001 5000.99} {海尔 山东}} //tv2 {{电视机002 5000.99} {夏普 北京}} //tv3 {电视机003 7000.99} {创维 河南} //tv4 {电视机004 9000.99} {长虹 四川}
课堂练习
结构体的匿名字段是基本数据类型,如何访问? type monster struct { name string age int } type e struct { monster int n int } func main() { //演示一下匿名字段时基本数据类型的使用 var e e e.name = "狐狸精" e.age = 300 e.int = 20 e.n = 40 fmt.println("e = ", e) } //输出:e = {{狐狸精 300} 20 40} 说明 1)如果一个结构体有int类型的匿名字段,就不能有第二个 2)如果需要有多个int的字段,则必须给int字段指定名字
面向对象编程-多重继承
多重继承说明
如一个struct嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。
案例演示
通过一个案例来说明多重继承使用
多重继承细节
- 若嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分
2)为了保证代码的简洁性,建议尽量不使用多重继承
接口
go中多态特性主要是通过接口来体现的
usb插槽就是现实中的接口你可以把手机,相机,u盘都插在usb插槽上,而不用担心那个插槽是专门插哪个的,原因是做usb插槽的厂家和做各种设备的厂家都遵守了统一的规定包括尺寸,排线等等。
这样的设计需求在go编程中也是会大量存在的,一个程序就是一个世界,在现实世界存在的情况,在程序中也会出现。用程序来模拟一个前面的应用场景
//声明/定义一个接口 type usb interface { //声明了两个没有实现的方法 start() stop() } type phone struct { } //让phone实现usb接口的方法 func (p phone) start() { fmt.println("手机开始工作...") } func (p phone) stop() { fmt.println("手机停止工作...") } type camera struct { } //让camera实现usb接口的方法 func (c camera) start() { fmt.println("相机开始工作...") } func (c camera) stop() { fmt.println("相机停止工作...") } //计算机 type computer struct { } //编写一个方法working方法,接收一个usb接口类型变量 //只要是实现了usb接口(所谓实现usb接口,就是指实现了usb接口声明所有方法) func (c computer) working(usb usb) { //usb变量会根据传入的实参,来判断到底是phone,还是camera //通过usb接口变量来调用start和stop方法 usb.start() usb.stop() } func main() { //测试 //先创建结构体变量 computer := computer{} phone := phone{} camera := camera{} //关键点 computer.working(phone) computer.working(camera) }
interface类型可以定义一组方法,但是这些不需要实现。并且interface不能包含任何变量。到某个自定义类型(比如结构体phone)要使用的时候,在根据具体情况把这些方法写出来(实现)
基本语法
小结说明:
-
接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想
-
go中的接口,不需要显示的实现。只要一个变量,含有接口类型中的所有方法,那么变量就实现了这个接口,因此,go中没有implement这样的关键字
接口使用的应用场景
-
中国要制造的轰炸机,专家只需要把飞机需要的功能/规格定下来即可,然后让别的人具体实现即可
-
现在有一个项目经理,管理三个程序员,开发一个软件,为了控制和管理软件,项目经理可以定义一些接口,然后由程序员具体实现
......
注意事项和细节
接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)
type ainterface interface { say() } type stu struct { name string } func (stu stu) say() { fmt.println("stu say()") } func main() { var stu stu //结构体变量,实现了say() 实现了ainterface var a ainterface = stu a.say() }
接口中所有的方法都没有方法体,即都是没有实现的方法
在go中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口
一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型
type ainterface interface { say() } type stu struct { name string } type integer int func (i integer) say() { fmt.println("integer say i =", i) } func (stu stu) say() { fmt.println("stu say()") } func main() { var i integer = 10 var b ainterface = i b.say() var stu stu //结构体变量,实现了say() 实现了ainterface var a ainterface = stu a.say() } //输出:integer say i = 10 //stu say()
一个自定义类型可以实现多个接口
type ainterface interface { say() } type binterface interface { hello() } type monster struct { } func (m monster) hello() { fmt.println("monster hello()") } func (m monster) say() { fmt.println("monster say ") } func main() { //monster实现了ainterface 和binterface var monster monster var a2 ainterface = monster var b2 binterface = monster a2.say() b2.hello() } //输出:monster say //monster hello()
go接口中不能有任何变量
一个接口(比如a接口)可以继承多个别的接口(比如b,c接口),这时如果要实现a接口,也必须将b,c接口的方法也全部实现
package main type binterface interface { test01() } type cinterface interface { test02() } type ainterface interface { binterface cinterface test03() } //如果需要实现ainterface,就需要将binterface cinterface的方法都实现 type stu struct { } func (stu stu) test01() { } func (stu stu) test02() { } func (stu stu) test03() { } func main() { var stu stu var a ainterface = stu a.test01() }
interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil
空接口interface{}没有任何方法,所以所有类型都实现了空接口,即可以把任何一个变量赋给空接口
type binterface interface { test01() } type cinterface interface { test02() } type ainterface interface { binterface cinterface test03() } //如果需要实现ainterface,就需要将binterface cinterface的方法都实现 type stu struct { } func (stu stu) test01() { } func (stu stu) test02() { } func (stu stu) test03() { } type t interface { //空接口 } func main() { var stu stu var t t = stu //ok fmt.println(t) var t2 interface{} = stu var num1 float64 = 8.8 t2 = num1 t = num1 fmt.println(t2, t) var a ainterface = stu a.test01() } //输出:{} //8.8 8.8
接口编程最佳实践
import ( "fmt" "math/rand" "sort" ) //1.声明hero结构体 type hero struct { name string age int } //2.声明一个hero结构体切片类型 type heroslice []hero //3.实现interface接口 func (hs heroslice) len() int { return len(hs) } //less方法就是决定使用什么标准进行排序 //1.按hero的年龄从小到大排序 func (hs heroslice) less(i, j int) bool { return hs[i].age < hs[j].age //修改成对name排序 //return hs[i].name < hs[j].name } func (hs heroslice) swap (i,j int) { //交换 hs[i], hs[j] = hs[j], hs[i] } //1.声明student结构体 type student struct { name string age int score float64 } //将student的切片,按score从大到小排序! func main() { //先定义一个数组/切片 var intslice = []int{0, -1, 10, 7, 90} //要求对intslice切片进行排序 //1. 冒泡排序... //2. 也可以使用系统提供的方法 sort.ints(intslice) fmt.println(intslice) //对结构体切片进行排序 //1. 冒泡排序... //2. 也可以使用系统提供的方法 //测试看看我们是否可以对结构体切片进行排序 var heroes heroslice for i := 0; i < 10; i++ { hero := hero{ name: fmt.sprintf("英雄 %d",rand.intn(100 )), age: rand.intn(100), } //将 hero append 到heroes切片 heroes = append(heroes,hero) } //看看排序前的顺序 for _,v := range heroes { fmt.println(v) } //调用sort.sort sort.sort(heroes) fmt.println("______________排序后______________") //看看排序后的顺序 for _,v := range heroes { fmt.println(v) } i := 10 j := 20 i,j = j,i fmt.println("i = ", i, "j = ", j) } //[-1 0 7 10 90] //{英雄 81 87} //{英雄 47 59} //{英雄 81 18} //{英雄 25 40} //{英雄 56 0} //{英雄 94 11} //{英雄 62 89} //{英雄 28 74} //{英雄 11 45} //{英雄 37 6} //______________排序后______________ //{英雄 56 0} //{英雄 37 6} //{英雄 94 11} //{英雄 81 18} //{英雄 25 40} //{英雄 11 45} //{英雄 47 59} //{英雄 28 74} //{英雄 81 87} //{英雄 62 89} //i = 20 j = 10
接口练习题
实现接口 vs 继承
package main import "fmt" //monkey结构体 type monkey struct { name string } //声明接口 type birdable interface { flying() } type fishable interface { swimming() } func (this *monkey) climbing() { fmt.println(this.name,"生来会爬树..") } //littlemonkey 结构体 type littlemonkey struct { monkey //继承 } //让littlemonkey 实现birdable func (this *littlemonkey) flying() { fmt.println(this.name,"通过学习,会飞翔...") } //让littlemonkey 实现fishable func (this *littlemonkey) swimming() { fmt.println(this.name,"通过学习,会游泳...") } func main() { //创建一个littlemonkey实例 monkey := littlemonkey{ monkey{ name: "悟空", }, } monkey.climbing() monkey.flying() monkey.swimming() } //输出:悟空 生来会爬树.. //悟空 通过学习,会飞翔... //悟空 通过学习,会游泳... 对上面代码的小结 当a结构体继承了b结构体,那么a结构就自动的继承了b结构体的字段和方法,并且可以直接使用 当a结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此可以认为:实现接口是对继承机制的补充
实现接口可以看作是对继承的一种补充
接口和继承解决的问题不同
继承的价值主要在于:解决代码的*复用性*和*可维护性*
接口的价值主要在于:*设计*,设计好各种规范(方法),让其它自定义类型去实现这些方法
接口比继承更加灵活 person student birdable littlemonkey
接口比继承更加灵活,继承是满足is - a的关系,而接口只需满足like - a的关系
接口在一定程度上实现*代码*解耦
面向对象编程三大特性 - 多态
变量(实例)具有多种形态。面向对象的第三大特性,在go语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的变量实现。这时接口变量就呈现不同的形态
快速入门案例
在前面的usb接口案例中,usb usb 即可以接收手机变量,又可以接收相机变量,就体现了usb接口多态特性。
//声明/定义一个接口 type usb interface { //声明了两个没有实现的方法 start() stop() } type phone struct { } //让phone实现usb接口的方法 func (p phone) start() { fmt.println("手机开始工作...") } func (p phone) stop() { fmt.println("手机停止工作...") } type camera struct { } //让camera实现usb接口的方法 func (c camera) start() { fmt.println("相机开始工作...") } func (c camera) stop() { fmt.println("相机停止工作...") } //计算机 type computer struct { } //编写一个方法working方法,接收一个usb接口类型变量 //只要是实现了usb接口(所谓实现usb接口,就是指实现了usb接口声明所有方法) func (c computer) working(usb usb) { //usb变量会根据传入的实参,来判断到底是phone,还是camera //usb接口变量就体现出多态的特点 //通过usb接口变量来调用start和stop方法 usb.start() usb.stop() } func main() { //测试 //先创建结构体变量 computer := computer{} phone := phone{} camera := camera{} //关键点 computer.working(phone) computer.working(camera) }
接口体现多态的两种形式
多态参数
在前面的usb接口案例中,usb usb 即可以接收手机变量,又可以接收相机变量,就体现了usb接口多态特性。
多态数组
演示一个案例:在usb数组中,存放phone结构体和camera结构体变量
//声明/定义一个接口 type usb interface { //声明了两个没有实现的方法 start() stop() } type phone struct { name string } //让phone实现usb接口的方法 func (p phone) start() { fmt.println("手机开始工作...") } func (p phone) stop() { fmt.println("手机停止工作...") } type camera struct { name string } //让camera实现usb接口的方法 func (c camera) start() { fmt.println("相机开始工作...") } func (c camera) stop() { fmt.println("相机停止工作...") } //计算机 type computer struct { } func main() { //定义一个usb接口数组,可以存放phone和camera的结构体变量 //这里体现出多态数组 var usbarr [3]usb usbarr[0] = phone{"vivo"} usbarr[1] = phone{"华为"} usbarr[2] = phone{"小米"} fmt.println(usbarr) }
类型断言
类型断言:由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言
案例演示
func main() { //类型断言的其它案例 var x interface{} var b2 float32 = 2.2 x = b2 //空接口,可以接收任意类型 //x => float32 [使用类型那个断言] y := x.(float32) //转成具体类型 fmt.printf("y 的类型是 %t 值是 = %v", y, y) } //输出:y 的类型是 float32 值是 = 2.2 对上面代码的说明: 在进行类型断言时,如果类型不匹配,就会报panic,因此进行类型断言时, 要确保原来的空接口指向的就是断言的类型 如何在进行断言时,带上检测机制,如果成功就ok,否则也不要报panic func main() { //类型断言的其它案例 var x interface{} var b2 float32 = 3.3 x = b2 //空接口,可以接收任意类型 //x => float32 [使用类型那个断言] if y, ok := x.(float32); ok { fmt.println("convert success") fmt.printf("y 的类型是%t 值是%v", y, y) } else { fmt.println("convert fail") } fmt.println("继续执行...") //y := x.(float32) //转成具体类型 //fmt.printf("y 的类型是 %t 值是 = %v", y, y) } //输出:y 的类型是float32 值是3.3继续执行...
类型断言的最佳实践
在前面的usb接口案例做改进:
给phone结构体增加一个特有的方法call(),当usb接口接收的是phone变量时,还需要调用call方法
//声明/定义一个接口 type usb interface { //声明了两个没有实现的方法 start() stop() } type phone struct { name string } //让phone实现usb接口的方法 func (p phone) start() { fmt.println("手机开始工作...") } func (p phone) stop() { fmt.println("手机停止工作...") } func (p phone) call() { fmt.println("手机 在打电话...") } type camera struct { name string } //让camera实现usb接口的方法 func (c camera) start() { fmt.println("相机开始工作...") } func (c camera) stop() { fmt.println("相机停止工作...") } //计算机 type computer struct { } //编写一个方法working方法,接收一个usb接口类型变量 //只要是实现了usb接口(所谓实现usb接口,就是指实现了usb接口声明所有方法) func (c computer) working(usb usb) { //usb变量会根据传入的实参,来判断到底是phone,还是camera //usb接口变量就体现出多态的特点 //通过usb接口变量来调用start和stop方法 usb.start() //如果usb是指向phone结构体变量,则还需要调用call方法 //类型断言... if phone, ok := usb.(phone); ok { phone.call() } usb.stop() } func main() { //定义一个usb接口数组,可以存放phone和camera的结构体变量 //这里体现出多态数组 var usbarr [3]usb usbarr[0] = phone{"vivo"} usbarr[1] = phone{"华为"} usbarr[2] = phone{"小米"} fmt.println(usbarr) //遍历usbarr //phone还有一个特有的方法call(),请遍历usb数组,如果是phone变量 //除了调用usb接口声明的方法外,还需要调用phone特有方法call => 类型断言 var computer computer for _, v := range usbarr { computer.working(v) fmt.println() } //fmt.print(usbarr) } //[{vivo} {华为} {小米}] //手机开始工作... //手机 在打电话... //手机停止工作... // //手机开始工作... //手机 在打电话... //手机停止工作... // //手机开始工作... //手机 在打电话... //手机停止工作...
写一函数,循环判断传入参数的类型:
/编写一个函数,可以判断输入的参数是什么类型 func typejudge(items... interface{}) { for index, x := range items { switch x.(type) { case bool: fmt.printf("第%v个参数是 bool 类型,值是%v\n", index, x) case float32: fmt.printf("第%v个参数是 float32 类型,值是%v\n", index, x) case float64: fmt.printf("第%v个参数是 float64 类型,值是%v\n", index, x) case int, int32, int64: fmt.printf("第%v个参数是 整数 类型,值是%v\n", index, x) case string: fmt.printf("第%v个参数是 string 类型,值是%v\n", index, x) default: fmt.printf("第%v个参数是 类型,值是%v\n", index, x) } } } func main() { var n1 float32 = 1.1 var n2 float64 = 2.3 var n3 int32 = 30 var name string = "tom" address := "北京" n4 := 300 typejudge(n1, n2, n3, name, address, n4) } //第0个参数是 float32 类型,值是1.1 //第1个参数是 float64 类型,值是2.3 //第2个参数是 整数 类型,值是30 //第3个参数是 string 类型,值是tom //第4个参数是 string 类型,值是北京 //第5个参数是 整数 类型,值是300
在前面代码的基础上,增加判断student类型和*student类型
package main import ( "fmt" ) type student struct { name string } //编写一个函数,可以判断输入的参数是什么类型 func typejudge(items... interface{}) { for index, x := range items { switch x.(type) { case student: fmt.printf("第%v个参数是 student 类型,值是%v\n", index, x) case *student: fmt.printf("第%v个参数是 *student 类型,值是%v\n", index, x) 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, int32, int64: fmt.printf("第%v个参数是 整数 类型,值是%v\n", index, x) case string: fmt.printf("第%v个参数是 string 类型,值是%v\n", index, x) default: fmt.printf("第%v个参数是 类型,值是%v\n", index, x) } } } func main() { var n1 float32 = 1.1 var n2 float64 = 2.3 var n3 int32 = 30 var name string = "tom" address := "北京" n4 := 300 var b bool = false stu1 := student{"zise"} stu2 := &student{"feizhzu"} typejudge(n1, n2, n3, name, address, n4, b, stu1, stu2) } //第0个参数是 float32 类型,值是1.1 //第1个参数是 float64 类型,值是2.3 //第2个参数是 整数 类型,值是30 //第3个参数是 string 类型,值是tom //第4个参数是 string 类型,值是北京 //第5个参数是 整数 类型,值是300 //第6个参数是 bool 类型,值是false //第7个参数是 student 类型,值是{zise} //第8个参数是 *student 类型,值是&{feizhzu}