第九课 go语言基础-面向对象编程

第九课 go语言基础-面向对象编程

tags:

  • golang
  • 2019尚硅谷

categories:

  • 结构体
  • 面向对象
  • 封装
  • 继承
  • 接口
  • 多态

第一节 结构体的介绍和基本使用

1.1 结构体的说明

  1. Golang也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说Golang支持面向对象编程特性是比较准确的。
  2. Golang 没有类(class), Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解Golang是基于struct 来实现OOP特性的
  3. Golang面向对象编程非常简洁,去掉了传统OOP语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等等
  4. Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不一样,比如继承: Golang 没有extends关键字,继承是通过匿名字段来实现
  5. Golang面向对象(OOP)很优雅,OOP本身就是语言类型系统(type system)的一部分, 通过接口(interface)关联,耦合性低,也非常灵活。也就是说在Golang中面向接口编程是非常重要的特性
  6. 举个例子(具体抽象为类的过程)
    • 将一类事物的特性提取出来(比如猫类), 形成-一个新的数据类型, 就是一个结构体。通过这个结构体,我们可以创建多个变量(实例/对象)
    • 事物可以猫类,也可以是Person,Fish或是某个工具类。。。

在这里插入图片描述

1.2 如何声明结构体

  1. 基本语法
type 结构体名称 struct {
    field1 type
    field2 type
}
  1. 举例说明。
package main
import (
	"fmt"
)

//如果结构体的字段类型是:指针,slice, 和map的零值都是nil ,即还没有分配空间
//如果需要使用这样的字段,需要先make,才能使用。
type Person struct{
	Name string
	Age int
	Scores [5]float64
	ptr *int //指针
	slice []int //切片
	map1 map[string]string //map
}

type Monster struct {
	Name string
	Age int
}


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 //ok 
	//使用map,一定要先make
	p1.map1 = make(map[string]string) 
	p1.map1["key1"] = "tom~"
	fmt.Println(p1)

	//不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,
	//不影响另外一个,结构体是值类型
	var monster1 Monster
	monster1.Name = "牛魔王"
	monster1.Age = 500
	monster2 := monster1 //结构体是值类型,默认为值拷贝
	monster2.Name ="青牛精"
	fmt.Println("monster1=", monster1) //monster1= {牛魔王500} 
	fmt.Println("monster2=", monster2) //monster2= {青牛精500}
}
  1. 从概念或叫法上看: 结构体字段=属性=field。字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。
  2. 注意事项和细节说明
    • 字段声明语法同变量,示例:字段名字段类型
    • 字段的类型可以为:基本类型、数组或引用类型
    • 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:布尔类型是false ,数值是0,字符串是""。
    • 数组类型的默认值和它的元素类型相关,比如score [3]int则为[0, 0, 0]指针,slice, 和map的零值都是nil ,即还没有分配空间。
    • 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个,结构体是值类型

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

  1. 方式1直接声明
    • 案例演示: var person Person
  2. 方式2-{}
    • 案例演示: var person Person = Person{}
  3. 方式3-&
    • 案例:var person *Person = new ( Person )
  4. 方式4-{}
    • 案例: var person *Person = &Person{}
  5. 说明:
    • 第3种和第4种方式返回的是结构体指针
    • **结构体指针访问字段的标准方式应该是: (结构体指针).字段名,比如(person).Name = “tom”
    • 但go做了一个简化,也支持结构体指针.字段名,比如person.Name = “tom”。更加符合程序员使用的习惯,*go 编译器底层对person.Name做了转化(person).Name
package main
import (
	"fmt"
)

//如果结构体的字段类型是:指针,slice, 和map的零值都是nil ,即还没有分配空间
//如果需要使用这样的字段,需要先make,才能使用。
type Person struct{
	Name string
	Age int
}


func main() {
	//第一种方式
	var p1 Person
	fmt.Println(p1)
	//方式2
	p2 := Person{"mary", 20}
	p2.Name = "tom"
	p2.Age = 18
	fmt.Println(p2)

	//方式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)
	//fmt.Println(p3)

	//方式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.4 结构体变量内存分析

在这里插入图片描述

package main
import (
	"fmt"
)

type Person struct{
	Name string
	Age int
}


func main() {
	var p1 Person
	p1.Age = 10 
	p1.Name ="小明"

	var p2 *Person = &p1 //这里是关键-->画出示意图
	fmt.Println((*p2).Age)
	fmt.Println(p2.Age)

	p2.Name = "tom~"
	fmt.Printf("p2.Name=%v p1.Name=%v \n", p2.Name, p1.Name) // tom~ tom~
	fmt.Printf("p2.Name=%v p1.Name=%v \n", (*p2).Name, p1.Name) // tom~ tom~
	fmt.Printf("p1的地址%p\n", &p1)
	fmt.Printf("p2的地址%p p2的值%p\n", &p2, p2)
}

在这里插入图片描述

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

  1. 结构体的所有字段在内存中是连续的
  2. 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
  3. 结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转
  4. struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
package main
import (
	"fmt"
	"encoding/json"
)

type A struct {
	Num int
}

type B struct {
	Num int
}

// `json:"name"` 中间不能有空格呀 `json: "name"`
type Monster struct{
	Name string `json:"name"`  //`json:"name"`就是struct tag
	Age int `json:"age"`
	Skill string `json:"skill"`
}

func main() {
	var a A
	var b B
	a = A(b) // ?可以转换,但是有要求,就是结构体的的字段要完全一样(包括:名字、 个数和类型!
	fmt.Println(a, b)

	//1.创建一个Monster变量
	monster := Monster{"牛魔王", 500, "芭蕉扇~"}
	fmt.Println(monster)
	//2.将monster变量序列化为json格式字串
	// json.Marshal函数中使用反射,这个讲解反射时,我会详细介绍
	jsonstr, err := json.Marshal(monster)
	if err != nil {
		fmt.Println("json处理错误", err) 
	}
	fmt.Println("jsonstr", string(jsonstr))
}

第二节 方法的介绍和基本使用

2.1 golang中的方法定义

  1. 在某些情况下,我们要需要声明(定义)方法。比如Person结构体:除了有一些字段外(年龄,姓名), Person结构体还有一些行为比如:可以说话、跑步…,通过学习,还可以做算术题。这时就要用方法才能完成。
  2. Golang中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct。
  3. 方法的定义和说明
    • 参数列表:表示方法输入
    • recevier type:表示这个方法和type这个类型进行绑定,或者说该方法作用于type类型
    • receiver type: type可以是结构体,也可以其它的自定义类型
    • receiver: 就是type类型的一个变量(实例),比如: Person 结构体的一个变量(实例)
    • 返回值列表:表示返回的值,可以多个
    • 方法主体:表示为了实现某一 功能代码块
    • return语句不是必须的。
func (recevier type) methodNames (参数列表) (返回值列表){
    方法体
    return返回值
}
  1. 方法的基本案例
    • test方法和Person类型绑定
    • test方法只能通过Person类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
    • func (p Person) test() }…p表示哪个Person变量调用,这个p就是它的副本,这点和函数传参非常相似。
    • p这个名字,由程序员指定,不是固定,比如修改成person也是可以
package main
import (
	"fmt"
)

type Person struct{
	Name string
}

// 给结构体绑定一个test方法
func (p Person) test(){
	fmt.Println("test() name", p.Name)
}

func main() {
	var p Person
	p.Name = "tom"
	p.test() //调用方法
}

2.2 方法的调用和传参机制原理

package main
import (
	"fmt"
)


type Person struct{
	Name string
}

// 给结构体绑定一个test方法
func (p Person) test(){
	fmt.Println("test() name", p.Name)
}


//给Person结构体添加speak方法,输出xxx是一个好人
func (p Person) speak() {
	fmt.Println(p.Name, "是一个goodman~")
}

//给Person结构体添加jisuan 方法,可以计算从1+...+1000的结果,
//说明方法体内可以函数一样,进行各种运算
func (p Person) jisuan() {
	res := 0
	for i:=1;i<=2;i++ {
		res += i
		fmt.Println(p.Name, "计算的结果是=", res)
	}
}

//给Person结构体jisuan2方法,该方法可以接收一个参数n,计算从1+..+n 的结果
func (p Person) jisuan2(n int) {
	res := 0
	for i:=1;i<=n;i++{
		res += i
	}
	fmt.Println(p. Name, "计算的结果是=", res)
}

//给Person结构体添加getSum方法,可以计算两个数的和,并返回结果
func (p Person) getSum(n1 int, n2 int) int {
	return n1 + n2
}

func main() {
	var p Person
	p.Name = "tom"
	p.test() //调用方法
	p.speak()
	p.jisuan()
	p.jisuan2(10)
	res := p.getSum(1, 12)
	fmt.Println("res:", res)
}
  1. 方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法
    • 在通过一个变量去调用方法时,其调用机制和函数–样
    • 不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝)
      在这里插入图片描述

2.3 方法的注意事项和细节

  1. 因为结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
  2. 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
  3. Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,
    都可以有方法,而不仅仅是struct,比如 int , float32等都可以有方法
  4. 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母
    大写,可以在本包和其它包访问。
  5. 如果一个类型实现了String()这个方法,那么fmt.Println 默认会调用这个变量的String()进行输

2.4 方法和函数区别

  1. 调用方式不一样
    • 函数的调用方式:函数名(实参列表)
    • 方法的调用方式:变量.方法名(实参列表)
  2. 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
  3. 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
  4. 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定
  5. *如果是和值类型,比如(p Person),则是值拷贝,如果和指针类型,比如是(p Person)则是地址拷贝。

第三节 结构体的赋值

3.1 创建结构体变量时指定字段值-包内

  1. Golang在创建结构体实例(变量)时,可以直接指定字段的值
package main
import (
	"fmt"
)

type Stu struct{
	Name string
	Age int
}

func main() {
	//在创建结构体变量时,就直接指定字段的值
	var stu1 = Stu{"小明", 19}
	stu2 := Stu{"小明~把", 29}

	//在创建结构体变量时,把字段名和字段值写在一起,这种写法,就不依赖字段的定义顺序.
	var stu3 = Stu{
		Name : "jack",
		Age : 13,
	}
	stu4 := Stu{
		Name : "Tony",
		Age : 15,
	}
	fmt.Println(stu1, stu2, stu3, stu4)

	//方式2, 返回结构体的指针类型在main函数中。
	//在创建结构体指针变量时,把字段名和字段值写在一起,这种写法,就不依赖字段的定义顺序
	var stu5 *Stu = &Stu{"小王", 29}
	stu6 := &Stu{"小紫", 21}
	stu7 := &Stu{
		Age: 23,
		Name: "小杨",
	}
	var stu8 = &Stu{
		Name: "小罗",
		Age: 39,
	}
	fmt.Println(stu5, stu6, stu7, stu8)
}

3.2 工程模式->实现构造函数(包外)

  1. Golang的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。
  2. 实际问题一如下:
    • 因为这里的Student的首字母S是大写的,如果我们想在其它包创建Student的实例(比如main包),引入model包后,就可以直接创建Student结构体的变量(实例)。
    • 但是问题来了,如果首字母是小写的,比如是type student struct …就不不行了,怎么办? 工厂模式来解决.
package model
type Student struct {
	Name string...
}
  1. 解决如下:
// main.go
package main
import (
	"fmt"
	"go_code/go_study_chapter09/demo03_oop_init/model"
)


func main() {
	//如果model包的结构体变量首字母大写,引入后,直接使用,没有问题
	//var stu1 = model.Student{"小明", 19.1}
	var stu1 = model.NewStudent("小明", 19.1)
	fmt.Println(*stu1)
	fmt.Println(stu1.Name, stu1.Score)
}
// model/main.go
package model


type student struct{
	Name string
	Score float64
}

//因为student结构体首字母是小写,因此是只能在mode1使用
//我们通过工厂模式来解决
func NewStudent(n string, s float64) *student{
	return &student{
		Name : n,
		Score : s,
	}
}
  1. 实际问题二如下:
    • 如果model包的student的结构体的字段Score 改成score, 我们还能正常访问吗?不能够直接访问
  2. 解决办法如下(在main中通过stu1.GetScore()调用):
package model


type student struct{
	Name string
	score float64
}

//因为student结构体首字母是小写,因此是只能在mode1使用
//我们通过工厂模式来解决
func NewStudent(n string, s float64) *student{
	return &student{
		Name : n,
		score : s,
	}
}

func (s *student) GetScore() float64{
	return s.score
}

第四节 面向对象编程特性

Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它OOP语言不一样,本节详细的讲解Golang的三大特性是如何实现的。

4.1 面向对象编程三大特性-封装

  1. 封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作
  2. 封装的好处
    • 隐藏实现细节
    • 可以对数据进行验证,保证安全合理(Age)
  3. 封装的表现形式
    • 对结构体中的属性进行封装
    • 通过方法,包实现封装
  4. 封装的实现步骤
    • 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似private)
    • 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
    • 提供一个首字母大写的Set方法(类似其它语言的public),用于对属性判断并赋值
    • 提供-一个首字母大写的Get方法(类似其它语言的public), 用于获取属性的值
func (var 结构体类型名) SetXxx(参数列表) (返回值列表) {
    //加入数据验证的业务逻辑
    var.字段=参数
}
func (var结构体类型名) GetXxx() {
	return var.age;
}
  1. 举个列子。
package model
import (
	"fmt"
)


type person struct{
	Name string
	age int // 小写其他包不能直接访问
	sal float64
}


func NewPerson(name string) *person{
	return &person{
		Name : name,
	}
}

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) SetSalary(sal int) {
	if sal >= 3000 && sal <= 30000{
		p.age = sal
	}else{
		fmt.Println("薪水输入错误!")
	}
}

func (p *person) GetSalary() float64{
	return p.sal
}

4.2 面向对象编程三大特性-继承

  1. 继承可以解决代码复用,让我们的编程更加靠近人类思维。
    • 代码的复用性提高了
    • 代码的扩展性和维护性提高了
  2. 当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法。
  3. 其它的结构体不需要重新定义这些属性和方法,只需嵌套一个Student匿名结构体。
  4. 在Golang中,如果一个struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。
type Goods struct {
    Name string
    Price int
}
type Book struct {
    Goods//这里就是嵌套匿名结构体Goods
    Writer string
}
  1. 继承的深入讨论
    • 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用
    • 匿名结构体字段访问可以简化
      • 当我们直接通过b访问字段或方法时,其执行流程如下比如b.Name
      • 编译器会先看b对应的类型有没有Name,如果有,则直接调用B类型的Name字段
      • 如果没有就去看B中嵌入的匿名结构体A有没有声明Name字段,如果有就调用,如果没有继续查找,如果都找不到就报错.
    • 结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
    • 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错
package main
import (
	"fmt"
)

type A struct{
	Name string
	Age int
}

func (a *A) SayOk(){
	fmt.Println("A SayOk", a.Name)
}

func (a *A) hello(){
	fmt.Println("A hello", a.Name)
}

type B struct{
	A
	Name string
}

type C struct{
	Name string
	Score float64
}

type D struct{
	A 
	C
}
func main() {
	// 1. 通过B结构体访问 A结构体的方法 小写或者大写
	var b B
	b.A.Name = "tom"
	b.A.Age = 12
	b.A.SayOk()
	b.A.hello()
	// 2. 简化访问方式
	b.SayOk()
	b.hello()
	// 3. 就近访问原则 如果B有 直接访问 当然也可以指定访问
	b.Name = "xiao"
	fmt.Println(b.Name)
	fmt.Println(b.A.Name)
	// 4. 父类中有相同字段 且子类没有字段 那么必须指定访问那个父类的字段 否则会报错
	var d D
	// d.Name = "ming"
	// fmt.Println(d.Name)
	d.A.Name = "ming"
	fmt.Println(d.A.Name)
}
  1. 如果一个struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
package main
import (
	"fmt"
)

type A struct{
	Name string
	Age int
}
type F struct{
	a A // 有名结构体 组合关系
}
func main() {
//如果D中是一个有名结构体,则访问有名结构体的字段时,就必须带上有名结构体的名字
	var f F
	f.a.Name = "小军"
	fmt.Println(f.a.Name)
}
  1. 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
  2. 一个struct嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方
    法,从而实现了多重继承。
    • 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分
    • 为了保证代码的简洁性,建议大家尽量不使用多重继承
package main
import (
	"fmt"
)


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(){
	//嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
	tv1 := TV{Goods{"电视机1", 5900}, Brand{"海尔", "山东"}}
	tv2 := TV{
		Goods{
			Price : 1324,
			Name : "电视机2",
		},
		Brand{
			Name : "夏普",
			Address : "北京", 
		},
	}
	tv3 := TV2{ &Goods{"电视机3", 1200}, &Brand{"苹果", "山东"},}
	tv4 := TV2{
		&Goods{
			Price : 1254,
			Name : "电视机4",
		},
		&Brand{
			Name : "长虹",
			Address : "北京", 
		},
	}
	fmt.Println("tv1", tv1)
	fmt.Println("tv2", tv2)
	fmt.Println("tv3", *tv3.Goods, *tv3.Brand)
	fmt.Println("tv4", *tv4.Goods, *tv4.Brand)
}
  1. 结构体的匿名字段是基本数据类型
package main
import (
	"fmt"
)


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)
}

第五节 接口和多态

5.1 接口的定义

  1. Golang中多态特性主要是通过接口来体现的。
  2. interface类型可以定义一组方法,但是这些不需要实现。并且interface不能包含任何变量。到某个自定义类型(比如结构体Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)。
  3. 接口的基本语法
    • 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。
    • Golang 中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有implement这样的关键字
      在这里插入图片描述
package main
import (
	"fmt"
)

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.Start()
	usb.Stop()
}

func main(){
	// 测试 
	// 创建结构体变量
	computer := Computer{}
	phone := Phone{}
	camera := Camera{}

	//关键点
	computer.Working(phone)
	computer.Working(camera)
}

5.2 接口的注意事项和细节

  1. 重点:接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量
package main
import (
	"fmt"
)

type AInterface interface{
	Say()
}

type Stu struct{
	Name string
}

func (stu Stu) Say(){
	fmt.Println("Stu Say()")
}

func main() {
	var stu Stu
	var a AInterface = stu
	a.Say()
}
  1. 接口中所有的方法都没有方法体即都是没有实现的方法。
  2. 在Golang中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现
    了该接口。
  3. 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
  4. 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。
type integer int
func(i integer) Say(){
	fmt.Println("integer say i=", i)
}

var i integer = 10
var b AInterface = i
b.Say()
  1. 一个自定义类型可以实现多个接口
package main
import (
	"fmt"
)

type AInterface interface{
	Say()
}
type BInterface interface{
	Hello()
}

type Monster struct{

}

func (m Monster) Say(){
	fmt.Println("Monster say~~")
}
func (m Monster) Hello(){
	fmt.Println("Monster hello~~")
}
func main() {
	var m Monster
	var a AInterface = m
	var b BInterface = m
	a.Say()
	b.Hello()
}
  1. Golang接口中不能有任何变量
    在这里插入图片描述

  2. 一个接口(比如A接口)可以继承多个别的接口(比如B,C接口),这时如果要实现A接口,也必
    须将B,C接口的方法也全部实现。

  3. interface 类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil

  4. 空接口interface{} 没有任何方法,所以所有类型都实现了空接口,即我们可以把任何一个变量赋给空接口

type T interface{

}
type Stu struct{
	Name string
}


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)

5.3 接口编程的最佳实践

  1. 实现对Hero结构体切片的排序: sort. Sort(data Interface)
  2. 实现Sort对应的接口,Len Less Swap
package main
import (
	"fmt"
	"sort"
	"math/rand"
)

// 声明一个Hero的结构体
type Hero struct{
	Name string
	Age int
}

// 声明一个结构体切片类型
type HeroSlice []Hero

// 实现一个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) {
	// temp := hs[i]
	// hs[i] = hs[j]
	// hs[j] = temp
	//下面的一句话等价于三句话
	hs[i], hs[j] = hs[j], hs[i]
}

//1.声明Student结构体
type Student struct{
	Name string
	Age int 
	Score float64
}

func main() {
	//先定义一个数组/切片
	var intSlice = []int{0,-1, 10, 7, 90}
	//要求对intSlice 切片进行排序
	//1.冒泡排序
	//2.也可以使用系统提供的方法.
	sort.Ints(intSlice)
	fmt.Println(intSlice)
	//请大家对结构体切片进行排序.
	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)
	}
}

5.4 接口VS继承

在这里插入图片描述

  1. 当A结构体继承了B结构体,那么A结构就自动的继承了B结构体的字段和方法,并且可以直接使用

  2. 当A结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此我们可以认为:实现接口是对继承机制的补充.
    在这里插入图片描述

  3. 接口和继承解决的解决的问题不同.

    • 继承的价值主要在于:解决代码的复用性和可维护性
    • 接口的价值主要在于设计:设计好各种规范(方法),让其它自定义类型去实现这些方法。
  4. 接口比继承更加灵活Person Student BirdAble LittleMonkey

    • 接口比继承更加灵活,继承是满足is-a 的关系,而接口只需满足like-a 的关系。
    • 接口在一定程度上实现代码解耦

5.5 面向对象编程三大特性-多态

  1. 变量(实例)具有多种形态。面向对象的第三大特征,在Go语言,多态特征是通过接口实现的。可
    以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。
  2. 在前面的Usb接口案例,Usb usb ,既可以接收手机变量,又可以接收相机变量,就体现了Usb接
    口多态特性。
  3. 接口体现多态的两种形式
    • 多态参数(前面的Usb案例)
    • 多态数组(数组中存两种结构体实例)
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("手机停止工作。。。")
}

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.Start()
	usb.Stop()
}

func main(){
	//定义一个Usb接口数组,可以存放Phone和Camera的结构体变量
	//这里就体现出多态数组
	var usbArr [3]Usb 
	usbArr[0] = Phone{"vivo"} 
	usbArr[1]= Phone{"小米"}
	usbArr[2] = Camera{"尼康"}
	fmt.Println(usbArr)
}

5.6 类型断言

在这里插入图片描述

  1. 类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言,具体的如下:
//类型断言的其它案例
var x interface{}
var b2 float32 = 1.1
x = b2 //空接口,可以接收任意类型
// x=>float32 [使用类型断言]
y := x.(float32)
fmt. Printf("y的类型是%T值是=%v", y, y)
  1. 在进行类型断言时,如果类型不匹配,就会报panic, 因此进行类型断言时,要确保原来的空接口,指向的就是断言的类型.
  2. 如何在进行断言时,带上检测机制,如果成功就ok,否则也不要报panic
var x interface{}
	var b2 float32 = 1.1
	x = b2 //空接口,可以接收任意类型
	if y, ok := x.(float32); ok{
		fmt.Println("convert success")
		fmt.Printf("y的类型是%T值是=%v", y, y)
	}else{
		fmt.Println("convert fail")
	}
	fmt.Println("继续执行...")

5.7 类型断言的最佳实践

  1. 最佳实践一
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{

}

// 编写一个方法Working方法,接收一个Usb接口类型变量
// 只要是实现了Usb接口(所谓实现Usb接口,就是指实现了Usb接口声明所有方法)
func (c 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{"尼康"}
	//Phone还有一个特有的方法call(), 请遍历Usb数组, 如果是Phone变量,
	//除了调用Usb接口声明的方法外, 还需要调用Phone 特有方法call.=》类型断言
	var computer Computer
	for _, v := range usbArr{
		computer.Working(v)
		fmt.Println()
	}
	fmt.Println(usbArr)
}
  1. 最佳实践二
package main
import (
	"fmt"
)

type student struct{
	Name string
}
//编写一个函数,可以判断输入的参数是什么类型
// ...可变参数(可接收任意多个任意类型的参数)
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 类型, 值是%y\n", index, x)
			case int, 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.1
	var n2 float64 = 2.3
	var n3 int32 = 30
	var name string = "tom"
	address := "北京"
	n4 := 300
	stu := student{"xiaoming"}
	TypeJudge(n1, n2, n3, name, address, n4, stu, &stu)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值