介绍

这个是在B站上看边看视频边做的笔记,这一章是Glang面向对象编程

这一章内容较多,内容有Go语言的结构体是什么,怎么声明;Golang方法的调用和声明;go语言面向对象实例,go语言工厂模式;golang面向对象的三大特性:继承、封装、多态,面向对象编程应用实例,golang的接口介绍,类型断言

配套视频自己去B站里面搜【go语言】,最高的播放量就是

里面的注释我写的可能不太对,欢迎大佬们指出╰(°▽°)╯

文章目录

  • 介绍
  • (八)、面向对象编程
      • 1.看一个问题
      • 2.使用现有技术解决
      • 3.现有技术解决的缺点分析
      • 4.Golang 语言面向对象编程说明
    • 一、结构体
      • 1.快速入门
      • 2.结构体变量(实例)在内存的布局(重要!)
      • 3.如何声明结构体
      • 4.字段/属性
      • 5.创建结构体变量和访问结构体字段
      • 6.struct 类型的内存分配机制
      • 7.结构体使用注意事项和细节
    • 二、方法
      • 1.基本介绍
      • 2.方法的声明和调用
      • 3.方法快速入门
      • 4.方法的调用和传参机制原理:(重要!)
      • 5.方法的声明(定义)
      • 6.方法的注意事项和细节
      • 7.方法的练习题
        • 练习题
      • 8.方法和函数区别
    • 三、面向对象编程应用实例
      • 1.步骤
      • 2.案例1
      • 3.案例2
      • 4.案例3
      • 5.案例4
    • 四、创建结构体变量时指定字段值
    • 五、工厂模式
      • 1.说明
      • 2.工厂模式案例
      • 3.思考题
    • 六、面向对象编程思想-抽象
      • 1.抽象的介绍
    • 七、面向对象编程三大特性-封装
      • 1.基本介绍
      • 2.封装介绍
      • 3.封装的理解和好处
      • 4.如何体现封装
      • 5.封装的实现步骤
      • 6.快速入门案例
      • 7.练习
    • 八、面向对象编程三大特性-继承
      • 1.引出继承的必要性
      • 2.继承基本介绍和示意图
      • 3.嵌套匿名结构体的基本语法
      • 4.快速入门案例
      • 5.继承的深入讨论
      • 6.练习
      • 7.面向对象编程-多重继承
    • 九、接口(interface)
      • 1.基本介绍
      • 2.接口快速入门
      • 3.基本语法
      • 4.接口使用的应用场景
      • 5.注意事项和细节
      • 6.接口编程的最佳实践
      • 7.实现接口vs 继承
    • 十、面向对象编程三大特性-多态
      • 1.基本介绍
      • 2.快速入门
      • 3.接口体现多态的两种形式
      • 4.类型断言
        • 1.基本介绍
        • 2.类型断言的最佳实践
  • 章节目录

(八)、面向对象编程

1.看一个问题

2.使用现有技术解决

package mainfunc main() {/*张老太养了两只猫猫:一只名字叫小白,今年3岁,白色。还有一只叫小花,今年100岁,花色。请编写一个程序,当用户输入小猫的名字时,就显示该猫的名字,年龄,颜色。如果用户输入的小猫名错误,则显示张老太没有这只猫猫。*///1.使用变量处理// var cat1Name string = "小白"// var cat1Age int = 3// var cat1Color string = "白色"// var cat2Name string = "小花"// var cat2Age int = 100// var cat2Color string = "花色"//2.使用数组处理// var catNames [2]string = [...]string{"小白", "小花"}// var catAge [2]int = [...]int{3, 100}// var catColor [2]string = [...]string{"白色", "花色"}//3.使用map解决}

3.现有技术解决的缺点分析

  1. 使用变量或者数组来解决养猫的问题,不利于数据的管理和维护。因为名字,年龄,颜色都是属于一只猫,但是这里是分开保存。
  2. 如果我们希望对一只猫的属性(名字、年龄,颜色)进行操作(绑定方法), 也不好处理。
  3. 引出我们要讲解的技术–> 结构体

4.Golang 语言面向对象编程说明

  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 中面向接口编程是非常重要的特性。

一、结构体

对上图的说明

  1. 将一类事物的特性提取出来(比如猫类), 形成一个新的数据类型, 就是一个结构体。
  2. 通过这个结构体,我们可以创建多个变量(实例/对象)
  3. 事物可以猫类,也可以是Person , Fish 或是某个工具类。。。

package mainimport "fmt"type monster struct {Name  string   `json:"name"`Age   int      `json:"age"`Skill []string `json:"skill"`
}func main() {monster := monster{Name: "哈哈",Age:  100,}// monster.Skill = make([]string, 2, 6)monster.Skill = append(monster.Skill[:], "武器1")monster.Skill = append(monster.Skill[:], "武器2")monster.Skill = append(monster.Skill[:], "武器3")// monster.Skill[1] = "武器2"fmt.Println(monster)
}

输出

{哈哈 100 [武器1 武器2 武器3]}

1.快速入门

  • 面向对象的方式(struct结构体)解决养猫问题

    package mainimport "fmt"//定义一个Cat结构体,将Cat的各个字段/属性信息,放入到Cat结构体进行管理
    type Cat struct {Name  string //字符串模式是空Age   int    //int类型默认为0Color string //字符串模式是空Hobby string
    }func main() {/*张老太养了两只猫猫:一只名字叫小白,今年3岁,白色。还有一只叫小花,今年100岁,花色。请编写一个程序,当用户输入小猫的名字时,就显示该猫的名字,年龄,颜色。如果用户输入的小猫名错误,则显示张老太没有这只猫猫。*///使用struct结构体来完成案例var cat1 Cat //创建一个cat的变量fmt.Println(cat1)//给结构体赋值cat1.Name = "小白"cat1.Age = 3cat1.Color = "白色"fmt.Println("猫的信息如下:")fmt.Println("Nmae =", cat1.Name)fmt.Println("Age =", cat1.Age)fmt.Println("Color =", cat1.Color)
    }
    

通过上面的案例和讲解我们可以看出:

  • 结构体和结构体变量(实例)的区别和联系

    1. 结构体是自定义的数据类型,代表一类事物.
    2. 结构体变量(实例)是具体的,实际的,代表一个具体变量

2.结构体变量(实例)在内存的布局(重要!)

3.如何声明结构体

  • 基本语法

    type 结构体名称 struct {field1 typefield2 type
    }
    
  • 举例:

    type Student struct {Name string //字段Age int     //字段Score float32
    }
    

4.字段/属性

  • 基本介绍

    1. 从概念或叫法上看: 结构体字段= 属性= field (即授课中,统一叫字段)
    2. 字段是结构体的一个组成部分,一般是基本数据类型数组,也可是引用类型。比如我们前面定义猫结构体的Name string 就是属性
  • 注意事项和细节说明

    1. 字段声明语法同变量,示例:字段名字段类型

    2. 字段的类型可以为:基本类型、数组或引用类型

    3. 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:

    布尔类型是false ,数值是0 ,字符串是""。

    数组类型的默认值和它的元素类型相关,比如score [3]int 则为[0, 0, 0]

    指针,slice,和map 的零值都是nil ,即还没有分配空间。

    案例演示:

    package mainimport "fmt"//指针,slice,和map 的零值都是nil ,即还没有分配空间。
    //如果需要使用这样的字段,需要先make,才能使用type Person struct {Name   string
    Age    int
    Scores [5]float64
    ptr    *int              //指针
    slice  []int             //切片
    map1   map[string]string //map
    }func main() {//定义结构体
    var p1 Person
    fmt.Println(p1) //{ 0 [0 0 0 0 0] <nil> [] map[]}if p1.ptr == nil {fmt.Println("ptr为空")
    }if p1.slice == nil {fmt.Println("slice为空")
    }if p1.map1 == nil {fmt.Println("map1为空")
    }//使用slice,一定要make
    p1.slice = make([]int, 10)
    p1.slice[0] = 100
    fmt.Println(p1.slice) //[100 0 0 0 0 0 0 0 0 0]//使用map,一定要make
    p1.map1 = make(map[string]string)
    p1.map1["key1"] = "1"
    fmt.Println(p1.map1) //map[key1:1]fmt.Println(p1) //{ 0 [0 0 0 0 0] <nil> [100 0 0 0 0 0 0 0 0 0] map[key1:1]}}
    
    1. 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体是值类型
    package mainimport "fmt"type Monster struct {Name string
    Age  int
    }func main() {//不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,
    //不影响另外一个,结构体是值类型
    var monster1 Monster
    monster1.Name = "牛魔王"
    monster1.Age = 500monster2 := monster1  //这里因为是值拷贝 结构体是值类型,默认为拷贝
    monster2.Name = "红孩儿" //不会影响monster1fmt.Println("monster1=", monster1) //monster1= {牛魔王 500}
    fmt.Println("monster2=", monster2) //monster2= {红孩儿 500}
    }
    

    上图内存示意图:

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

  • 方式1-直接声明

    案例演示: var person Person

        //方式1var p1 Personp1.Name = "tom"p1.Age = 23fmt.Println(p1)
    
  • 方式2-{}

    案例演示: var person Person = Person{}

       //方式2p2 := Person{"mary", 20}fmt.Println(p2)
    
  • 方式3-&

    案例演示: var person *Person = new (Person)

       //方式3var p3 *Person = new(Person) //因为p3是一个指针,因此标准的给字段方式是赋值(*p3).Name = "smith"         //标准写法(*p3).Age = 30fmt.Println(*p3)
    
  • 方式4-{}

    案例演示: var person *Person = &Person{}

     //方式4//也可以直接给值//var person *Person = &Person{"jak", 30}var person *Person = &Person{}//因为person是一个指针,因此标准的访问字段的方法是//(*person).Name = "scott"(*person).Name = "scott" //标准写法person.Name = "scott"    //简化写法(*person).Age = 16 //标准写法person.Age = 16    //简化写法fmt.Println(*person)
    
  • 说明:

    1. 第3 种和第4 种方式返回的是结构体指针。
    2. 结构体指针访问字段的标准方式应该是:(*结构体指针).字段名,比如(*person).Name = "tom"
    3. 但go 做了一个简化,也支持结构体指针.字段名, 比如person.Name = "tom"。更加符合程序员使用的习惯,go 编译器底层对person.Name 做了转化(*person).Name

6.struct 类型的内存分配机制

  • 看一个思考题

输出的结果是: p2.Name = tom p1.Name = 小明

  • 看下面代码,并分析原因

原因:

  • 看下面代码,并分析原因

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

  1. 结构体的所有字段在内存中是连续的

    package mainimport "fmt"//结构体
    type Point struct { //声明一个结构体x int //声明类型y int
    }type Rect struct { //声明一个结构体leftUp, reghtDown Point //把Point分别传入给leftUp和reghtDown
    }type Rect2 struct {leftUp, reghtDown *Point //把Point的指针地分别传入给leftUp和reghtDown}func main() {//r1有4个int,在内存中是连续分布//打印地址r1 := Rect{Point{1, 2}, Point{3, 4}} //调用结构体Rect,并把Point{1, 2}传入给leftUp,Point{3, 4}出入给reghtDownfmt.Printf("r1.leftUp.x 地址=%p\nr1.leftUp.y 地址=%p\n", &r1.leftUp.x, &r1.leftUp.y)fmt.Printf("r1.reghtDown.x 地址=%p\nr1.reghtDown.y 地址=%p\n", &r1.reghtDown.x, &r1.reghtDown.y)fmt.Println()//r2有两个*Point类型, 这个两个*point类型的本身地址也是连续的,//但是他们指向的地址不一定是连续r2 := Rect2{&Point{1, 2}, &Point{3, 4}}//本身地址fmt.Printf("r2.leftUp 本身地址=%p\nr2.reghtDown 本身地址=%p\n", &r2.leftUp, &r2.reghtDown)//指向地址不一定连续,fmt.Printf("r2.leftUp 指向地址=%p\nr2.reghtDown 指向地址=%p\n", r2.leftUp, r2.reghtDown)
    }
    

    输出:

    r1.leftUp.x 地址=0xc00000c240
    r1.leftUp.y 地址=0xc00000c248
    r1.reghtDown.x 地址=0xc00000c250
    r1.reghtDown.y 地址=0xc00000c258r2.leftUp 本身地址=0xc00005a250
    r2.reghtDown 本身地址=0xc00005a258
    r2.leftUp 指向地址=0xc0000140c0
    r2.reghtDown 指向地址=0xc0000140d0
    

    对应的分析图:

  1. 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)

    package mainimport "fmt"type A struct {Num int
    }type B struct {Num int
    }//结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
    func main() {var a A           //a的类型是结构体Avar b B           //b的类型是结构体Bfmt.Println(a, b) //{0} {0}a = A(b) //对b进行强制转换,转换为结构体A,之所以能进行强转,是因为结构体的类型字段是一致的(名字、格式、类型)fmt.Println(a)
    }
    
  1. 结构体进行type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转

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

    • 序列化的使用场景:

      package mainimport ("encoding/json""fmt"
      )type Monster struct {Name  string `json:"name"` //`json:"name"`就是struct结构体的标签,使用json进行序列化时会输出标签Age   int    `json:"age"`Skill string `json:"skill"`
      }func main() {//1.创建一个Monster变量monster := Monster{"牛魔王", 500, "野蛮冲撞"}//2.将monster变量序列化为json格式字符串//内置函数json.Marshal函数中使用反射jsonMonster, err := json.Marshal(monster)if err != nil {fmt.Println("转换错误", err)}fmt.Println("jsonStr", jsonMonster) //jsonStr [123 34 110 ......... 158 34 125]//需要转换为string类型fmt.Println("jsonStr", string(jsonMonster)) // jsonStr {"name":"牛魔王","age":500,"skill":"野蛮冲撞"}
      }
      

二、方法

1.基本介绍

在某些情况下,我们要需要声明(定义)方法。

比如Person 结构体:除了有一些字段外( 年龄,姓名…)

Person 结构体还有一些行为比如:可以说话、跑步…,通过学习,还可以做算术题。

这时就要用方法才能完成。

Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct。

2.方法的声明和调用

type A struct {Num int
}
func (a A) test() {fmt.Println(a.Num)
}
  • 对上面的语法的说明

    1. func (a A) test() {} 表示A 结构体有一方法,方法名为test
    2. (a A) 体现test 方法是和A 类型绑定的
  • 举例:

    package mainimport "fmt"type Person struct {Name string
    }//给Person类型绑定一个方法
    func (p Person) test() { //把结构Person绑定给p,并调用test()函数,此时test()为方法fmt.Println("test() name =", p.Name) //由于绑定过Person结构体
    }func main() {var p Person //声明p的类型p.test()     //调用方法   test() name=p.Name = "tom" //tom赋值给p.Namep.test()       //调用方法   test() name = tom}
    
  • 对上面的总结

    1. test 方法和Person 类型绑定

    2. test 方法只能通过Person 类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用

    3. func (p Person) test() {}… p 表示哪个Person 变量调用,这个p 就是它的副本, 这点和函数传参非常相似。

    4. p 这个名字,有程序员指定,不是固定, 比如修改成person 也是可以

3.方法快速入门

  1. 给Person 结构体添加speak 方法,输出xxx 是一个好人

    package mainimport "fmt"type Person struct {Name string
    }func (p Person) speak() {fmt.Println(p.Name, "是一个好人")
    }func main() {var p Person //必须先声明pp.Name = "tom"p.speak()
    }
    
  2. 给Person 结构体添加jisuan 方法,可以计算从1+…+1000 的结果, 说明方法体内可以函数一样,进行各种运算

    package mainimport "fmt"type Person struct {Name string
    }//给Person 结构体添加jisuan 方法,可以计算从1+..+1000 的结果, 说明方法体内可以函数一样,进行各种运算
    func (p Person) jisuan() {res := 0for i := 0; i <= 1000; i++ {res += i}fmt.Println(p.Name, "计算的结果是=", res)
    }func main() {var p Person //必须先声明pp.Name = "tom"p.speak()p.jisuan() //tom 计算的结果是= 500500
    }
    
  3. 给Person 结构体jisuan2 方法,该方法可以接收一个数n,计算从1+…+n 的结果

    type Person struct {Name string
    }
    //给Person 结构体jisuan2 方法,该方法可以接收一个数n,计算从1+..+n 的结果
    func (p Person) jisuan2(n int) {res := 0for i := 0; i <= n; i++ {res += i}fmt.Println(p.Name, "计算的结果是=", res)
    }
    func main() {var p Person //必须先声明pp.Name = "tom"p.jisuan2(10) //tom 计算的结果是= 55
    }
    
  4. 给Person 结构体添加getSum 方法,可以计算两个数的和,并返回结果

    type Person struct {Name string
    }//给Person 结构体添加getSum 方法,可以计算两个数的和,并返回结果
    func (p Person) getSum(n1 int, n2 int) int {return n1 + n2
    }func main() {var p Person //必须先声明pp.Name = "tom"res := p.getSum(10, 20)fmt.Println(res) //30
    }

4.方法的调用和传参机制原理:(重要!)

  • 说明:

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

  • 案例1:

    画出前面getSum 方法的执行过程+说明

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LhFSHfUb-1669362409965)(assets/image-20221017140607810.png)]

    说明:

    1. 在通过一个变量去调用方法时,其调用机制和函数一样
    2. 不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地质拷贝)
  • 案例2

    请编写一个程序,要求如下:

    1. 声明一个结构体Circle, 字段为radius
    2. 声明一个方法area 和Circle 绑定,可以返回面积。
    3. 提示:画出area 执行过程+说明
    package mainimport "fmt"//1) 声明一个结构体Circle, 字段为radius
    //2) 声明一个方法area 和Circle 绑定,可以返回面积。
    //3) 提示:画出area 执行过程+说明type Circle struct {radius float64
    }func (c Circle) area() float64 {return 3.14 * c.radius * c.radius //圆面积=圆周率×半径×半径
    }func main() {//创建一个Circle变量var c Circlec.radius = 4.0res := c.area()fmt.Println(res) //50.24
    }
    

5.方法的声明(定义)

func (recevier type) methodName(参数列表) (返回值列表){方法体return 返回值
}
  1. 参数列表:表示方法输入
  2. recevier type : 表示这个方法和type 这个类型进行绑定,或者说该方法作用于type 类型
  3. receiver type : type 可以是结构体,也可以其它的自定义类型
  4. receiver : 就是type 类型的一个变量(实例),比如:Person 结构体的一个变量(实例)
  5. 返回值列表:表示返回的值,可以多个
  6. 方法主体:表示为了实现某一功能代码块
  7. return 语句不是必须的。

6.方法的注意事项和细节

  1. 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式

  2. 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理

    package mainimport "fmt"type Circle struct {radius float64
    }//为了提高效率,通常我们方法和结构体的指针类型绑定
    func (c *Circle) area2() float64 {//因为c是指针,因此我们标准的访问其字段的方式是(*c).redius//标准写法 return 3.14 * (*c).radius * (*c).radius //圆面积=圆周率×半径×半径return 3.14 * c.radius * c.radius
    }func main() {//创建一个Circle变量var c Circlec.radius = 5.0res2 := (&c).area2() //标准写法fmt.Println(res2)//编译器底层做了优化(&c).area2() 等价c.area()//因为编译器会自动的给加上&cres1 := c.area2()fmt.Println(res1)}
    

  3. Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct, 比如int , float32 等都可以有方法

    package mainimport "fmt"/*
    Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,
    都可以有方法,而不仅仅是struct, 比如int , float32 等都可以有方法
    */type integer intfunc (i integer) print() {fmt.Println("i =", i)
    }//编写一个方法, 可以改变i的值
    func (i *integer) change() {*i = *i + 1
    }func main() {var i integer = 10i.print() //i = 10i.change()fmt.Println("i =", i) //i = 11
    }
    
  4. 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。

  5. 如果一个类型实现了String()这个方法,那么fmt.Println 默认会调用这个变量的String()进行输出

    package mainimport "fmt"type Student struct {Name stringAge  int
    }func (stu *Student) String() string { //赋值Student的指针给stustr := fmt.Sprintf("Name=[%v] Age=[%v]", (*stu).Name, (*stu).Age)return str
    }func main() {//定义一个Student变量stu := Student{Name: "tom",Age:  20,}fmt.Println(stu) //不使用指针输出,使用默认方式输出 {tom 20}fmt.Println(&stu) //使用地址进行输出 Name=[tom] Age=[20]
    }
    

7.方法的练习题

  1. 编写结构体(MethodUtils),编程一个方法,方法不需要参数,在方法中打印一个10*8 的矩形,在main 方法中调用该方法。
package mainimport "fmt"//编写结构体(MethodUtils),编程一个方法,方法不需要参数,在方法中打印一个10*8 的矩形,
//在main 方法中调用该方法。type MethodUtils struct {}//给MethodUtils编写方法
func (mu MethodUtils) Print() {for j := 0; j < 10; j++ {for i := 0; i < 8; i++ {fmt.Printf("*")}fmt.Println()}}func main() {var mu MethodUtilsmu.Print()
}
  1. 编写一个方法,提供m 和n 两个参数,方法中打印一个m*n 的矩形
package mainimport "fmt"//编写结构体(MethodUtils),编程一个方法,方法不需要参数,在方法中打印一个10*8 的矩形,
//在main 方法中调用该方法。type MethodUtils struct {}//给MethodUtils编写方法
func (mu MethodUtils) Print(n1 int, n2 int) {for j := 0; j < n1; j++ {for i := 0; i < n2; i++ {fmt.Printf("*")}fmt.Println()}}func main() {var mu MethodUtilsmu.Print(8, 10)
}
  1. 编写一个方法算该矩形的面积(可以接收长len,和宽width), 将其作为方法返回值。在main方法中调用该方法,接收返回的面积值并打印。

  2. 编写方法:判断一个数是奇数还是偶数

  3. 根据行、列、字符打印对应行数和列数的字符,比如:行:3,列:2,字符*,则打印相应的效果

  4. 定义小小计算器结构体(Calcuator),实现加减乘除四个功能

    实现形式1:分四个方法完成

    实现形式2:用一个方法搞定

练习题

8.方法和函数区别

  1. 调用方式不一样

    函数的调用方式: 函数名(实参列表)

    方法的调用方式: 变量.方法名(实参列表)

  2. 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然

    package mainimport "fmt"type Person struct {Name string
    }func test01(p Person) {fmt.Println(p.Name)
    }func test02(p *Person) {fmt.Println(p.Name)
    }func main() {p := Person{"tom"}test01(p)test02(&p)
    }
    
  3. 对于方法(如struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以

  • 总结:

    1. 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
    2. 如果是和值类型,比如(p Person) , 则是值拷贝, 如果和指针类型,比如是(p *Person) 则是地址拷贝。

三、面向对象编程应用实例

1.步骤

  1. 声明(定义)结构体,确定结构体名
  2. 编写结构体的字段
  3. 编写结构体的方法

2.案例1

  1. 编写一个Student 结构体,包含name、gender、age、id、score 字段,分别为string、string、int、int、float64 类型。

  2. 结构体中声明一个say 方法,返回string 类型,方法返回信息中包含所有字段值。

  3. 在main 方法中,创建Student 结构体实例(变量),并访问say 方法,并将调用结果打印输出。

    package mainimport "fmt"/*
    1) 编写一个Student 结构体,包含name、gender、age、id、score 字段,分别为string、string、int、int、float64 类型。
    2) 结构体中声明一个say 方法,返回string 类型,方法返回信息中包含所有字段值。
    3) 在main 方法中,创建Student 结构体实例(变量),并访问say 方法,并将调用结果打印输出。
    */type Student struct {name   stringgender stringage    intid     intscore  float64
    }func (student *Student) say() string {infoStr := fmt.Sprintf("student信息 name=[%v] gender=[%v] age=[%v] id=[%v] score=[%v]",student.name, student.gender, student.age, student.id, student.score,)return infoStr
    }func main() {//测试//创建一个Student实例变量var stu = Student{name:   "tom",gender: "male",age:    18,id:     1000,score:  99.98,}fmt.Println(stu.say())
    }
    

3.案例2

  1. 编写一个Dog 结构体,包含name、age、weight 字段
  2. 结构体中声明一个say 方法,返回string 类型,方法返回信息中包含所有字段值。
  3. 在main 方法中,创建Dog 结构体实例(变量),并访问say 方法,将调用结果打印输出。

4.案例3

  1. 编程创建一个Box 结构体,在其中声明三个字段表示一个立方体的长、宽和高,长宽高要从终端获取

  2. 声明一个方法获取立方体的体积。

  3. 创建一个Box 结构体变量,打印给定尺寸的立方体的体积

5.案例4

  1. 一个景区根据游人的年龄收取不同价格的门票,比如年龄大于等于18,收费20 元,其它情况门票免费.

  2. 请编写Visitor 结构体,根据年龄段决定能够购买的门票价格并输出

四、创建结构体变量时指定字段值

  • 说明

    Golang 在创建结构体实例(变量)时,可以直接指定字段的值

  • 方式1

    package mainimport "fmt"type Stu struct {Name stringAge  int
    }func main() {//方式1//在创建结构体变量时,就直接指定字段的值var stu1 = Stu{"小明", 19}stu2 := Stu{"小米", 20}//在创建结构体变量时,把字段名和字段值写在一起var stu3 = Stu{Name: "jack",Age:  18,}var stu4 = Stu{Name: "mary",Age:  21,}fmt.Println(stu1, stu2, stu3, stu4) //{小明 19} {小米 20} {jack 18} {mary 21}
    }
  • 方式2

    package mainimport "fmt"type Stu struct {Name stringAge  int
    }func main() {//方式2 返回结构体的指针类型(!! !var stu5 = &Stu{"小王", 29}stu6 := &Stu{"小花", 28}//在创建结构体指针变量时,把字段名和字段值写在一起,这种写法,就不依赖字段的定义顺序.var stu7 = &Stu{Name: "小李",Age:  39,}stu8 := &Stu{ Name: "小李",Age:  39,}fmt.Println(stu5, stu6, stu7, stu8) //&{小王 29} &{小花 28} &{小李 39} &{小李 39}//&表示是地址符,使用*对原值进行取值fmt.Println(*stu5, *stu6, *stu7, *stu8) //{小王 29} {小花 28} {小李 39} {小李 39}}

五、工厂模式

1.说明

Golang 的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。

  • 看一个需求

    一个结构体的声明是这样的:

    package modeltype Student struct {Name string...
    }
    

因为这里的Student 的首字母S 是大写的,如果我们想在其它包创建Student 的实例(比如main 包),引入model 包后,就可以直接创建Student 结构体的变量(实例)。

但是问题来了,如果首字母是小写的,比如是type student struct {....} 就不不行了,怎么办 —> 工厂模式来解决.

2.工厂模式案例

  • 使用工厂模式实现跨包创建结构体实例(变量)的案例:

    如果model 包的结构体变量首字母大写,引入后,直接使用, 没有问题

    student.go

    package modeltype Student struct {Name  stringScore float64
    }
    

    main.go

    package mainimport ("demo/28demo/02/model""fmt"
    )func main() {//创建要给Student实例var stu = model.Student{Name:  "小明",Score: 86.25,}fmt.Println(stu)
    }
    
  • 如果model 包的结构体变量首字母小写,引入后,不能直接使用, 可以工厂模式解决

    student.go

    package modeltype student struct {Name  stringScore float64
    }//因为student结构体首字母是小写,因此是只能在mode1使用
    //我们通过工厂模式来解决func NewStudent(n string, s float64) *student { //定义一个公开的方法,使用一个方法来接收。并把值返回return &student{Name:  n,Score: s,}
    }
    

    main.go

    package mainimport ("demo/28demo/02/model""fmt"
    )func main() {//因为student结构体是首字母小写,我们可以通过工厂模式来解决var stu = model.NewStudent("tom", 88.8)fmt.Println(*stu)fmt.Printf("name=%v score=%v", stu.Name, stu.Score)
    }

3.思考题

思考一下,如果model 包的student 的结构体的字段Score 改成score,我们还能正常访问吗?又应该如何解决这个问题呢?

package modeltype student struct {Name  stringscore float64
}//如果score字段首字母小写,则,在其它包不可以直接方法,我们可以提供一个方法
func (s *student) GetScore() float64 {return s.score
}
package mainimport ("demo/28demo/02/model""fmt"
)func main() {//因为student结构体是首字母小写,我们可以通过工厂模式来解决var stu = model.NewStudent("tom", 88.8)fmt.Println(*stu)fmt.Printf("name=%v score=%v", stu.Name, stu.GetScore())
}

六、面向对象编程思想-抽象

1.抽象的介绍

我们在前面去定义一个结构体时候,实际上就是把一类事物的共有的属性(字段)和行为(方法)提取出来,形成一个物理模型(结构体)。这种研究问题的方法称为抽象。

package mainimport "fmt"//定义一个结构体
type Account struct {AccountNo stringPwd       stringBalance   float64
}//方法
//1.存款
func (account *Account) Deposite(useraccount string, pwd string, money float64) {// if useraccount != account.AccountNo {//    fmt.Println("你输入的账号不正确")//    return// }// //看下输入的密码是否正确// if pwd != account.Pwd {//     fmt.Println("你输入的密码不正确")//    return// }if money <= 0 {fmt.Println("你输入的金额不正确")return}account.Balance += moneyfmt.Println("存款成功")fmt.Printf("你的账号为=%v,你的余额为=%v\n", account.AccountNo, account.Balance)}//2.取款
func (account *Account) WithDraw(useraccount string, pwd string, money float64) {// if useraccount != account.AccountNo {//    fmt.Println("你输入的账号不正确")//    return// }// if pwd != account.Pwd {//     fmt.Println("你输入的密码不正确")//    return// }if money <= 0 || money > account.Balance { //取款小于等于0,你要取款的钱大于你存的钱fmt.Println("你输入的金额不正确")return}account.Balance -= moneyfmt.Println("取款成功")fmt.Printf("你的账号为=%v,你的余额为=%v\n", account.AccountNo, account.Balance)}//3.查询余额
func (account *Account) Query(useraccount string, pwd string) {// if useraccount != account.AccountNo {//  fmt.Println("你输入的账号不正确")//    return// }// if pwd != account.Pwd {//     fmt.Println("你输入的密码不正确")//    return// }fmt.Printf("你的账号为=%v,你的余额为=%v\n", account.AccountNo, account.Balance)
}func main() {account := Account{AccountNo: "icbc",Pwd:       "123456",Balance:   100.0,}var user stringvar password stringvar input stringfmt.Println("请输入账号:")fmt.Scanln(&user) //用户输入fmt.Println("请输入密码:")fmt.Scanln(&password)if user != account.AccountNo || password != account.Pwd {fmt.Println("您输入的账号或密码有误")return} else {fmt.Printf("请输入您要办理的业务:\n1.存款\n2.取款\n3.查看余额\n")fmt.Scanln(&input)switch input {case "1", "存款", "1.存款":account.Deposite(user, password, 123)case "2", "取款", "2.取款":account.WithDraw(user, password, 12.66)case "3", "查看余额", "3.查看余额":account.Query(user, password)default:fmt.Println("输入有误,请重新输入")}fmt.Println()}
}

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

1.基本介绍

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

2.封装介绍

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

3.封装的理解和好处

  1. 隐藏实现细节
  2. 提可以对数据进行验证,保证安全合理(Age)

4.如何体现封装

  1. 对结构体中的属性进行封装
  2. 通过方法,包实现封装

5.封装的实现步骤

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

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

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

    func (var 结构体类型名) SetXxx(参数列表) (返回值列表) {//加入数据验证的业务逻辑var.字段= 参数
    }
    
  4. 提供一个首字母大写的Get 方法(类似其它语言的public),用于获取属性的值

    func (var 结构体类型名) GetXxx() {return var.age;
    }
    
  • 特别说明:在Golang 开发中并没有特别强调封装,这点并不像Java. 所以提醒学过java 的朋友,不用总是用java 的语法特性来看待Golang, Golang 本身对面向对象的特性做了简化的.

6.快速入门案例

  • 案例

    请大家看一个程序(person.go),不能随便查看人的年龄,工资等隐私,并对输入的年龄进行合理的验证。设计: model(person.go) main 包(main.go 调用Person 结构体)

    person.go

    package modelimport "fmt"type person struct {Name stringage  int     //其他包不能直接访问 年龄sal  float64 //其他包不能直接访问 薪水
    }//写一个工厂模式的函数,相当于构造函数
    func NwePerson(name string) *person { //让小写的结构体可以被其他包引用return &person{Name: name,}
    }//为了访问age和sal 我们编写一 对SetXxx的方法和Getxx的方法
    func (p *person) SetAge(age int) { //使用指针赋值给结构体中的年龄 存入值if age > 0 && age < 150 {p.age = age} else {fmt.Println("年龄范围不正确")}
    }func (p *person) GetAge() int { //Get查看这个值 ,直接返回return p.age
    }func (p *person) SetSal(sal float64) { //存入值if sal > 3000 && sal < 30000 {p.sal = sal} else {fmt.Println("年龄范围不正确")}
    }func (p *person) GetSal() float64 { //Get查看sal这个值并直接返回给GetSal()return p.sal
    }
    

    main.go

    package mainimport ("demo/29demo/model""fmt"
    )func main() {p := model.NwePerson("小明") //声明model并把小明赋值给结构体person的namefmt.Println(*p)            //使用*取原值进行输出p.SetAge(18) //set存入age值p.SetSal(5000)fmt.Println(*p)fmt.Printf("name=%v age=%v sal=%v", p.Name, p.GetAge(), p.GetSal()) //调用方法并输出
    }
    

7.练习

  • 要求

    1. 创建程序,在model 包中定义Account结构体:在main 函数中体会Golang 的封装性。

    2. Account 结构体要求具有字段:账号(长度在6-10 之间)、余额(必须>20)、密码(必须是六

    3. 通过SetXxx 的方法给Account 的字段赋值。

    4. 在main 函数中测试

  • 代码

    account.go

    package modelimport "fmt"type account struct {user    string  //长度在6-10之间pwd     string  //必须是6位balance float64 //必须>20
    }//小写的结构体可以被其他包引用
    func NewAccount(user string, pwd string, balance float64) *account {fmt.Println(len(user))if len(user) < 6 || len(user) > 10 {fmt.Println("输入的范围有误,长度必须在6-10之间")return nil}if len(pwd) != 6 {fmt.Println("输入的范围有误,长度必须是6位")return nil}if balance < 20 {fmt.Println("输入的范围有误,余额(必须>20)")return nil}return &account{user:    user,pwd:     pwd,balance: balance,}
    }// func (a *account) SetUser(user string) {length := len(user)if 6 <= length && length <= 10 {a.user = user} else {fmt.Println("输入的范围有误,长度必须在6-10之间")}
    }func (a *account) SetPwd(pwd string) {length := len(pwd)if length == 6 {a.pwd = pwd} else {fmt.Println("输入的范围有误,长度必须是6位")}
    }func (a *account) SetBalance(balance float64) {if balance >= 20 {a.balance = balance} else {fmt.Println("输入的范围有误,余额(必须>20)")}
    }func (a *account) GetUser() string {return a.user
    }func (a *account) GetPwd() string {return a.pwd
    }func (a *account) GetBalance() float64 {return a.balance
    }
    

    main.go

    package mainimport ("demo/29demo/01/model""fmt"
    )/*
    1) 创建程序,在`model` 包中定义`Account`结构体:在`main` 函数中体会Golang 的封装性。
    2) `Account` 结构体要求具有字段:账号(长度在6-10 之间)、余额(必须>20)、密码(必须是六
    3) 通过SetXxx 的方法给Account 的字段赋值。
    4) 在main 函数中测试
    */func main() {account := model.NewAccount("小红帽", "123456", 123)if account != nil {fmt.Println("创建成功", *account)} else {fmt.Println("创建失败")}account.SetUser("小明")fmt.Println(*account)fmt.Println(account.GetUser())
    }
    

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

1.引出继承的必要性

一个小问题,看个学生考试系统的程序extends01.go,提出代码复用的问题

代码

package mainimport "fmt"//编写一个学生考试系统//小学生考试
type Pupil struct {Name  stringAge   intScore int
}//小学生考试
//显示学生信息
func (p *Pupil) ShowInfo() {fmt.Printf("学生姓名=%v 年龄=%v 成绩=%v\n", p.Name, p.Age, p.Score)
}//设置学生成绩
func (p *Pupil) SteScore(score int) {p.Score = score
}func (p *Pupil) Tesing() {fmt.Println("小学生正在考试中")
}//大学生考试
type Graduate struct {Name  stringAge   intScore int
}//显示学生信息
func (p *Graduate) ShowInfo() {fmt.Printf("学生姓名=%v 年龄=%v 成绩=%v\n", p.Name, p.Age, p.Score)
}//设置学生成绩
func (p *Graduate) SteScore(score int) {p.Score = score
}func (p *Graduate) Tesing() {fmt.Println("大学生正在考试中")
}func main() {//测试var pupil = &Pupil{Name: "tom",Age:  10,}pupil.Tesing()pupil.SteScore(89)pupil.ShowInfo()var graduate = &Graduate{Name: "mary",Age:  20,}graduate.Tesing()graduate.SteScore(76)graduate.ShowInfo()
}

对上面代码的小结

  1. Pupil 和Graduate 两个结构体的字段和方法几乎,但是我们却写了相同的代码, 代码复用性不强
  2. 出现代码冗余,而且代码不利于维护,同时也不利于功能的扩展
  3. 解决方法-通过继承方式来解决

2.继承基本介绍和示意图

继承可以解决代码复用,让我们的编程更加靠近人类思维。

当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体(比如刚才的Student),在该结构体中定义这些相同的属性和方法。

其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个Student 匿名结构体即可。[示意图]

在Golang 中,如果一个struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性。

3.嵌套匿名结构体的基本语法

type Goods struct {Name stringPrice int
}
type Book struct {Goods //这里就是嵌套匿名结构体GoodsWriter string
}

4.快速入门案例

  • 案例

    我们对extends01.go 改进,使用嵌套匿名结构体的方式来实现继承特性,请大家注意体会这样编程的好处

  • 代码

    package mainimport "fmt"type Student struct {Name  stringAge   intScore int
    }///
    //将Pupil和Graduate共有的方法绑定到*Student
    func (stu *Student) ShowInfo() {fmt.Printf("学生姓名=%v 年龄=%v 成绩=%v\n", stu.Name, stu.Age, stu.Score)}
    func (stu *Student) SteScore(score int) {stu.Score = score
    }///
    ///type Pupil struct {Student //嵌入Student匿名结构体
    }type Graduate struct {Student //嵌入Student匿名结构体
    }//保留特有的方法
    func (p *Pupil) Tesing() {fmt.Println("小学生正在考试中")
    }func (p *Graduate) Tesing() {fmt.Println("大学生正在考试中")
    }///func main() {//当我们对结构体嵌入了匿名结构体后使用方法会发生变化//方式1pupil := &Pupil{}pupil.Student.Name = "tom"pupil.Student.Age = 10pupil.Tesing()pupil.Student.SteScore(75)pupil.Student.ShowInfo()graduate := &Graduate{}graduate.Student.Name = "mary"graduate.Student.Age = 20graduate.Tesing()graduate.Student.SteScore(68)graduate.Student.ShowInfo()fmt.Println()//方式2......pupil.Name = "tom~"pupil.Age = 100pupil.Tesing()pupil.SteScore(750)pupil.ShowInfo()graduate.Name = "mary~"graduate.Age = 200graduate.Tesing()graduate.SteScore(680)graduate.ShowInfo()}
    

5.继承的深入讨论

  1. 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。【举例说明】

  2. 匿名结构体字段访问可以简化

package mainimport "fmt"type A struct { //公开Name string //公开age  int    //私有
}func (a *A) SayOk() {fmt.Println("A SayOk", a.Name)
}func (a *A) hello() {fmt.Println("A hello", a.age)
}type B struct {A
}func main() {var b Bb.A.Name = "tom"b.A.age = 19b.A.SayOk()b.A.hello()//上面写法可以简化b.Name = "smith"b.age = 20b.SayOk()b.hello()
}
  • 对上面的代码小结

    (1) 当我们直接通过b 访问字段或方法时,其执行流程如下比如b.Name

    (2) 编译器会先看b 对应的类型有没有Name, 如果有,则直接调用B 类型的Name 字段

    (3) 如果没有就去看B 中嵌入的匿名结构体A 有没有声明Name 字段,如果有就调用,如果没有继续查找…如果都找不到就报错

  1. 结构体匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分【举例说明】
package mainimport "fmt"type A struct { //公开Name string //公开age  int    //私有
}func (a *A) SayOk() {fmt.Println("A SayOk", a.Name)
}func (a *A) hello() {fmt.Println("A hello", a.age)
}///
type B struct {AName string
}func (b *B) SayOk() {fmt.Println("B SayOk", b.Name)
}///func main() {var b Bb.Name = "jack"    //使用就近原则,导入jackb.A.Name = "scott" //使用匿名结构体全路径b.age = 100b.SayOk()   //使用就近原则b.A.SayOk() //使用匿名结构体全路径b.hello()
}
  1. 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。【举例说明】
package mainimport "fmt"type A struct {Name stringAge  int
}type B struct {Name  stringScore float64
}//C同时嵌入了2个不同的结构体,此时就必须明确指定匿名结构体名字
type C struct {AB
}func main() {var c C//如果c没有Name字段,而A和B有Name, 这时就必须通过指定置名结构体名字来区分//所以 c.Name 就会包编译错误, 这个规则对方法也是一 样的!c.A.Name = "tom"fmt.Println(c.A.Name)}
  1. 如果一个struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字

    package mainimport "fmt"type A struct {Name stringAge  int
    }type D struct {a A //有名结构体 组合关系}func main() {如果D中是一个有名结构体,则访问有名结构体的字段时,就必须带上有名结构体的名字var d D//d.Name = "jack" //直接去d里面找Name,发现没有叫做Name的就直接报错,d.a.Name = "jack" //因为A是有名结构体,需要完整的路径}
  2. 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的

    package mainimport "fmt"type Goods struct {Name  stringPrice float64
    }type Brend struct {Name    stringAddrees string
    }type Tv1 struct {GoodsBrend
    }type Tv2 struct { //使用指针类型*Goods*Brend
    }func main() {//嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值tv1 := Tv1{Goods{"电视机001", 5999.09}, Brend{"海尔", "山东"}} //方式1fmt.Println(tv1)tv2 := Tv1{ //方式2Goods{Name:  "电视机002",Price: 5000.09,},Brend{Name:    "海尔",Addrees: "山东",},}fmt.Println(tv2)//使用指针tv3 := Tv2{&Goods{"电视机003", 6999.09}, &Brend{"创维", "河南"}}fmt.Println(*tv3.Goods, *tv3.Brend)tv4 := Tv2{&Goods{Name:  "电视机004",Price: 4000.99,},&Brend{Name:    "创维",Addrees: "河南",},}fmt.Println(*tv4.Goods, *tv4.Brend)
    }
    

6.练习

结构体的匿名字段是基本数据类型,如何访问, 下面代码输出什么

package mainimport "fmt"type Monster struct {Name stringAge  int
}type E struct {Monsterint     //匿名字段时基本数据类型n   int //如果有了1个int,后面int必须有名称
}func main() {var e Ee.Name = "牛魔王"e.Age = 500e.int = 20e.n = 10fmt.Println(e) //{{牛魔王 500} 20 10}
}

说明

  1. 如果一个结构体有 int 类型的匿名字段,就不能第二个。
  2. 如果需要有多个 int 的字段,则必须给 int 字段指定名

7.面向对象编程-多重继承

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

  • 案例

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

  • 多重继承细节说明

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

    2. 为了保证代码的简洁性,建议大家尽量不使用多重继承

九、接口(interface)

1.基本介绍

按顺序,我们应该讲解多态,但是在讲解多态前,我们需要讲解接口(interface),因为在Golang 中多态特性主要是通过接口来体现的

2.接口快速入门

这样的设计需求在Golang 编程中也是会大量存在的,前面说过,一个程序就是一个世界,在现实世界存在的情况,在程序中也会出现。我们用程序来模拟一下前面的应用场景。

  • 快速入门案例

    package mainimport "fmt"//声明一个接口
    type Usb interface {//声明了2个没有实现的方法Start()Stop()
    }//让手机Phone实现Usb接口的方法
    type Phone struct {}func (p Phone) Start() {fmt.Println("启动手机...")
    }
    func (p Phone) Stop() {fmt.Println("关闭手机...")
    }//让相机Camera实现Usb接口的方法
    type Camera struct {}func (c Camera) Start() {fmt.Println("启动相机...")
    }func (c Camera) Stop() {fmt.Println("关闭相机...")
    }//计算机
    type Computer struct{}//编写一个方法Working,接收一个usb接口类型变量
    //只要是实现了Usb接口 (所谓实现Usb接口,就是指实现了Usb接口声明所有方法)
    func (c Computer) Working(usb Usb) { //2.接收到phone//通过usb接口变量来调用start和stop方法usb.Start() //3.寻找关于phone的Start方法 输出:启动手机...usb.Stop()  //4.寻找关于phone的Stop方法 输出:关闭手机...// //上面等同于下面这个// phone := Phone{}// phone.Start()
    }func main() {//测试,先创建结构体变量computer := Computer{}phone := Phone{}camera := Camera{}computer.Working(phone) //1.传入phone参数fmt.Println()computer.Working(camera)
    }
    

3.基本语法

interface 类型可以定义一组方法,但是这些不需要实现。并且interface 不能包含任何变量。到某个自定义类型(比如结构体Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)。

  1. 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想。
  2. Golang 中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有implement 这样的关键字

4.接口使用的应用场景

5.注意事项和细节

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

    package mainimport "fmt"type AInterface interface { //定义一个接口Say()
    }type Stu struct { //定义一个结构体Name string
    }func (stu Stu) Say() { //定义一个方法体fmt.Println("Stu Say()")
    }func main() { //调用接口var stu Stu //结构体变量var a AInterface = stustu.Say()a.Say()
    }
    
  2. 接口中所有的方法都没有方法体,即都是没有实现的方法。

  3. 在Golang 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口。

  4. 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型

  5. 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。

  6. 一个自定义类型可以实现多个接口

    package mainimport "fmt"type AInterface interface { //定义一个接口Say()
    }type BInterface interface { //定义一个接口Hello()
    }type Monster struct { //定义struct结构体
    }func (m Monster) Hello() { //定义Hello并调用Monster结构体fmt.Println("Monster Hello()")
    }func (m Monster) Say() {fmt.Println("Monster Say()")
    }func main() { //调用接口//Monster实现 了AInterface 和BInterfacevar monster Monstervar a AInterface = monstervar b BInterface = monstera.Say()b.Hello()
    }
    
  7. Golang 接口中不能有任何变量

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

    package maintype BInterface interface {test01()
    }type CInterface interface {test02()
    }type AInterface interface { //如果要实现A接口,必须把A接口所有的反法全部实现BInterfaceCInterfacetest03()
    }type Stu struct {}//如果需要实现AInterface,就需要将BInterface CInterface的方法都实现
    func (stu Stu) test01() {} //缺一不可func (stu Stu) test02() {} //缺一不可func (stu Stu) test03() {} //缺一不可//如果需要实现AInterface,就需要将BInterface CInterface的方法都实现
    func main() {var stu Stuvar a AInterface = stuvar b BInterface = stua.test01()b.test01()
    }
    
  9. interface 类型默认是一个指针(引用类型),如果没有对interface 初始化就使用,那么会输出nil

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

    package mainimport "fmt"type T interface{} //定义一个空接口type Stu struct { //定义一个结构体
    }func main() {var nume float64 = 8.0//方式1var stu Stu    //调用结构体var t1 T = stu //声明位空接口类型并调用结构体t1 = numefmt.Println(t1)//方式2var t2 interface{} = stu //定义一个空接口类型,并传给t2t2 = numefmt.Println(t2)
    }
    

6.接口编程的最佳实践

  • 实现对Hero 结构体切片的排序: sort.Sort(data Interface)

    package mainimport ("fmt""math/rand""sort"
    )// 实现对Hero 结构体切片的排序: sort.Sort(data Interface)//1.声明Hero结构体
    type Hero struct {Name stringAge  int
    }//2.声明一个Hero结构体切片类型
    type HeroSlice []Hero //声明Hero切片//3.interface接口
    func (hs HeroSlice) Len() int {return len(hs) //遍历数组的数量,并返回
    }//Less方法就是决定你使用什么标准进行排序
    //3.1.按Hero年龄进行从小到大进行排序
    func (hs HeroSlice) Less(i, j int) bool {return hs[i].Age < hs[j].Age //判断大小,返回bool类型//修改对Name的排序//return hs[i].Name < hs[j].Name
    }//3.2把值进行交换
    func (hs HeroSlice) Swap(i, j int) {hs[i], hs[j] = hs[j], hs[i]
    }func main() {//先定义一个数组/切片var intSlice = []int{0, -1, 10, 7, 90}//要求对interface进行排序fmt.Println(intSlice)sort.Ints(intSlice)fmt.Println(intSlice)//========================================//对结构体切片进行排序var heros HeroSlice//生成英雄herofor i := 0; i < 10; i++ {hero := Hero{ //调用Hero结构体,传入英雄和英雄的年龄Name: fmt.Sprintf("英雄%d", rand.Intn(100)), //使用rand.Intn生成0到100的伪随机数Age:  rand.Intn(100),                      //使用rand.Intn生成0到100的伪随机数}heros = append(heros, hero) //使用append对切片进行动态追加,并赋值给heros}//看看排序前的顺序fmt.Println(heros)for _, v := range heros { //使用range对heros进行遍历fmt.Println(v)}fmt.Println("-----排序后-----")//调用sort.Sortsort.Sort(heros)//看看排序后的顺序for _, v := range heros {fmt.Println(v)}
    }
    
  • 接口编程练习

    //1.声明Student 结构体
    type Student struct{Name stringAge intScore float64
    }
    //将Student 的切片,安Score 从大到小排序!!
    

7.实现接口vs 继承

  • 大家听到现在,可能会对实现接口和继承比较迷茫了, 这个问题,那么他们究竟有什么区别呢

    • 接口是对继承的补充

  • 代码

    package mainimport ("fmt"
    )/
    //猴子Monkey结构体
    type Monkey struct {Name string
    }func (ithis *Monkey) Climbing() { //猴子Monkey的方法fmt.Println(ithis.Name, "生来会爬树")
    }
    //
    //小猴子LittleMonkey结构体
    type LittleMonkey struct {Monkey //继承猴子Monkey
    }
    //
    //声明鸟的能力BirdAble接口
    type BirdAble interface {Flying() //飞翔Flying方法
    }//让小猴子LittleMonkey实现鸟的能力BirdAble
    func (ithis *LittleMonkey) Flying() {fmt.Println(ithis.Name, "通过学习,会飞翔了")
    }
    //
    //声明鱼的能力FishAble接口
    type FishAble interface {Swimming() //游泳Swimming方法
    }//让小猴子LittleMonkey实现鱼的能力FishAble
    func (ithis *LittleMonkey) Swimming() {fmt.Println(ithis.Name, "学会了游泳")
    }
    /func main() {//创建一个LittleMonkey实例monkey := LittleMonkey{Monkey{Name: "悟空",},}monkey.Climbing() //调用Monkey的方法monkey.Flying()   //调用鸟的能力BirdAble接口,实现飞翔Flying方法monkey.Swimming() //调用鱼的能力BirdAble接口,实现游泳Swimming方法
    }
    
  • 对上面代码的小结

    1. 当A 结构体继承了B 结构体,那么A 结构就自动的继承了B 结构体的字段和方法,并且可以直接使用
    2. 当A 结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此我们可以认为:实现接口是对继承机制的补充.
  • 实现接口可以看作是对继承的一种补充

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

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

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

  • 接口比继承更加灵活

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

    继承:Person 继承 学生Student :学生继承了人

    接口:鸟的能力BirdAble 小猴子LittleMonkey:小猴子像鸟一样飞翔

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

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

1.基本介绍

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

2.快速入门

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

package mainimport "fmt"//声明一个接口
type Usb interface {//声明了2个没有实现的方法Start()Stop()
}//让手机Phone实现Usb接口的方法
type Phone struct {}func (p Phone) Start() {fmt.Println("启动手机...")
}
func (p Phone) Stop() {fmt.Println("关闭手机...")
}//让相机Camera实现Usb接口的方法
type Camera struct {}func (c Camera) Start() {fmt.Println("启动相机...")
}func (c Camera) Stop() {fmt.Println("关闭相机...")
}//计算机
type Computer struct{}//编写一个方法Working,接收一个usb接口类型变量
//只要是实现了Usb接口 (所谓实现Usb接口,就是指实现了Usb接口声明所有方法)
func (c Computer) Working(usb Usb) { //2.接收到phone//通过usb接口变量来调用start和stop方法usb.Start() //3.寻找关于phone的Start方法 输出:启动手机...usb.Stop()  //4.寻找关于phone的Stop方法 输出:关闭手机...// //上面等同于下面这个// phone := Phone{}// phone.Start()
}func main() {//测试,先创建结构体变量computer := Computer{}phone := Phone{}camera := Camera{}computer.Working(phone) //1.传入phone参数fmt.Println()computer.Working(camera)
}

3.接口体现多态的两种形式

  • 多态参数

    在前面的Usb 接口案例,Usb usb ,即可以接收手机变量,又可以接收相机变量,就体现了Usb 接口多态。

  • 多态数组

    演示一个案例:给Usb 数组中,存放Phone 结构体和Camera 结构体变量

    案例说明:

    package mainimport "fmt"///
    //声明一个接口
    type Usb interface {//声明了2个没有实现的方法Start()Stop()
    }//////
    //让手机Phone实现Usb接口的方法
    type Phone struct {Name string
    }func (p Phone) Start() {fmt.Println("启动手机...")
    }
    func (p Phone) Stop() {fmt.Println("关闭手机...")
    }func (p Phone) Call() {fmt.Println("可以使用电话...")
    }//////
    //让相机Camera实现Usb接口的方法
    type Camera struct {Name string
    }func (c Camera) Start() {fmt.Println("启动相机...")
    }func (c Camera) Stop() {fmt.Println("关闭相机...")
    }///type Computer struct {}func (computer Computer) Working(usb Usb) {usb.Start()usb.Stop()//如果usb是指向Phone结构体变量,则还需要调用Call方法//类型断言if phone, ok := usb.(Phone); ok { //判断usb的参数是否为phone,phone.Call() //当等于时就执行Call()方法,否则跳过}
    }func main() {//定义一个Usb接口数组,可以存放Phone和Camear的结构体变量//这里就体现出多态数组var usbArr [3]Usb   //声明一个数组,里面可以存放3个元素fmt.Println(usbArr) //[<nil> <nil> <nil>]//使用接口实现多个数据类型usbArr[0] = Phone{"vivo"}usbArr[1] = Phone{"小米"}usbArr[2] = Camera{"索尼"}fmt.Println(usbArr) //多态数组[{vivo} {小米} {索尼}]fmt.Println()var computer Computerfor _, v := range usbArr {computer.Working(v)fmt.Println()}
    }
    

4.类型断言

1.基本介绍

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

  • 对上面代码的说明:

    在进行类型断言时,如果类型不匹配,就会报panic, 因此进行类型断言时,要确保原来的空接口指向的就是断言的类型.

  • 如何在进行断言时,带上检测机制,如果成功就ok,否则也不要报panic

    package mainimport "fmt"func main() {//带检测的类型断言var x interface{}var b float32 = 1.1x = by, ok := x.(float64) //返回ok是否为ture或falseif ok {              //等同于ok == turefmt.Println("convert succes")fmt.Printf("y的类型是%T 值是%v", y, y)} else {fmt.Println("convert fail")}fmt.Println("继续执行")
    }
    

2.类型断言的最佳实践

  • 在前面的Usb 接口案例做改进:

    给Phone 结构体增加一个特有的方法call(), 当Usb 接口接收的是Phone 变量时,还需要调用call方法

    package mainimport "fmt"///
    //声明一个接口
    type Usb interface {//声明了2个没有实现的方法Start()Stop()
    }//////
    //让手机Phone实现Usb接口的方法
    type Phone struct {Name string
    }func (p Phone) Start() {fmt.Println("启动手机...")
    }
    func (p Phone) Stop() {fmt.Println("关闭手机...")
    }func (p Phone) Call() {fmt.Println("可以使用电话...")
    }//////
    //让相机Camera实现Usb接口的方法
    type Camera struct {Name string
    }func (c Camera) Start() {fmt.Println("启动相机...")
    }func (c Camera) Stop() {fmt.Println("关闭相机...")
    }///type Computer struct {}func (computer Computer) Working(usb Usb) {usb.Start()usb.Stop()//如果usb是指向Phone结构体变量,则还需要调用Call方法//类型断言if phone, ok := usb.(Phone); ok { //判断usb的参数是否为phone,phone.Call() //当等于时就执行phone.Call()方法,否则跳过}
    }func main() {//定义一个Usb接口数组,可以存放Phone和Camear的结构体变量//这里就体现出多态数组var usbArr [3]Usb   //声明一个数组,里面可以存放3个元素fmt.Println(usbArr) //[<nil> <nil> <nil>]//使用接口实现多个数据类型usbArr[0] = Phone{"vivo"}usbArr[1] = Phone{"小米"}usbArr[2] = Camera{"索尼"}fmt.Println(usbArr) //多态数组[{vivo} {小米} {索尼}]fmt.Println()var computer Computerfor _, v := range usbArr {computer.Working(v)fmt.Println()}
    }
    
  • 写一函数,循环判断传入参数的类型:

    package mainimport "fmt"//编写一个函数,可以判断输入的参数是什么类型
    func TyeepJudge(items ...interface{}) { //定义一个...根据大小自动扩容的空接口for index, x := range items { //遍历items ,index为下标,x为值index++switch x.(type) { //固定写法,传入遍历后的值对类型进行判断case bool:fmt.Printf("第%v个参数是bool类型,值是%v\n", index, x)case float32:fmt.Printf("第%v个参数是float32类型,值是%v\n", index, x)case float64:fmt.Printf("第%v个参数是float64类型,值是%v\n", index, x)case int, int16, int32, int64:fmt.Printf("第%v个参数是 整数 类型,值是%v\n", index, x)case string:fmt.Printf("第%v个参数是string类型,值是%v\n", index, x)default:fmt.Printf("第%v个参数类型不确定,值是%v\n", index, x)}}
    }func main() {var n1 float32 = 1.2var n2 float64 = 1.43var n3 int32 = 43var n4 string = "tom"n5 := "北京"n6 := 123TyeepJudge(n1, n2, n3, n4, n5, n6)
    }
    
  • 在前面代码的基础上,增加判断Student 类型和*Student 类型

    package mainimport "fmt"//1.自定义一个类型
    type Student struct{}//编写一个函数,可以判断输入的参数是什么类型
    func TyeepJudge(items ...interface{}) { //定义一个...根据大小自动扩容的空接口for index, x := range items { //遍历items ,index为下标,x为值index++switch x.(type) { //固定写法,传入遍历后的值对类型进行判断case bool:fmt.Printf("第%v个参数是bool类型,值是%v\n", index, x)case float32:fmt.Printf("第%v个参数是float32类型,值是%v\n", index, x)case float64:fmt.Printf("第%v个参数是float64类型,值是%v\n", index, x)case int, int16, int32, int64:fmt.Printf("第%v个参数是 整数 类型,值是%v\n", index, x)case string:fmt.Printf("第%v个参数是string类型,值是%v\n", index, x)case Student: //可以直接进行判断fmt.Printf("第%v个参数是Student类型,值是%v\n", index, x)case *Student: //可以直接进行判断fmt.Printf("第%v个参数是*Student类型,值是%v\n", index, x)default:fmt.Printf("第%v个参数类型不确定,值是%v\n", index, x)}}
    }func main() {var n1 float32 = 1.2var n2 float64 = 1.43var n3 int32 = 43var n4 string = "tom"n5 := "北京"n6 := 123stu1 := Student{}  //2.自定义类型stu2 := &Student{} //3.指针类型TyeepJudge(n1, n2, n3, n4, n5, n6, stu1, stu2) //4.传入参数
    }
    

章节目录

【Golang第1~3章:基础】如何安装golang、第一个GO程序、golang的基础

【Golang第4章:函数】Golang包的引用,return语句、指针、匿名函数、闭包、go函数参数传递方式,golang获取当前时间

【Golang第5章:数组与切片】golang如何使用数组、数组的遍历和、使用细节和内存中的布局;golang如何使用切片,切片在内存中的布局

【Golang第6章:排序和查找】golang怎么排序,golang的顺序查找和二分查找,go语言中顺序查找二分查找介绍和案例

【Golang第7章:map】go语言中map的基本介绍,golang中map的使用案例,go语言中map的增删改查操作,go语言对map的值进行排序

【Golang第8章:面向对象编程】Go语言的结构体是什么,怎么声明;Golang方法的调用和声明;go语言面向对象实例,go语言工厂模式;golang面向对象的三大特性:继承、封装、多态

【Golang第9章:项目练习】go项目练习家庭收支记账软件项目、go项目练习客户管理系统项目

【Golang第10章:文件操作】GO语言的文件管理,go语言读文件和写文件、GO语言拷贝文件、GO语言判断文件是否存在、GO语言Json文件格式和解析

【Golang第11章:单元测试】GO语言单元测试

【Golang第12章:goroutine协程与channel管道】GO语言goroutine协程和channel管道的基本介绍、goroutine协

【Golang第8章:面向对象编程】Go语言的结构体是什么,怎么声明;Golang方法的调用和声明;go语言面向对象实例,go语言工厂模式;golang面向对象的三大特性:继承、封装、多态相关推荐

  1. 面向对象之三大特性:继承,封装,多态

    python面向对象的三大特性:继承,封装,多态. 1. 封装: 把很多数据封装到⼀个对象中. 把固定功能的代码封装到⼀个代码块, 函数, 对象, 打包成模块. 这都属于封装的思想. 具体的情况具体分 ...

  2. Swift学习 OOP三大特性:继承、多态、封装

    先看个例子  从上面的例子可以总结那么一句话:"学生是人".也就是Student类继承People类.简而言之,学生是人,这句话是说得通的,但是"人是学生"这句 ...

  3. c语言字符数组赋值_C语言关于结构体字符成员元素赋值的方法

    C语言部分有些容易忘记的内容,这样贴在这里方便大家学习和查阅 本章节里面有结构体和数组长度的问题. 欢迎大家来点评 #include #include //结构体 struct student{ in ...

  4. 面向对象的三大特性————继承,多态

    1,继承:继承是一种创建新类的方式,在python中,新建的类可以继承一个或多个父类,父类又可称为基类或超类,新建的类称派生类或子类. 一个类可以被多个类继承,一个类可以继承多个父类. 没有继承父类默 ...

  5. C语言结构体自动初始化实现,C语言中结构体(struct)的几种初始化方法

    本文给大家总结的struct数据有3种初始化方法 1.顺序 2.C风格的乱序 3.C++风格的乱序 下面通过示例代码详细介绍这三种初始化方法. 1)顺序 这种方法很常见,在一般的介绍C的书中都有介绍. ...

  6. c++ 结构体初始化_单片机C语言 - 基于结构体的面向对象编程技巧

    单片机C语言 - 基于结构体的面向对象编程技巧 一.面向对象 面向对象是软件开发方法,是相对于面向过程来讲的.通过把数据与方法组织为一个整体来看待,从更高的层次来进行系统建模,更贴近事物的自然运行模式 ...

  7. 【Go语言】【13】再谈GO语言的结构体

    本文从如下四个方面再领着大家认识结构体 匿名结构体和匿名成员的结构体 值传递和引用传递 再谈嵌套结构体 面向对象 1.匿名结构体和匿名成员的结构体 如上篇所述,一个结构体需要先声明,再初始化,最后把初 ...

  8. c语言如何将值赋给结构体指针,C语言给结构体指针赋值

    <C语言给结构体指针赋值>由会员分享,可在线阅读,更多相关<C语言给结构体指针赋值(6页珍藏版)>请在人人文库网上搜索. 1.指向结构体的指针 在C语言中几乎可以创建指向任何类 ...

  9. Java继承_Hachi君浅聊Java三大特性之 封装 继承 多态

    Hello,大家好~我是你们的Hachi君,一个来自某学院的资深java小白.最近利用暑假的时间,修得满腔java语言学习心得.今天小宇宙终于要爆发了,决定在知乎上来一场根本停不下来的Hachi君个人 ...

最新文章

  1. 第八章 泛型程序设计
  2. linux两个文件修改主机名
  3. 熟悉的亲切-老外婆教做的豌豆蔬菜汤
  4. mysql數據庫的增刪改查_MySQL數據庫之基礎增刪改查操作
  5. MySQL设置表的字符编码为utf-8
  6. S5PV210之GPIO模拟I2c时序之pcf8591与at24xx linux3.0.8驱动
  7. 知乎超高赞:见识多的人,平时都在看些什么?
  8. 监督学习 | 线性回归 之正则线性模型原理及Sklearn实现
  9. Python面向对象基础一
  10. OpenGL:绘制太阳地球例子
  11. 视频PPT互动问答丨数据驱动的业务实践专题
  12. 利用UTL_FILE包实现文件I/O操作
  13. 3 天开发物联网应用!腾讯云 IoT 超级小程序来了
  14. python开发环境anaconda3_使用Anaconda3配置多版本Python虚拟开发环境
  15. 系统学习NLP(八)--中文分词整理
  16. 一步步学习SPD2010--第十四章节--在Web页面使用控件(2)--使用标准ASP.NET服务器控件...
  17. 图书速读 | 一分钟读完《如何成为学习高手》
  18. ros中odometry数据生成方式与分发去向
  19. C. DZY Loves Fibonacci Numbers(线段树fibonacci)
  20. printf 函数使用 可变参数函数实现原理

热门文章

  1. vscode开发vue项目页面修改保存时不自动编译的问题解决办法
  2. 内容都是XXXXX 如何把他们批量的插入表中
  3. 自动化测试平台化[v1.0.0][自动化测试基本需求]
  4. 中文姓名转全拼音,包括多音姓
  5. 我写了一个套路,助你随心所欲运用二分搜索
  6. 自动驾驶平台Apollo 5.5阅读手记:Cyber RT中的任务调度
  7. 如何实时计算日累计逐单资金流
  8. 用GO语言编写一个简单的区块链
  9. mysql增加数据 条件,mysql根据条件决定是否插入数据
  10. 女大学生的280块川西环游功略(含帐单)