Go语言精修(尚硅谷笔记)第十章

十、面向对象编程

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 结构体在内存里的布局

image-20210115123639224

  • 基本语法
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}
}

画出上面代码的内存示意图:

image-20210115124240183

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

注意

image-20210115143653501

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)

}

对应的分析图:

image-20210115143859166

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认为是新的数据类型,但是相互间可以强转

image-20210115144349214

4 ) struct的每个字段上,可以写上一个 tag , 该tag可以通过反射机制获取,常见的使用场景就是序列化和反序列化。

  • 序列化的使用场景:

image-20210115144144616

  • 举例:
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 方法的调用与传参机制原理*

方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法。下面我们举例说明。

image-20210115152305557

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 包的 结构体变量首字母大写,引入后,直接使用 , 没有问题

image-20210115164621482

如果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)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作

  • 封装的好处

    1. 隐藏实现细节
    2. 提可以对数据进行验证,保证安全合理(Age)
    • 封装的步骤(工厂说了)

      1. 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似private)

      2. 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数

      3. 提供一个首字母大写的Set方法(类似其它语言的public),用于对属性判断并赋值

        func(var 结构体类型名)SetXxx(参数列表)(返回值列表){
            //加入数据验证的业务逻辑
            var.字段 =参数
        }
        
      4. 提供一个首字母大写的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()
}
  1. Pupil 和 Graduate 两个结构体的字段和方法几乎,但是我们却写了相同的代码,代码复用性不强
  2. 出现代码冗余,而且代码不利于维护,同时也不利于功能的扩展
  3. 解决方法-通过继承方式来解决
  • 继承介绍

    当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体(比如刚才的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()

}

对上面的代码小结

  1. 当我们直接通过 b 访问字段或方法时,其执行流程如下比如 b.Name
  2. 编译器会先看b对应的类型有没有Name, 如果有,则直接调用B类型的Name字段
  3. 如果没有就去看B中嵌入的匿名结构体A有没有声明Name字段,如果有就调用,如果没有继续查找…如果都找不到就报错.
  4. 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
  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"
   }
  1. 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。

5 ) 如果一个struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字

6 ) 如果一个结构体有int类型的匿名字段,就不能第二个。

7 ) 如果需要有多个int的字段,则必须给int字段指定名字

10.7 多重继承

  • 多重继承说明

如一个 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。

  • 案例演示 通过一个案例来说明多重继承使用

image-20210115224909098

  • 多重继承细节说明

1 ) 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。【案例演示】

image-20210115224922874

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)要使用的时候,在根据具体情况把这些方法写出来(实现)。

image-20210115225240261

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结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此我们可以认为:实现接口是对继承机制的补充.

  • 实现接口可以看作是对 继承的一种补充

image-20210115231702844

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

    继承的价值主要在于:解决代码的复用性和可维护性。

    接口的价值主要在于:设计,设计好各种规范(方法),让其它自定义类型去实现这些方法。

  • 接口比继承更加灵活 Person Student BirdAbleLittleMonkey

    接口比继承更加灵活,继承是满足 is-a的关系,而接口只需满足 like-a的关系。

  • 接口在一定程度上实现代码解耦

10.10 面向对象编程特性三多态

变量(实例)具有多种形态。面向对象的第三大特征,在Go语言,多态特征是通过接口实现的。可 以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。

在前面的Usb接口案例,Usbusb ,既可以接收手机变量,又可以接收相机变量,就体现了Usb 接 口 多态特性。[点明]

image-20210115231808697

接口体现多态的两种形式

  • 多态参数

    在前面的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) // 
}	

image-20210115232125223

  • 断言介绍

类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言,

在进行类型断言时,如果类型不匹配,就会报 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)
}

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值