十、面向对象编程
10.1 go面向对象编程说明
1 ) Golang也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说Golang支持面向对象编程特性是比较准确的。
2 ) Golang没有类(class),Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解Golang是基于struct来实现OOP特性的。
3 ) Golang面向对象编程非常简洁,去掉了传统OOP语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等等
4 ) Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不一样,比如继承 :Golang没有extends 关键字,继承是通过匿名字段来实现。
5 ) Golang面向对象(OOP)很优雅,OOP本身就是语言类型系统(typesystem)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。也就是说在Golang中面向接口编程是非常重要的特性。
代码演示
package main
import (
"fmt"
)
//定义一个Cat结构体,将Cat的各个字段/属性信息,放入到Cat结构体进行管理
type Cat struct {
Name string
Age int
Color string
Hobby string
Scores [3]int // 字段是数组...
}
func main() {
// 张老太养了20只猫猫:一只名字叫小白,今年3岁,白色。还有一只叫小花,
// 今年100岁,花色。请编写一个程序,当用户输入小猫的名字时,就显示该猫的名字,
// 年龄,颜色。如果用户输入的小猫名错误,则显示 张老太没有这只猫猫。
// 使用struct来完成案例
// 创建一个Cat的变量
var cat1 Cat // var a int
fmt.Printf("cat1的地址=%p\n", &cat1)
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)
}
通过上面的案例和讲解我们可以看出:
1 ) 结构体是自定义的数据类型,代表一类事物.
2 ) 结构体变量(实例)是具体的,实际的,代表一个具体变量
10.2 结构体在内存里的布局
- 基本语法
type 结构体名称 struct {
field 1 type
field 2 type
}
- 举例:
type Student struct{
Namestring//字段
Ageint//字段
Scorefloat 32
}
不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体是值类型。
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 = "青牛精"
fmt.Println("monster1=", monster1) //monster1= {牛魔王 500}
fmt.Println("monster2=", monster2) //monster2= {青牛精 500}
}
画出上面代码的内存示意图:
10.3 创建结构体变量和访问字段
-
方式 1 - 直接声明
案例演示:var person Person
前面我们已经说了。
-
方式 2 - {}
案例演示: var person Person = Person{}
package main
import (
"fmt"
)
type Person struct{
Name string
Age int
}
func main() {
//方式2
p2 := Person{"mary", 20}
// p2.Name = "tom"
// p2.Age = 18
fmt.Println(p2)
}
- 方式 3 - &
package main
import (
"fmt"
)
type Person struct{
Name string
Age int
}
func main() {
//方式3-&
//案例: var person *Person = new (Person)
var p3 *Person= new(Person)
//因为p3是一个指针,因此标准的给字段赋值方式
//(*p3).Name = "smith" 也可以这样写 p3.Name = "smith"
//原因: go的设计者 为了程序员使用方便,底层会对 p3.Name = "smith" 进行处理
//会给 p3 加上 取值运算 (*p3).Name = "smith"
(*p3).Name = "smith"
p3.Name = "john" //
(*p3).Age = 30
p3.Age = 100
fmt.Println(*p3)
}
- 方式 4 - {}
package main
import (
"fmt"
)
type Person struct{
Name string
Age int
}
func main() {
//方式4-{}
//案例: var person *Person = &Person{}
//下面的语句,也可以直接给字符赋值
//var person *Person = &Person{"mary", 60}
var person *Person = &Person{}
//因为person 是一个指针,因此标准的访问字段的方法
// (*person).Name = "scott"
// go的设计者为了程序员使用方便,也可以 person.Name = "scott"
// 原因和上面一样,底层会对 person.Name = "scott" 进行处理, 会加上 (*person)
(*person).Name = "scott"
person.Name = "scott~~"
(*person).Age = 88
person.Age = 10
fmt.Println(*person)
}
- 说明:
1 ) 第 3 种和第 4 种方式返回的是 结构体指针。
2 ) 结构体指针访问字段的标准方式应该是:(* 结构体指针 ) . 字段名 ,比如
(*person).Name="tom"
3 ) 但go做了一个简化,也支持 结构体指针. 字段名, 比如 person.Name=“tom”。更加符合程序员使用的习惯, go 编译器底层 对 person.Name 做了转化 (*person).Name
注意
10.3 结构体细节
1 ) 结构体的所有字段在内存中是连续的
package main
import "fmt"
//结构体
type Point struct {
x int
y int
}
//结构体
type Rect struct {
leftUp, rightDown Point
}
//结构体
type Rect2 struct {
leftUp, rightDown *Point
}
func main() {
r1 := Rect{Point{1,2}, Point{3,4}}
//r1有四个int, 在内存中是连续分布
//打印地址
fmt.Printf("r1.leftUp.x 地址=%p r1.leftUp.y 地址=%p r1.rightDown.x 地址=%p r1.rightDown.y 地址=%p \n",
&r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y)
//r2有两个 *Point类型,这个两个*Point类型的本身地址也是连续的,
//但是他们指向的地址不一定是连续
r2 := Rect2{&Point{10,20}, &Point{30,40}}
//打印地址
fmt.Printf("r2.leftUp 本身地址=%p r2.rightDown 本身地址=%p \n",
&r2.leftUp, &r2.rightDown)
//他们指向的地址不一定是连续..., 这个要看系统在运行时是如何分配
fmt.Printf("r2.leftUp 指向地址=%p r2.rightDown 指向地址=%p \n",
r2.leftUp, r2.rightDown)
}
对应的分析图:
2 ) 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
package main
import "fmt"
import "encoding/json"
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)
}
3 ) 结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转
4 ) struct的每个字段上,可以写上一个 tag , 该tag可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
- 序列化的使用场景:
- 举例:
package main
import "fmt"
import "encoding/json"
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))
}
10.4 方法
在某些情况下,我们要需要声明(定义)方法。比如Person结构体:除了有一些字段外( 年龄,姓名…),Person结构体还有一些行为比如:可以说话、跑步…,通过学习,还可以做算术题。这时就要用方法才能完成。
Golang中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct
10.4.1 方法的声明与调用
func(recevier type)methodName(参数列表) (返回值列表){
方法体
return 返回值
}
1 ) 参数列表:表示方法输入
2 ) receviertype: 表示这个方法和type这个类型进行绑定,或者说该方法作用于type类型
3 ) receivertype:type可以是结构体,也可以其它的自定义类型
4 ) receiver: 就是type类型的一个变量(实例),比如 :Person结构体 的一个变量(实例)
5 ) 返回值列表:表示返回的值,可以多个
6 ) 方法主体:表示为了实现某一功能代码块
7 ) return 语句不是必须的。
type A struct{
Numint
}
func(a A)test(){
fmt.Println(a.Num)
}
- 对上面的语法的说明
1 ) func( a A )test() {} 表示 A结构体有一方法,方法名为 test
2 ) (a A) 体现 test方法是和A类型绑定的
package main
import (
"fmt"
)
type Person struct{
Name string
}
//给Person类型绑定一方法
func (p Person) test() {
fmt.Println("test() name=",p.Name)
func main() {
var p Person
p.Name = "tom"
p.test() //调用方法
}
1 ) test方法和Person类型绑定
2 ) test方法只能通过 Person类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
3 ) func(p Person)test(){}…p 表示哪个Person变量调用,这个p就是它的副本, 这点和函数传参非常相似。
4 ) p 这个名字,有程序员指定,不是固定, 比如修改成person也是可以
10.4.2 方法的调用与传参机制原理*
方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法。下面我们举例说明。
1 ) 在通过一个结构体实例变量去调用方法时,其调用机制和函数一样
2 ) 不一样的地方时,变量调用方法时,该结构体实例变量变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝)
10.4.3 方法使用细节
1 ) 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
2 ) 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
package main
import (
"fmt"
)
type Circle struct {
radius float64
}
//为了提高效率,通常我们方法和结构体的指针类型绑定
func (c *Circle) area2() float64 {
//因为 c是指针,因此我们标准的访问其字段的方式是 (*c).radius
//return 3.14 * (*c).radius * (*c).radius
// (*c).radius 等价 c.radius
fmt.Printf("c 是 *Circle 指向的地址=%p", c)
c.radius = 10
return 3.14 * c.radius * c.radius
}
func main() {
//创建一个Circle 变量
var c Circle
fmt.Printf("main c 结构体变量地址 =%p\n", &c)
c.radius = 7.0
//res2 := (&c).area2()
//编译器底层做了优化 (&c).area2() 等价 c.area()
//因为编译器会自动的给加上 &c
res2 := c.area2()
fmt.Println("面积=", res2)
fmt.Println("c.radius = ", c.radius) //10
}
3 ) Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct, 比如int,float 32 等都可以有方法
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.change()
fmt.Println("i=", i)
}
4 ) 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。[讲解]
5 ) 如果一个类型实现了String()这个方法,那么fmt.Println默认会调用这个变量String()进行输出
package main
import (
"fmt"
)
type Student struct {
Name string
Age int
}
//给*Student实现方法String()
func (stu *Student) String() string {
str := fmt.Sprintf("Name=[%v] Age=[%v]", stu.Name, stu.Age)
return str
}
func main() {
//定义一个Student变量
stu := Student{
Name : "tom",
Age : 20,
}
//如果你实现了 *Student 类型的 String方法,就会自动调用
fmt.Println(&stu)
}
10.4.4 方法与函数的区别
1 ) 调用方式不一样
- 函数的调用方式: 函数名(实参列表)
- 方法的调用方式: 变量.方法名(实参列表)
2 ) 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
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)
}
3 ) 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
package main
import (
"fmt"
)
type Person struct {
Name string
}
//对于方法(如struct的方法),
//接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
func (p Person) test03() {
p.Name = "jack"
fmt.Println("test03() =", p.Name) // jack
}
func (p *Person) test04() {
p.Name = "mary"
fmt.Println("test03() =", p.Name) // mary
}
func main() {
p := Person{"tom"}
p.test03()
fmt.Println("main() p.name=", p.Name) // tom
(&p).test03() // 从形式上是传入地址,但是本质仍然是值拷贝
fmt.Println("main() p.name=", p.Name) // tom
(&p).test04()
fmt.Println("main() p.name=", p.Name) // mary
p.test04() // 等价 (&p).test04 , 从形式上是传入值类型,但是本质仍然是地址拷贝
}
1 ) 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
2 ) 如果是和值类型,比如 ( pPerson ), 则是值拷贝, 如果和指针类型,比如是 ( p*Person ) 则是地址拷贝。
10.5 工厂模式
Golang的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。
使用工厂模式实现跨包创建结构体实例(变量)的案例:
如果model 包的 结构体变量首字母大写,引入后,直接使用 , 没有问题
如果model 包的 结构体变量首字母小写,引入后,不能直接使用, 可以工厂模式解决(公共构造方法)
student.go
package model
//定义一个结构体
type student struct{
Name string
Score float64
}
//因为student结构体首字母是小写,因此是只能在model使用
//我们通过工厂模式来解决
func NewStudent(n string, s float64) *student { // 编写一个函数可以返回构造的结构体
return &student{ // 因为函数首字母是大写,公开函数所以可以在其他包里用本程序的私有类
Name : n, // 前提是私有类里变量首字母都大写
Score : s,
}
}
main.go
package main
import (
"fmt"
"go_code/chapter10/factory/model"
)
func main() {
//定student结构体是首字母小写,我们可以通过工厂模式来解决
var stu = model.NewStudent("tom~", 98.8)
fmt.Println(*stu) //&{....}
fmt.Println("name=", stu.Name, " score=", stu.Score)
}
如果model包的student 的结构体的字段 Score改成 score,我们还能正常访问吗?又应该如何解决这个问题呢?[老师给出思路,学员自己完成]
解决方法如下:
package model
//定义一个结构体
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 //ok
}
如果model包的student 的结构体的字段 Score改成 score,即使创建了类也访问不了score
解决方法如下:
package model
//定义一个结构体
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 //ok
}
10.6 面向对象编程特性一封装
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作
-
封装的好处
- 隐藏实现细节
- 提可以对数据进行验证,保证安全合理(Age)
-
封装的步骤(工厂说了)
-
将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似private)
-
给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
-
提供一个首字母大写的Set方法(类似其它语言的public),用于对属性判断并赋值
func(var 结构体类型名)SetXxx(参数列表)(返回值列表){ //加入数据验证的业务逻辑 var.字段 =参数 }
-
提供一个首字母大写的Get方法(类似其它语言的public),用于获取属性的值
func(var 结构体类型名)GetXxx(){ returnvar.age; }
特别说明:在Golang开发中并没有特别强调封装,这点并不像Java. 所以提醒学过java 的朋友,不用总是用java的语法特性来看待Golang,Golang本身对面向对象的特性做了简化的.
-
10.6 面向对象编程特性二继承
package main
import(
"fmt"
)
//编写一个学生考试系统
//小学生
type Pupilstruct{
Namestring
Ageint
Scoreint
}
//显示他的成绩
func(p *Pupil)ShowInfo(){
fmt.Printf("学生名=%v 年龄=%v 成绩=%v\n",p.Name,p.Age,p.Score)
}
func(p *Pupil)SetScore(scoreint){
//业务判断
p.Score=score
}
func(p*Pupil)testing(){
fmt.Println("小学生正在考试中.....")
}
//大学生, 研究生。。
//大学生
type Graduatestruct{
Namestring
Ageint
Scoreint
}
//显示他的成绩
func(p *Graduate)ShowInfo(){
fmt.Printf("学生名=%v 年龄=%v 成绩=%v\n",p.Name,p.Age,p.Score)
}
func(p *Graduate)SetScore(scoreint){
//业务判断
p.Score=score
}
func(p *Graduate)testing(){
fmt.Println("大学生正在考试中.....")
}
//代码冗余.. 高中生....
func main(){
//测试
varpupil=&Pupil{
Name:"tom",
Age: 10 ,
}
pupil.testing()
pupil.SetScore( 90 )
pupil.ShowInfo()
//测试
vargraduate=&Graduate{
Name:"mary",
Age: 20 ,
}
graduate.testing()
graduate.SetScore( 90 )
graduate.ShowInfo()
}
- Pupil 和 Graduate 两个结构体的字段和方法几乎,但是我们却写了相同的代码,代码复用性不强
- 出现代码冗余,而且代码不利于维护,同时也不利于功能的扩展。
- 解决方法-通过继承方式来解决
-
继承介绍
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体(比如刚才的Student),在该结构体中定义这些相同的属性和方法。
其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个Student匿名结构体即可。
在Golang中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。
type Goods struct{
Namestring
Priceint
}
type Bookstruct{
Goods //这里就是嵌套匿名结构体Goods
Writerstring
}
-
深入讨论
1 ) 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用
2 ) 匿名结构体字段访问可以简化
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.Name)
}
type B struct {
A
Name string
}
func (b *B) SayOk() {
fmt.Println("B SayOk", b.Name)
}
func main() {
b.Name = "smith"
b.age = 20
b.SayOk()
b.hello()
}
对上面的代码小结
- 当我们直接通过 b 访问字段或方法时,其执行流程如下比如 b.Name
- 编译器会先看b对应的类型有没有Name, 如果有,则直接调用B类型的Name字段
- 如果没有就去看B中嵌入的匿名结构体A有没有声明Name字段,如果有就调用,如果没有继续查找…如果都找不到就报错.
- 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
func main() {
var b B
b.Name = "jack" // ok
b.A.Name = "scott"
b.age = 100 //ok
b.SayOk() // B SayOk jack
b.A.SayOk() // A SayOk scott
b.hello() // A hello ? "jack" 还是 "scott"
}
- 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。
5 ) 如果一个struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
6 ) 如果一个结构体有int类型的匿名字段,就不能第二个。
7 ) 如果需要有多个int的字段,则必须给int字段指定名字
10.7 多重继承
- 多重继承说明
如一个 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。
- 案例演示 通过一个案例来说明多重继承使用
- 多重继承细节说明
1 ) 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。【案例演示】
2 ) 为了保证代码的简洁性,建议大家尽量不使用多重继承
10.8 接口
讲解多态前,我们需要讲解接口(interface),因为在Golang中 多态 特性主要是通过接口来体现的。
package main
import (
"fmt"
)
//声明/定义一个接口
type Usb interface {
//声明了两个没有实现的方法
Start()
Stop()
}
//声明/定义一个接口
type Usb2 interface {
//声明了两个没有实现的方法
Start()
Stop()
Test()
}
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接口变量来调用Start和Stop方法
usb.Start()
usb.Stop()
}
func main() {
//测试
//先创建结构体变量
computer := Computer{}
phone := Phone{}
camera := Camera{}
//关键点
computer.Working(phone)
computer.Working(camera) //
}
-
接口概念
interface类型可以定义一组方法,但是这些不需要实现。并且interface不能包含任何变量。到某个 自定义类型(比如结构体Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)。
1 ) 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。
2 ) Golang中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang中没有 implement这样的关键字
10.8.1注意事项与细节
1 ) 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)
2 ) 接口中所有的方法都没有方法体,即都是没有实现的方法。
3 ) 在Golang中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现 了该接口。
4 ) 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
5 ) 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。
6 ) 一个自定义类型可以实现多个接口
package main
import (
"fmt"
)
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()
}
7 ) Golang接口中不能有任何变量
type AInterface interface {
Name string //错误
Say()
}
8 ) 一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必须将B,C接口的方法也全部实现。
package main
import (
"fmt"
)
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 a AInterface = stu
a.test01()
}
10 ) 空接口interface{}没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量 赋给空接口。
package main
import (
"fmt"
)
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 a AInterface = stu
a.test01()
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)
}
10.9 接口与继承的区别
10 ) 空接口interface{}没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量 赋给空接口。
package main
import (
"fmt"
)
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 a AInterface = stu
a.test01()
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)
}
1 ) 当A结构体继承了B结构体,那么A结构就自动的继承了B结构体的字段和方法,并且可以直接使用
2 ) 当A结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此我们可以认为:实现接口是对继承机制的补充.
- 实现接口可以看作是对 继承的一种补充
-
接口和继承解决的解决的问题不同
继承的价值主要在于:解决代码的复用性和可维护性。
接口的价值主要在于:设计,设计好各种规范(方法),让其它自定义类型去实现这些方法。
-
接口比继承更加灵活 Person Student BirdAbleLittleMonkey
接口比继承更加灵活,继承是满足 is-a的关系,而接口只需满足 like-a的关系。
-
接口在一定程度上实现代码解耦
10.10 面向对象编程特性三多态
变量(实例)具有多种形态。面向对象的第三大特征,在Go语言,多态特征是通过接口实现的。可 以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。
在前面的Usb接口案例,Usbusb ,既可以接收手机变量,又可以接收相机变量,就体现了Usb 接 口 多态特性。[点明]
接口体现多态的两种形式
-
多态参数
在前面的Usb接口案例,Usbusb ,即可以接收手机变量,又可以接收相机变量,就体现了Usb 接口多态。
-
多态数组
演示一个案例:给Usb数组中,存放 Phone 结构体 和 Camera结构体变量
案例说明:
package main
import (
"fmt"
)
//声明/定义一个接口
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("相机停止工作。。。")
}
func main() {
//定义一个Usb接口数组,可以存放Phone和Camera的结构体变量
//这里就体现出多态数组
var usbArr [3]Usb
usbArr[0] = Phone{"vivo"}
usbArr[1] = Phone{"小米"}
usbArr[2] = Camera{"尼康"}
fmt.Println(usbArr)
}
10.11 类型断言
package main
import (
"fmt"
)
type Point struct {
x int
y int
}
func main() {
var a interface{}
var point Point = Point{1, 2}
a = point //oK
// 如何将 a 赋给一个Point变量?
var b Point
// b = a 不可以
// b = a.(Point) // 可以
b = a.(Point)
fmt.Println(b) //
}
- 断言介绍
类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言,
在进行类型断言时,如果类型不匹配,就会报 panic, 因此进行类型断言时,要确保原来的空接口指向的就是断言的类型.
- 如何在进行断言时,带上检测机制,如果成功就ok,否则也不要报panic
package main
import (
"fmt"
)
type Point struct {
x int
y int
}
func main() {
//类型断言(带检测的)
var x interface{}
var b2 float32 = 2.1
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("继续执行...")
}
- 给Phone结构体增加一个特有的方法call(), 当Usb 接口接收的是Phone 变量时,还需要调用call方法, 走代码:
package main
import (
"fmt"
)
//声明/定义一个接口
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 {
}
func (computer Computer) Working(usb Usb) {
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] = Camera{"尼康"}
//遍历usbArr
//Phone还有一个特有的方法call(),请遍历Usb数组,如果是Phone变量,
//除了调用Usb 接口声明的方法外,还需要调用Phone 特有方法 call. =》类型断言
var computer Computer
for _, v := range usbArr{
computer.Working(v)
fmt.Println()
}
//fmt.Println(usbArr)
}