欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

go语言系列-面向对象编程

程序员文章站 2022-04-15 22:05:38
[TOC] 面向对象编程 结构体 一个程序就是一个世界,有很多对象(变量) Go也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以说Go支持面向对象编程特性是比较准确的 Go没有类(class),Go语言的结构体(struct)和其它编程语言的类(class ......

目录

面向对象编程

结构体

一个程序就是一个世界,有很多对象(变量)

go也支持面向对象编程(oop),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以说go支持面向对象编程特性是比较准确的

go没有类(class),go语言的结构体(struct)和其它编程语言的类(class)有同等的地位,可以理解go是基于struct来实现oop特性的。

go面向对象编程非常简洁,去掉了传统oop语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等等

go仍然有面向对象编程的继承、封装和多态的特性,只是实现的方式和其它oop语言不一样,比如继承:go没有extends关键字,继承是通过匿名字段来实现

  1. go面向对象(oop)很优雅,oop本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,非常灵活。后面会充分体会这个特点。也就是说在go中面向接口编程是非常重要的特性

结构体与结构体变量(实例/对象)的关系示意图

go语言系列-面向对象编程

*注意:从猫结构体到变量,就是创建一个cat结构体变量,也可以说是定义cat结构体变量*

对上图的说明

​ 将一类事物的特性提取出来(比如猫类),形成一个新的数据类型,就是一个结构体

​ 通过这个结构体,我们可以创建多个变量(实例/对象)

​ 事物可以猫类,也可以是person,fish 或是某个工具类

基本语法
type 结构体名称 struct {
	field1 type
	field2 type
}


type cat struct {
	name string
	age int
	color string
	hobby string
}

func main()  {
	//创建一个cat的变量
	var cat1 cat
	cat1.name = "小白"
	cat1.age = 3
	cat1.color = "白色"
	cat1.hobby = "吃 <·)))><<"
	fmt.println("cat1 = ", cat1)
	fmt.println("猫猫的信息如下:")
	fmt.println("name = ", cat1.name)
	fmt.println("age = ", cat1.age)
	fmt.println("color = ", cat1.color)
	fmt.println("hobby = ", cat1.hobby)
}
//cat1 =  {小白 3 白色 吃 <·)))><<}
//猫猫的信息如下:
//name =  小白
//age =  3
//color =  白色
//hobby =  吃 <·)))><<

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

结构体是自定义的数据类型,代表一类事物

结构体变量(实例)是具体的,实际的,代表一个具体变量

结构体变量(实例)在内存的布局【重要】

go语言系列-面向对象编程

字段/属性

基本介绍

​ 1) 从概念或叫法上看:结构体字段 = 属性 = field

​ 2) 字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。比如我们前面定义猫结构体的name string 就是属性

注意事项和细节说明

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

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

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

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

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

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

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

func main()  {
   //定义结构体变量
   var p1 person
   fmt.println(p1)

   if p1.ptr == nil {
      fmt.println("ok1")
   }

   if p1.slice == nil {
      fmt.println("ok2")
   }

   if p1.map1 == nil {
      fmt.println("ok3")
   }

   //使用slice,再次说明,一定要make
   p1.slice = make([]int, 10)
   p1.slice[0] = 100
   //使用map,一定要先make
   p1.map1 = make(map[string]string)
   p1.map1["key1"] = "tom"
   fmt.println(p1)
}
//输出:{ 0 [0 0 0 0 0] <nil> [] map[]}
//ok1
//ok2
//ok3
//{ 0 [0 0 0 0 0] <nil> [100 0 0 0 0 0 0 0 0 0] map[key1:tom]}
  1. 不同结构体变量的字段是独立的,互不影响,一个结构体变量字段的更改,不影响另外一个,结构体是值类型
type monster struct {
   name string
   age int
}
func main(){
   var monster1 monster
   monster1.name = "牛魔王"
   monster1.age = 500

   monster2 := monster1 //结构体是值类型,默认为值拷贝
   monster2.name = "青牛精"
   fmt.println("monster1 = ",monster1)
   fmt.println("monster2 = ",monster2)
}
//输出:monster1 =  {牛魔王 500}
//monster2 =  {青牛精 500}

go语言系列-面向对象编程

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

方式1:直接声明   var person person
type cat struct {
	name string
	age int
	color string
	hobby string
}

func main()  {
	//创建一个cat的变量
	var cat1 cat
	cat1.name = "小白"
	cat1.age = 3
	cat1.color = "白色"
	cat1.hobby = "吃 <·)))><<"
  
方式2:{}         var person person = person{}  
type monster struct {
   name string
   age int
}
func main(){
  //{}
   p := monster{"zisefeizhu",21}
   fmt.println(p)
   //输出:{zisefeizhu 21}
}  
  
方式3: &        var person *person = new (person)
type monster struct {
   name string
   age int
}
func main(){
   //方式3
   //案例:var person *person = new (person)
   var p3 *monster = new(monster)
   //因为p3是一个指针,因此标准的给字段赋值方式
   //(*p3).name = "smith" 也可以这样写 p3.name = "smith"

   //原因:go的设计者为了程序员使用方便,底层会对p3.name = "smith"进行处理
   //会给p3加上取值运算(*p3).name = "smith"
   (*p3).name = "smith"
   p3.name = "john"

   (*p3).age = 30
   p3.age = 100
   fmt.println(*p3)  //{john 100}
}  

方式4: {}           var person *person = &person{}
type monster struct {
   name string
   age int
}
func main(){
   //方式4 - {}
   //var person *person = &person{}
   //下面的语句,也可以直接给字符赋值
   //var person *person = &person{"mary",60}
   var person *monster = &monster{}
   //因为person是一个指针,因此标准的访问字段的方法
   //(*person).name = "scott"
   //go的设计者为了程序使用方便,也可以person.name = "scott"
   //原因和上面一样,底层会对person.name = "scott" 进行处理,会加上(*person)
   (*person).name = "scott"
   person.name = "scott ~"

   (*person).age = 88
   person.age = 10
   fmt.println(*person)  //{scott ~ 10}
}  

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

struct类型的内存分配机制

定义一个person结构体(包括 名字,年龄)

type person struct {
   name string
   age int
}

func main()  {
   var p1 person
   p1.age = 10
   p1.name = "小明"
   var p2 person = p1

   fmt.println(p2.age)
   p2.name = "tom"
   fmt.printf("p2.name = %v p1.name = %v",p2.name,p1.name)
}
//输出:10
//p2.name = tom p1.name = 小明


变量总是存在内存中的,那么结构体变量在内存中究竟是怎样存在的?

画一个图说明:结构体变量在内存中如何存在
go语言系列-面向对象编程
看下面代码,分析原因

type person struct {
   name string
   age int
}

func main()  {
   var p1 person
   p1.age = 10
   p1.name = "小明"
   var p2 *person = &p1  //这里是关键 --> 画图示意图

   fmt.println((*p2).age)  //10
   fmt.println(p2.age)  //10
   p2.name = "tom ~"
   fmt.printf("p2.name = %v p1.name = %v \n",p2.name,p1.name)  //p2.name = tom ~ p1.name = tom ~
   fmt.printf("p2.name = %v p1.name = %v \n",(*p2).name,p1.name)  //p2.name = tom ~ p1.name = tom ~

   fmt.printf("p1的地址%p\n",&p1)  //p1的地址0xc00004a420
   fmt.printf("p2的地址%p p2的值%p\n",&p2, p2)  //p2的地址0xc000080018 p2的值0xc00004a420
   fmt.println(p2.age)  //10
   p2.name = "tom"
   fmt.printf("p2.name = %v p1.name = %v",p2.name,p1.name) //p2.name = tom p1.name = tom
}

go语言系列-面向对象编程
go语言系列-面向对象编程

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

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

//结构体
type point struct {
   x int
   y int
}
//结构体
type rect struct {
   leftup, rightdown point
}
//结构体
type rect2 struct {
   leftup, rightdown *point
}

func main()  {
   r1 := rect{point{1,2},point{3,4}}
   //r1有四个int,在内存中是连续分布
   //打印地址
   fmt.printf("r1.leftup.x 地址=%p r1.leftup.y 地址=%p r1.rightdown.x 地址=%p r1.rightdown.y 地址=%p\n" ,
      &r1.leftup.x, &r1.leftup.y, &r1.rightdown.x, &r1.rightdown.y) 
   //r.leftup.x 地址=0xc000052140 r1.leftup.y 地址=0xc000052148 r1.rightdown.x 地址=0xc000052150 r1.rightdown.y 地址=0xc000052158
   
   //r2有两个*point类型,这两个*point类型的本身地址也是连续的
   //但是它们指向的地址不一定是连续
   r2 := rect2{&point{10,20},&point{30,40}}
   //打印地址
   //打印地址
   fmt.printf("r2.leftup 本身地址 = %p r2.rightdown 本身地址 = %p \n",
      &r2.leftup, &r2.rightdown)
   //r2.leftup 本身地址 = 0xc0000401c0 r2.rightdown 本身地址 = 0xc0000401c8 
   
   //它们指向的地址不一定是连续...  这个要看系统在运行时是如何分配
   fmt.printf("r2.leftup 指向地址 = %p r2.rightdown 指向地址 = %p \n",
      r2.leftup, r2.rightdown)
   //r2.leftup 指向地址 = 0xc000054090 r2.rightdown 指向地址 = 0xc0000540a0 
}

对应的分析图
go语言系列-面向对象编程
2)结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)

type a struct {
   num int
}
type b struct {
   num int
}
func main()  {
   var a a
   var b b
   a = a(b) //? 可以转换,但是有要求,就是结构体的字段要完全一样(包括:名字、个数和类型)
   fmt.println(a, b)  //{0} {0}
}
  1. 结构体进行type重新定义(相当于取别名),go认为是新的数据类型,但是相互间可以强转
    go语言系列-面向对象编程
  2. struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用就是序列化和反序列化

序列化的常见使用
go语言系列-面向对象编程

import (
	"encoding/json"
	"fmt"
)

type monster struct {
	name string `json:"name"`  // `json:"name"` 就是struct tag
	age int `json:"age"`
	skill string `json:"skill"`
}

func main()  {
	//1.创建一个monster变量
	monster := monster{"牛魔王",500,"芭蕉扇"}
	//2.将monster变量序列化为json格式字串
	// json.marshal 函数中使用反射,这里只是用一下反射,在后面会详细介绍
	jsonstr, err := json.marshal(monster)
	if err != nil {
		fmt.println("json 处理错误",err)
	}
	fmt.println("jsonstr",string(jsonstr))  //jsonstr {"name":"牛魔王","age":500,"skill":"芭蕉扇"}
}

方法

在某些情况下,需要声明(定义)方法。比如person结构体:除了有一些字段外(年龄,姓名...),person结构体还有一些行为比如:可以说话、跑步...,通过学习,还可以做算术题。这时就要用方法才能完成

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

方法的声明和调用

type a struct {
	num int
}
func (a a)test(){
	fmt.println(a.num)
}

对上面的语法的说明
func (a a)test() {} 表示a结构体有 - 方法,方法名为test
(a a)体现test方法是和a类型绑定的

type person struct {
   name string
}
//给person类型绑定 -- 方法
func (p person) test() {
   fmt.println("test() name = ", p.name)  //test() name =  tom
}
func main()  {
   var p person
   p.name = "tom"
   p.test() // 调用方法
}

对上面的总结

  1. test方法和person类型绑定

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

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

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

type person struct {
   name string
}
//给person类型绑定 -- 方法
func (person person) test() {
   fmt.println("test() name = ", person.name)  //test() name =  tom
}
func main()  {
   var p person
   p.name = "tom"
   p.test() // 调用方法
}

快速入门

  1. 给person结构体添加speak方法,输出xxx是一个好人
type person struct {
   name string
}

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

func main()  {
   var p person
   p.name = "tom"
   p.speak()
}
  1. 给person结构体添加jisuan方法,可以计算从1+..+1000的1结果,说明:方法体内可以像函数一样进行各种运算
type person struct {
   name string
}

//给person结构题添加jisuan方法,可以计算从1+..+1000的1结果
func (p person) jisuan()  {
   res := 0
   for i:=1; i <= 1000; i++ {
      res += i
   }
   fmt.println(p.name,"计算的结构是 = ", res)  //tom 计算的结构是 =  500500
}
func main()  {
   var p person
   p.name = "tom"
   p.jisuan()
}
  1. 给person结构体jisuan2方法,该方法可以接收一个数n,计算从1+..+n的结果
type person struct {
   name string
}

func (p person) jisuan2(n int)  {
   res := 0
   for i:=1; i <= n; i++ {
      res += i
   }
   fmt.println(p.name,"计算的结构是 = ", res)  //tom 计算的结构是 =  210
}

func main()  {
   var p person
   p.name = "tom"
   p.jisuan2(20)
}
  1. 给person结构体添加getsum方法,可以计算两个数的和,并返回结果
type person struct {
   name string
}

func (p person) getsum(n1 int, n2 int) int {
   return n1 + n2   
}

func main()  {
   var p person
   res := p.getsum(10, 20)
   fmt.println("res = ", res) //res =  30
}

方法的调用和传参机制原理[重要]

方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当作实参也传递给方法。

案例1

画出前面getsum方法的执行过程+说明
go语言系列-面向对象编程

  1. 在通过一个变量去调用方法时,其调用机制和函数一样

  2. 不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝)

案例2

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

  1. 声明一个结构体circle,字段为radius

  2. 声明一个方法area和circle绑定,可以返回面积

  3. 提示:画出area执行过程+说明
    go语言系列-面向对象编程

方法的声明(定义)

func (recevier type) methodname (参数列表) (返回值列表) {
	方法体
	return 返回值
}

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

方法的注意事项和细节

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

  2. 如果程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
    go语言系列-面向对象编程

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

type integer int

func (i integer) print()  {
   fmt.println("i = ",i)
}
//编写一个方法,可以改变i的值
func (i *integer) change() {
   *i = *i + 1
}
func main()  {
   var i integer = 10
   i.print()
   i.change()
   fmt.println("i = ", i)
}

//输出:i =  10
//i =  11
  1. 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问

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

type student struct {
   name string
   age int
}
//给*student实现方法string()
func (stu *student) string() string {
   str := fmt.sprintf("name = [%v] age = [%v]",stu.name, stu.age)
   return str
}

func main() {
   //定义一个student变量
   stu := student{
      name: "tom",
      age:  20,
   }
   //如果实现了*student 类型的string方法,就会自动调用
   fmt.println(&stu)
}
//输出:name = [tom] age = [20]

方法和函数的区别

  1. 调用方式不一样

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

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

  1. 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
type person struct {
   name string
}

//函数
//对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
func test01(p person)  {
   fmt.println(p.name)  //tom
}
func test02(p *person)  {
   fmt.println(p.name)  //tom
}
func main()  {
   p := person{"tom"}
   test01(p)
   test02(&p)
}
  1. 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
type person struct {
   name string
}

//3)对于方法(如struct的方法),
// 接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
func (p person) test03()  {
   p.name = "jack"
   fmt.println("test03() = ",p.name)
}

func (p *person) test04()  {
   p.name = "mary"
-   fmt.println("test04() = ",p.name)
}

func main()  {
   p := person{"tom"}
   p.test03()
   fmt.println("main() p.name = ",p.name)

   (&p).test03()  //从形式上传入地址,但是本质仍然是值拷贝
   fmt.println("main() p.name = ",p.name)

   (&p).test04()
   fmt.println("main() p.name = ",p.name)
   p.test04()
}
//test03() =  jack
//main() p.name =  tom
//test03() =  jack
//main() p.name =  tom
//test04() =  mary
//main() p.name =  mary
//test04() =  mary

总结

  1. 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定

  2. 如果是和值类型,比如(p person),则是值拷贝,如果和指针类型,比如是(p person)则是地址拷贝

方法练习题

编写结构体(methodutils),编程一个方法,方法不需要参数,在方法中打印一个 10*8的矩形,在main方法中调用该方法

type methodutils struct {
   //字段...
}
//给methodutils编写方法
func (mu methodutils) print()  {
   for i := 1; i <= 10; i++ {
      for j := 1; j <= 8; j++ {
         fmt.print("*")
      }
      fmt.println()
   }
}
func main()  {
   var mu methodutils
   mu.print()
}
//输出:
//********
//********
//********
//********
//********
//********
//********
//********
//********
//********

编写一个方法,提供m 和 n 两个参数,方法中打印一个 m * n 的矩形

type methodutils struct {
   //字段...
}
func (mu methodutils) print2(m int, n int)  {
   for i := 1; i <= 10; i++ {
      for j := 1; j <= 8; j++ {
         fmt.print("*")
      }
      fmt.println()
   }
}

func main()  {
   var mu methodutils
   mu.print2(10,8)
}
//输出:
//********
//********
//********
//********
//********
//********
//********
//********
//********
//********

编写一个方法算该矩形的面积(可以接收长len,和宽width),将其作为方法返回值。在main方法中调用该方法,接收返回的面积值并打印

type methodutils struct {
   //字段...
}
func (mu methodutils) area(len float64, width float64) (float64)  {
   return  len * width
}

func main()  {
   var mu methodutils
   fmt.println("面积 =",mu.area(10,20))  //面积 = 200
}

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

type methodutils struct {
   //字段...
}

func (mu *methodutils) judgenum(num int)  {
   if num % 2 == 0 {
      fmt.println(num,"是偶数..")  //   10 是偶数..

   } else {
      fmt.println(num,"是奇数..")
   }
}

func main()  {
   var mu methodutils
   mu.judgenum(10)
}

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

type methodutils struct {
   //字段...
}

func (mu *methodutils) print(n int, m int, key string)  {
   for i := 1; i <= n; i++ {
      for j := 1; j <= m; j++ {
         fmt.print(key)
      }
      fmt.println()
   }
}

func main()  {
   var mu methodutils
   mu.print(5,5,"+")
}

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

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

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

//实现形式1
type calcuator struct {
   num1 float64
   num2 float64
}

func (calcuator *calcuator) getsum()  float64 {
   return calcuator.num1 + calcuator.num2
}

func (calcuator *calcuator) getsub() float64 {
   return calcuator.num1 - calcuator.num2
}

package main

import "fmt"
//实现形式2
type calcuator struct {
   num1 float64
   num2 float64
}

func (calcuator *calcuator) getres(operator byte) float64  {
   res := 0.0
   switch operator {
   case '+':
      res = calcuator.num1 + calcuator.num2
   case '-':
      res = calcuator.num1 - calcuator.num2
   case '*':
      res = calcuator.num1 * calcuator.num2
   case '/':
      res = calcuator.num1 / calcuator.num2
   default:
      fmt.println("运算符输入有误...")
   }
   return res
}

在merhodutils结构体编个方法,从键盘接收整数(1-9),打印对应乘法表
go语言系列-面向对象编程

type merhodutils struct {
	//字段
}

func (m merhodutils) jiu(n int)  {
	for i := 1; i <= n; i++ {
		for j := 1; j <= i; j++ {
			fmt.printf("%v * %v = %v\t",i,j,i*j)
		}
		fmt.println()
	}
}

func main()  {
	var mu merhodutils
	var num int
	fmt.println("请键入要输入的大于等于1小于等于9的自然数: ")
	fmt.scanln(&num)
	mu.jiu(num)
}
//请键入要输入的大于等于1小于等于9的自然数: 
//2
//1 * 1 = 1	
//2 * 1 = 2	2 * 2 = 4

编写方法,使给定的一个二维数组(3 × 3)转置
go语言系列-面向对象编程

面向对象编程应用实例

步骤

  1. 声明(定义)结构体,确定结构体名

  2. 编写结构体的字段

  3. 编写结构体的方法

学生案例

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

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

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

type student struct {
   name string
   gender string
   age int
   id int
   score float64
}

func (student *student) say() string  {
   infostr := fmt.sprintf("student 的信息 name = [%v] gender = [%v] age = [%v] id = [%v] score = [%v]",
      student.name, student.gender, student.age, student.id, student.score)
   return infostr
}
func main()  {
   //创建一个student实例变量
   var stu = student{
      name: "zisefeizhu",
      gender:"male",
      age: 18,
      id: 1000,
      score: 99.98,
   }
   fmt.println(stu.say())
}
//输出:student 的信息 name = [zisefeizhu] gender = [male] age = [18] id = [1000] score = [99.98]

小狗案例

  1. 编写一个dog结构体,包含name、age、weight字段

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

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

type dog struct {
   name string
   age int
   wgight float64
}

func (dog *dog) say() string  {
   infostr := fmt.sprintf("dog 的信息 name = [%v] age = [%v] weight = [%v]",
      dog.name, dog.age, dog.wgight)
   return infostr
}
func main()  {
   //创建一个student实例变量
   var stu = dog{
      name: "xiaohua",
      age: 18,
      wgight: 23,
   }
   fmt.println(stu.say())
}
//输出:dog 的信息 name = [xiaohua] age = [18] weight = [23]

盒子案例

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

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

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

type box struct {
   len float64
   width float64
   height float64
}
//声明一个方法获取立方体的体积
func (box *box) getvolum() float64 {
   return  box.len * box.width * box.height
}
func main()  {
   var box box
   box.len = 1.1
   box.width = 2.0
   box.height = 3.0
   volumn := box.getvolum()
   fmt.printf("体积为=%.2f",volumn)
}
//输出:体积为=6.60

景区门票案例

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

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

type visitor struct {
   name string
   age int
}
//声明一个方法获取立方体的体积
func (visitor *visitor) showprice() {
   if visitor.age >= 90 || visitor.age <= 8 {
      fmt.println("考虑到安全,就不要玩了")
      return
   }
   if visitor.age > 18 {
      fmt.printf("游客的名字为 %v 年龄为 %v 收费20元\n", visitor.name, visitor.age)
   } else {
      fmt.printf("游客的名字为 %v 年龄为%v 免费 \n", visitor.name, visitor.age)
   }
}

func main()  {
   var v  visitor
   for {
      fmt.println("请输入你的名字")
      fmt.scanln(&v.name)
      if v.name == "n" {
         fmt.println("退出程序...")
         break
      }
      fmt.println("请输入你的年龄")
      fmt.scanln(&v.age)
      v.showprice()
   }
}
//输出:请输入你的名字
//zisefeizhu
//请输入你的年龄
//20
//游客的名字为 zisefeizhu 年龄为 20 收费20元
//请输入你的名字
//n
//退出程序...

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

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

方式1:
type stu struct {
   name string
   age int
}

func main()  {
   //方式1
   //在创建结构体变量时,就直接指定字段的值
   var  stu1 = stu{"zisefeizhu",20}  // stu1 --> 结构体数据空间
   stu2 := stu{"jingxing",20}
   //在创建结构体变量时,把字段名和字段值写在一起,这种写法,就不依赖字段的定义顺序
   var stu3 = stu{
      name: "yike",
      age: 20,
   }
   var stu4 = stu{
      name: "gengpan",
      age: 20,
   }
   fmt.println(stu1, stu2, stu3, stu4)
}
//输出:{zisefeizhu 20} {jingxing 20} {yike 20} {gengpan 20}

方式2:
type stu struct {
   name string
   age int
}

func main()  {
   //方式2
   var stu5 *stu = &stu{"小王", 20} //stu5 --> 地址 --> 结构体数据[xxxx,xxxx]
   stu6 := &stu{"小紫", 20}
   //在创建结构体指针变量时。把字段名和字段值写在一起,这种写法,就不依赖字段的定义顺序
   var stu7 = &stu{
      name: "小林",
      age: 20,
   }
   var stu8 = &stu{
      age: 20,
      name: "小耿",
   }
   fmt.println(*stu5, *stu6, *stu7, *stu8)
}
//输出:{小王 20} {小紫 20} {小林 20} {小耿 20}

工厂模式

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

看一个需求

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

pachage model
type student struct {
name string ...
}

因为这里的student的首字母s是大写的,如果我们想在其它包创建student的实例(比如main包),引入model包后,就可以直接创建student结构体的变量(实例)。但是问题来了,如果首字母是小写的,比如是type student struct {...}就不行了,怎么办-->工厂模式来解决>

工厂模式来解决问题

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

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

![img](file:///c:\users\linkun\appdata\local\temp\ksohtml1372\wps1.jpg)

如果model包的结构体变量首字母小写,引入后,不能直接使用,可以工厂模式解决

student.go

package model

//定义一个结构体
type student struct {
   name string
   score float64
}

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

main.go

package main

import (
   "2020-04-04/model"
   "fmt"
)

func main() {
   var stu = model.newstudent("tom", 21)
   fmt.println(*stu) //&{...}
   fmt.println("name = ", stu.name, "score = ", stu.score)
}
//name =  tom score =  21

思考题

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

解决方法如下:

​ student.go

//定义一个结构体
type student struct {
   name string
   score float64
}

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

//如果score字段首字母小写,则,在其它包不可以直接访问,可以提供一个方法
func (s *student) getscore() float64  {
   return s.score
}

main.go

import (
   "2020-04-04/model"
   "fmt"
)

func main() {
   var stu = model.newstudent("tom", 22)
   fmt.println(*stu) //&{...}
   fmt.println("name = ", stu.name, "score = ", stu.getscore())
}
//{tom 22}
//name =  tom score =  22

面向对象编程思想-抽象

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

快速入门案例

go语言系列-面向对象编程

//定义一个结构体account
type account struct {
	accountno string
	pwd string
	balance float64
}
//方法
//1.存款
func (account *account) deposite(money float64,pwd string){
	//看下输入的密码是否正确
	if pwd != account.pwd {
		fmt.println("你输入的密码不正确")
		return
	}
	//看看存款是否正确
	if money <= 0 {
		fmt.println("你输入的金额不正确")
		return
	}
	account.balance += money
	fmt.println("存款成功!")
}
//取款
func (account *account) withdraw(money float64, pwd string)  {
	//看一下输入的密码是否正确
	if pwd != account.pwd {
		fmt.println("你输入的密码不正确")
		return
	}
	//看看存款金额是否正确
	if money <= 0 || money > account.balance{
		fmt.println("你输入的金额不正确")
		return
	}
	account.balance -= money
	fmt.println("取款成功~")
}
//查询余额
func (account *account) query(pwd string)  {
	//看一下输入的密码是否正确
	if pwd != account.pwd {
		fmt.println("你输入的密码不正确")
		return
	}
	fmt.printf("你的账号为=%v 余额=%v \n",account.accountno,account.balance)
}
func main()  {
	var pwd string
	var balance float64
	account := account{
		accountno: "1111111",
		pwd: "666666",
		balance: 0.0,
	}
	fmt.println("请输入密码")
	fmt.scanln(&pwd)
	if pwd == account.pwd {
		fmt.println("请输入金额")
		fmt.scanln(&balance)
		account.query(pwd)
		account.deposite(balance,pwd)
		account.query(pwd)
	}
}
//请输入密码
//666666
//请输入金额
//2000
//你的账号为=1111111 余额=0 
//存款成功!
//你的账号为=1111111 余额=2000 

对上面代码进行修饰:增加一个控制台的菜单,可以让用户动态的输入选项

package main

import "fmt"
//定义一个结构体account
type account struct {
	accountno string
	pwd string
	balance float64
}
//方法
//1.存款
func (account *account) deposite(money float64,pwd string){
	//看下输入的密码是否正确
	if pwd != account.pwd {
		fmt.println("你输入的密码不正确")
		return
	}
	//看看存款是否正确
	if money <= 0 {
		fmt.println("你输入的金额不正确")
		return
	}
	account.balance += money
	fmt.println("存款成功!")
}
//取款
func (account *account) withdraw(money float64, pwd string)  {
	//看一下输入的密码是否正确
	if pwd != account.pwd {
		fmt.println("你输入的密码不正确")
		return
	}
	//看看存款金额是否正确
	if money <= 0 || money > account.balance{
		fmt.println("你输入的金额不正确")
		return
	}
	account.balance -= money
	fmt.println("取款成功~")
}
//查询余额
func (account *account) query(pwd string)  {
	//看一下输入的密码是否正确
	if pwd != account.pwd {
		fmt.println("你输入的密码不正确")
		return
	}
	fmt.printf("你的账号为=%v 余额=%v \n",account.accountno,account.balance)
}
func main()  {
	var xuanxiang byte
	var pwd string
	var balance float64
	account := account{
		accountno: "1111111",
		pwd: "666666",
		balance: 0.0,
	}
	fmt.println("请输入密码")
	fmt.scanln(&pwd)
	if pwd == account.pwd {
	for  {
		fmt.println("请输入菜单选项:")
		fmt.println("1. 存款")
		fmt.println("2. 取款")
		fmt.println("3. 余额")
		fmt.println("4. 退出")
		fmt.println("请输入菜单选项:")
		fmt.scanln(&xuanxiang)
		switch xuanxiang {
		case 1:
			fmt.println("请输入金额")
			fmt.scanln(&balance)
			account.query(pwd)
			account.deposite(balance,pwd)
			account.query(pwd)
			//fmt.println("请输入金额")
			//fmt.scanln(&balance)
			//account.deposite(balance,pwd)
		case 2:
			fmt.println("请输入金额")
			fmt.scanln(&balance)
			account.query(pwd)
			account.withdraw(balance,pwd)
			account.query(pwd)
		case 3:
			//
			account.query(pwd)
		default:
			return
		}
	}

		//fmt.println("请输入金额")
		//fmt.scanln(&balance)
		//account.query(pwd)
		//account.deposite(balance,pwd)
		//account.query(pwd)
	}
}
//请输入密码
//666666
//请输入金额
//2000
//你的账号为=1111111 余额=0
//存款成功!
//你的账号为=1111111 余额=2000

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

go仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它oop语言不一样

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

封装的好处

  1. 隐藏实现细节

  2. 可以对数据进行验证,保证安全合理(age)

如何体现封装

  1. 对结构体中的属性进行封装

  2. 通过方法,包实现封装

封装的实现步骤

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

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

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

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

*特别说明*:在go开发中并没有特别强调封装,这点并不像java,所以不用总是用java的语法特性来看待go,go本身对面向对象的特性做了简化的

快速入门案例

编写一个程序(person.go),不能随便查看人的年龄、工资等隐私,并对输入的年龄进行合理的验证。

设计:model包(person.go),main包(main.go调用person结构体)

person.go

type person struct {
   name string
   age int  //其它包不能直接访问
   sal float64
}
//写一个工厂模式的函数,相当于构造函数
func newperson(name string) *person {
   return &person{
      name: name,
   }
}
//为了访问age和sal 编写一对setxxx的方法和getxxx的方法
func (p *person) setage(age int)  {
   if age > 0 && age < 150 {
      p.age = age
   } else {
      fmt.println("年龄范围不正确..")
      //给程序员一个默认值
   }
}

func (p *person) getage() int {
   return p.age
}
func (p *person) setsal(sal float64)  {
   if sal >= 3000 && sal <= 30000 {
      p.sal = sal
   } else {
      fmt.println("薪水范围不正确...")
   }
}
func (p *person) getsal() float64 {
   return p.sal
}

main.go

func main()  {
   p := model.newperson("smith")
   p.setage(18)
   p.setsal(5000)
   fmt.println(p)
   fmt.println(p.name, "age =", p.getage(), "sal =", p.getsal())
}
//输出:&{smith 18 5000}
//smith age = 18 sal = 5000

要求

  1. 创建程序,在model包中定义account结构体:在main函数中体现go的封装性

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

  3. 通过setxxx的方法给account的字段赋值

  4. 在main函数中测试

account.go

//定义一个结构体account
type account struct {
   accountno string
   pwd string
   balance float64
}
//工厂模式的函数-构造函数
func newaccount(accountno string, pwd string, balance float64) *account  {
   if len(accountno) < 6 || len(accountno) > 10 {
      fmt.println("账号的长度不对")
      return nil
   }
   if len(pwd) != 6 {
      fmt.println("密码的长度不对...")
      return nil
   }
   if balance < 20 {
      fmt.println("余额数目不对...")
      return nil
   }
   return &account{
      accountno: accountno,
      pwd: pwd,
      balance: balance,
   }
}

//方法
//存款
func (account *account) deposite(money float64, pwd string)  {
   //看下输入的密码是否正确
   if pwd != account.pwd {
      fmt.println("你输入的密码不正确")
      return
   }
   //看看存款金额是否正确
   if money <= 0 {
      fmt.println("你输入的金额不正确")
      return
   }
   account.balance += money
   fmt.println("存款成功~")
}
//取款
func (account *account) withdraw(money float64, pwd string)  {
   //看下输入的密码是否正确
   if pwd != account.pwd {
      fmt.println("你输入的密码不正确")
      return
   }
   //看看存款金额是否正确
   if money <= 0 || money > account.balance {
      fmt.println("你输入的金额不正确")
      return
   }
   account.balance -= money
   fmt.println("取款成功~")
}
//查询余额
func (account *account) query(pwd string)  {
   //看下输入的密码是否正确
   if pwd != account.pwd {
      fmt.println("你输入的密码不正确")
      return
   }

   fmt.printf("你的账号为 = %v 余额 = %v \n", account.accountno, account.balance)
}

main.go

func main()  {
   //创建一个account变量
   account := model.newaccount("zisefeizhu","000",40)
   if account != nil {
      fmt.println("创建成功 = ", account)
   } else {
      fmt.println("创建失败")
   }
}
//输出:密码的长度不对...
//创建失败

增加如下功能:通过setxxx的方法给account的字段赋值通过getxxx方法获取字段的值

account.go

package model

import "fmt"
//定义一个结构体account
type account struct {
	accountno string
	pwd string
	balance float64
}
//工厂模式的函数-构造函数
func newaccount(accountno string, pwd string, balance float64) *account  {
	return &account{
		accountno: accountno,
		pwd: pwd,
		balance: balance,
	}
}

func (accounter *account) setaccountno(accountno string) {
	if len(accountno) < 6 || len(accountno) > 10 {
		fmt.println("账号的长度不对")
	}
}

func (accounter *account) getaccountno() string {
	return accounter.accountno
}

//方法
//存款
func (account *account) deposite(money float64, pwd string)  {
	//看下输入的密码是否正确
	if pwd != account.pwd {
		fmt.println("你输入的密码不正确")
		return
	}
	//看看存款金额是否正确
	if money <= 0 {
		fmt.println("你输入的金额不正确")
		return
	}
	account.balance += money
	fmt.println("存款成功~")
}
//取款
func (account *account) withdraw(money float64, pwd string)  {
	//看下输入的密码是否正确
	if pwd != account.pwd {
		fmt.println("你输入的密码不正确")
		return
	}
	//看看存款金额是否正确
	if money <= 0 || money > account.balance {
		fmt.println("你输入的金额不正确")
		return
	}
	account.balance -= money
	fmt.println("取款成功~")
}
//查询余额
func (account *account) query(pwd string)  {
	//看下输入的密码是否正确
	if pwd != account.pwd {
		fmt.println("你输入的密码不正确")
		return
	}

	fmt.printf("你的账号为 = %v 余额 = %v \n", account.accountno, account.balance)
}

main.go

package main

import (
	"2020-04-04/model"
	"fmt"
)

func main()  {
	//创建一个account变量
	account := model.newaccount("zisefeizhu","000",40)
	fmt.println(account)
	fmt.println(account.getaccountno())
}
//&{zisefeizhu 000 40}
//zisefeizhu

类似改法

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

看一个问题,引出继承的必要性

看一个学生考试系统的程序extend01.go,提出代码复用的问题
go语言系列-面向对象编程

//编写一个学生考试系统
//小学生
type pupil struct {
   name string
   age int
   score int
}
//显示他的成绩
func (p *pupil) showinfo() {
   fmt.printf("学生名 = %v 年龄 = %v 成绩 = %v\n", p.name, p.age, p.score)
}
func (p *pupil) setscore(score int) {
   //业务判断
   p.score = score
}
func (p *pupil) testing() {
   fmt.println("小学生正在考试中...")
}
//大学生,研究生...

//大学生
type graduate struct {
   name string
   age int
   score int
}
//显示他的成绩
func (p *graduate) showinfo() {
   fmt.printf("学生名 = %v 年龄 = %v 成绩 = %v\n", p.name, p.age, p.score)
}
func (p *graduate) setscore(score int) {
   //业务判断
   p.score = score
}
func (p *graduate) testing() {
   fmt.println("大学生正在考试中...")
}
//代码冗余... 研究生
//代码冗余... 高中生
func main()  {
   //测试
   var pupil = &pupil{
      name: "tom",
      age: 10,
   }
   pupil.testing()
   pupil.setscore(90)
   pupil.showinfo()

   //测试
   var graduate = &graduate{
      name: "tom",
      age: 20,
   }
   graduate.testing()
   graduate.setscore(90)
   graduate.showinfo()
}
//输出:小学生正在考试中...
//学生名 = tom 年龄 = 10 成绩 = 90
//大学生正在考试中...
//学生名 = tom 年龄 = 20 成绩 = 90

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

继承基本介绍和示意图

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

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

其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个student匿名结构体即可
go语言系列-面向对象编程
在go中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性

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

type goods struct {
	name string
	price int
}
type book struct {
	goods //这里就是嵌套匿名结构体goods
	writer string
}

快速入门案例

对extends01.go改进,使用嵌套匿名结构体的方式来实现继承特性,体会继承的好处

//编写一个学生考试系统
//小学生
type student struct {
   name string
   age int
   score int
}
//将pupil 和 graduate 共有的方法也绑定到 *student
func (stu *student) showinfo() {
   fmt.printf("学生名 = %v 年龄 = %v 成绩 = %v\n", stu.name, stu.age, stu.score)
}

func (stu *student) setscore(score int) {
   //业务判断
   stu.score = score
}
//小学生
type pupil struct {
   student  //嵌入了student匿名结构体
}
//显示他的成绩
//这时pupil结构体特有的方法,保留
func (p *pupil) testing() {
   fmt.println("小学生正在考试中...")
}
//大学生,研究生...

//大学生
type graduate struct {
   student  //嵌入了student匿名结构体
}
//显示他的成绩
//这时graduate结构体特有的方法,保留
func (p *graduate) testing() {
   fmt.println("大学生正在考试中...")
}
//代码冗余... 研究生
//代码冗余... 高中生
func main()  {
   //测试
   //当我们对结构体嵌入了匿名结构体使用方法会发生变化
   pupil := &pupil{}
   pupil.student.name = "tom"
   pupil.student.age = 8
   pupil.testing()
   pupil.setscore(70)
   pupil.showinfo()

   //测试
   graduate := &graduate{}
   graduate.student.name = "marry"
   graduate.student.age = 28
   graduate.testing()
   graduate.setscore(90)
   graduate.showinfo()
}
//输出:小学生正在考试中...
//学生名 = tom 年龄 = 8 成绩 = 70
//大学生正在考试中...
//学生名 = marry 年龄 = 28 成绩 = 90

继承给编程带来的便利

  1. 代码的复用性提高了

  2. 代码的扩展性和维护性提高了

继承的深入讨论

1)结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用

type a struct {
   name string
   age int
}

func (a *a) sayok() {
   fmt.println("a sayok",a.name)
}
func (a *a) hello() {
   fmt.println("a hello",a.name)
}
type b struct {
   a
}

func main()  {
   var b b
   b.a.name = "zisefeizhu"
   b.a.age = 19
   b.a.sayok()
   b.a.hello()
}
//输出:a sayok zisefeizhu
//a hello zisefeizhu
  1. 匿名结构体字段访问可以简化
    go语言系列-面向对象编程
    对上面的代码小结
    (1)当我们直接通过b访问字段或方法时,其执行流程如下比如b.name
    (2)编译器会先看b对应的类型有没有name,如果有,则直接调用b类型的name字段
    (3)如果没有就去看b中嵌入的匿名结构体a有没有声明name字段,如果有就调用,如果没有继续查找...如果都找不到就报错
    3) 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
    go语言系列-面向对象编程

  2. 结构体嵌入两个(或多个)匿名结构体,如果两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明指定匿名结构体名字,否则编译报错
    go语言系列-面向对象编程

  3. 如果一个struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
    go语言系列-面向对象编程
    6 ) 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值

type goods struct {
   name string
   price float64
}
type brand struct {
   name string
   address string
}
type tv struct {
   goods
   brand
}
type tv2 struct {
   *goods
   *brand
}

func main()  {
   tv := tv{ goods{"电视机001", 5000.99},brand{"海尔","山东"},}
   tv2 := tv{
      goods{
         price: 5000.99,
         name: "电视机002",
      },
      brand{
         name: "夏普",
         address: "北京",
      },
   }
   fmt.println("tv", tv)
   fmt.println("tv2", tv2)
   tv3 := tv2{ &goods{"电视机003", 7000.99},&brand{"创维","河南"},}
   tv4 := tv2{
      &goods{
         name: "电视机004",
         price: 9000.99,
      },
      &brand{
         name: "长虹",
         address: "四川",
      },
   }
   fmt.println("tv3", *tv3.goods, *tv3.brand)
   fmt.println("tv4", *tv4.goods, *tv4.brand)
}
//tv {{电视机001 5000.99} {海尔 山东}}
//tv2 {{电视机002 5000.99} {夏普 北京}}
//tv3 {电视机003 7000.99} {创维 河南}
//tv4 {电视机004 9000.99} {长虹 四川}

课堂练习

结构体的匿名字段是基本数据类型,如何访问?
type monster struct {
   name string
   age int
}
type e struct {
   monster
   int
   n int
}

func main()  {
   //演示一下匿名字段时基本数据类型的使用
   var e e
   e.name = "狐狸精"
   e.age = 300
   e.int = 20
   e.n = 40
   fmt.println("e = ", e)
}
//输出:e =  {{狐狸精 300} 20 40}

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

面向对象编程-多重继承

多重继承说明
如一个struct嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。
案例演示
通过一个案例来说明多重继承使用
go语言系列-面向对象编程
多重继承细节

  1. 若嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分
    go语言系列-面向对象编程
    2)为了保证代码的简洁性,建议尽量不使用多重继承

接口

go中多态特性主要是通过接口来体现的

usb插槽就是现实中的接口你可以把手机,相机,u盘都插在usb插槽上,而不用担心那个插槽是专门插哪个的,原因是做usb插槽的厂家和做各种设备的厂家都遵守了统一的规定包括尺寸,排线等等。

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

//声明/定义一个接口
type usb interface {
   //声明了两个没有实现的方法
   start()
   stop()
}
type phone struct {

}
//让phone实现usb接口的方法
func (p phone) start() {
   fmt.println("手机开始工作...")
}
func (p phone) stop() {
   fmt.println("手机停止工作...")
}

type camera struct {

}
//让camera实现usb接口的方法
func (c camera) start() {
   fmt.println("相机开始工作...")
}
func (c camera) stop() {
   fmt.println("相机停止工作...")
}
//计算机
type computer struct {

}
//编写一个方法working方法,接收一个usb接口类型变量
//只要是实现了usb接口(所谓实现usb接口,就是指实现了usb接口声明所有方法)
func (c computer) working(usb usb) { //usb变量会根据传入的实参,来判断到底是phone,还是camera
   //通过usb接口变量来调用start和stop方法
   usb.start()
   usb.stop()
}
func main()  {
   //测试
   //先创建结构体变量
   computer := computer{}
   phone := phone{}
   camera := camera{}
   //关键点
   computer.working(phone)
   computer.working(camera)
}

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

基本语法

go语言系列-面向对象编程
小结说明:

  1. 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想

  2. go中的接口,不需要显示的实现。只要一个变量,含有接口类型中的所有方法,那么变量就实现了这个接口,因此,go中没有implement这样的关键字

接口使用的应用场景

  1. 中国要制造的轰炸机,专家只需要把飞机需要的功能/规格定下来即可,然后让别的人具体实现即可

  2. 现在有一个项目经理,管理三个程序员,开发一个软件,为了控制和管理软件,项目经理可以定义一些接口,然后由程序员具体实现

......
go语言系列-面向对象编程

注意事项和细节

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

type ainterface interface {
   say()
}
type stu struct {
   name string
}

func (stu stu) say() {
   fmt.println("stu say()")
}
func main()  {
   var stu stu //结构体变量,实现了say() 实现了ainterface
   var a ainterface = stu
   a.say()
}

接口中所有的方法都没有方法体,即都是没有实现的方法

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

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

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

type ainterface interface {
   say()
}
type stu struct {
   name string
}
type integer int

func (i integer) say()  {
   fmt.println("integer say i =", i)
}

func (stu stu) say() {
   fmt.println("stu say()")
}
func main()  {
   var i integer = 10
   var b ainterface = i
   b.say()
   var stu stu //结构体变量,实现了say() 实现了ainterface
   var a ainterface = stu
   a.say()
}
//输出:integer say i = 10
//stu say()

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

type ainterface interface {
   say()
}
type binterface interface {
   hello()
}
type monster struct {

}

func (m monster) hello() {
   fmt.println("monster hello()")
}
func (m monster) say()  {
   fmt.println("monster say ")
}
func main()  {
   //monster实现了ainterface 和binterface
   var monster monster
   var a2 ainterface = monster
   var b2 binterface = monster
   a2.say()
   b2.hello()
}
//输出:monster say 
//monster hello()

go接口中不能有任何变量
go语言系列-面向对象编程
一个接口(比如a接口)可以继承多个别的接口(比如b,c接口),这时如果要实现a接口,也必须将b,c接口的方法也全部实现

package main

type binterface interface {
   test01()
}
type cinterface interface {
   test02()
}
type ainterface interface {
   binterface
   cinterface
   test03()
}
//如果需要实现ainterface,就需要将binterface cinterface的方法都实现
type stu struct {

}

func (stu stu) test01() {

}
func (stu stu) test02() {

}
func (stu stu) test03() {

}

func main()  {
   var stu stu
   var a ainterface = stu
   a.test01()
}

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

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

type binterface interface {
   test01()
}
type cinterface interface {
   test02()
}
type ainterface interface {
   binterface
   cinterface
   test03()
}
//如果需要实现ainterface,就需要将binterface cinterface的方法都实现
type stu struct {

}

func (stu stu) test01() {

}
func (stu stu) test02() {

}
func (stu stu) test03() {

}

type  t interface {
   //空接口
}
func main()  {
   var stu stu
   var t t = stu //ok
   fmt.println(t)
   var t2 interface{} = stu
   var num1 float64 = 8.8
   t2 = num1
   t = num1
   fmt.println(t2, t)
   var a ainterface = stu
   a.test01()
}
//输出:{}
//8.8 8.8

接口编程最佳实践

import (
   "fmt"
   "math/rand"
   "sort"
)
//1.声明hero结构体
type hero struct {
   name string
   age int
}
//2.声明一个hero结构体切片类型
type heroslice []hero

//3.实现interface接口
func (hs heroslice) len() int  {
   return len(hs)
}
//less方法就是决定使用什么标准进行排序
//1.按hero的年龄从小到大排序
func (hs heroslice) less(i, j int) bool {
   return hs[i].age < hs[j].age
   //修改成对name排序
   //return hs[i].name < hs[j].name
}
func (hs heroslice) swap (i,j int) {
   //交换
   hs[i], hs[j] = hs[j], hs[i]
}
//1.声明student结构体
type student struct {
   name string
   age int
   score float64
}

//将student的切片,按score从大到小排序!
func main() {
   //先定义一个数组/切片
   var intslice = []int{0, -1, 10, 7, 90}
   //要求对intslice切片进行排序
   //1. 冒泡排序...
   //2. 也可以使用系统提供的方法
   sort.ints(intslice)
   fmt.println(intslice)

   //对结构体切片进行排序
   //1. 冒泡排序...
   //2. 也可以使用系统提供的方法

   //测试看看我们是否可以对结构体切片进行排序
   var heroes heroslice
   for i := 0; i < 10; i++ {
      hero := hero{
         name: fmt.sprintf("英雄 %d",rand.intn(100 )),
         age: rand.intn(100),
   }
      //将 hero append 到heroes切片
      heroes = append(heroes,hero)
   }
   //看看排序前的顺序
   for _,v := range heroes {
      fmt.println(v)
   }
   //调用sort.sort
   sort.sort(heroes)
   fmt.println("______________排序后______________")
   //看看排序后的顺序
   for _,v := range heroes {
      fmt.println(v)
   }
   i := 10
   j := 20
   i,j = j,i
   fmt.println("i = ", i, "j = ", j)
}
//[-1 0 7 10 90]
//{英雄 81 87}
//{英雄 47 59}
//{英雄 81 18}
//{英雄 25 40}
//{英雄 56 0}
//{英雄 94 11}
//{英雄 62 89}
//{英雄 28 74}
//{英雄 11 45}
//{英雄 37 6}
//______________排序后______________
//{英雄 56 0}
//{英雄 37 6}
//{英雄 94 11}
//{英雄 81 18}
//{英雄 25 40}
//{英雄 11 45}
//{英雄 47 59}
//{英雄 28 74}
//{英雄 81 87}
//{英雄 62 89}
//i =  20 j =  10

接口练习题

go语言系列-面向对象编程

实现接口 vs 继承

package main

import "fmt"
//monkey结构体
type monkey struct {
   name string
}
//声明接口
type birdable interface {
   flying()
}
type fishable interface {
   swimming()
}

func (this *monkey) climbing()  {
   fmt.println(this.name,"生来会爬树..")
}
//littlemonkey 结构体
type littlemonkey struct {
   monkey //继承
}
//让littlemonkey 实现birdable
func (this *littlemonkey) flying() {
   fmt.println(this.name,"通过学习,会飞翔...")
}

//让littlemonkey 实现fishable
func (this *littlemonkey) swimming() {
   fmt.println(this.name,"通过学习,会游泳...")
}
func main()  {
   //创建一个littlemonkey实例
   monkey := littlemonkey{
      monkey{
         name: "悟空",
      },
   }
   monkey.climbing()
   monkey.flying()
   monkey.swimming()
}
//输出:悟空 生来会爬树..
//悟空 通过学习,会飞翔...
//悟空 通过学习,会游泳...

对上面代码的小结
	当a结构体继承了b结构体,那么a结构就自动的继承了b结构体的字段和方法,并且可以直接使用
	当a结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可,因此可以认为:实现接口是对继承机制的补充

go语言系列-面向对象编程
实现接口可以看作是对继承的一种补充
go语言系列-面向对象编程
接口和继承解决的问题不同

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

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

接口比继承更加灵活 person student birdable littlemonkey

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

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

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

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

快速入门案例

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

//声明/定义一个接口
type usb interface {
   //声明了两个没有实现的方法
   start()
   stop()
}
type phone struct {

}
//让phone实现usb接口的方法
func (p phone) start() {
   fmt.println("手机开始工作...")
}
func (p phone) stop() {
   fmt.println("手机停止工作...")
}

type camera struct {

}
//让camera实现usb接口的方法
func (c camera) start() {
   fmt.println("相机开始工作...")
}
func (c camera) stop() {
   fmt.println("相机停止工作...")
}
//计算机
type computer struct {

}
//编写一个方法working方法,接收一个usb接口类型变量
//只要是实现了usb接口(所谓实现usb接口,就是指实现了usb接口声明所有方法)
func (c computer) working(usb usb) { //usb变量会根据传入的实参,来判断到底是phone,还是camera  //usb接口变量就体现出多态的特点
   //通过usb接口变量来调用start和stop方法
   usb.start()
   usb.stop()
}
func main()  {
   //测试
   //先创建结构体变量
   computer := computer{}
   phone := phone{}
   camera := camera{}
   //关键点
   computer.working(phone)
   computer.working(camera)
}

接口体现多态的两种形式

多态参数

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

多态数组

​ 演示一个案例:在usb数组中,存放phone结构体和camera结构体变量

//声明/定义一个接口
type usb interface {
   //声明了两个没有实现的方法
   start()
   stop()
}
type phone struct {
   name string
}
//让phone实现usb接口的方法
func (p phone) start() {
   fmt.println("手机开始工作...")
}
func (p phone) stop() {
   fmt.println("手机停止工作...")
}

type camera struct {
   name string
}
//让camera实现usb接口的方法
func (c camera) start() {
   fmt.println("相机开始工作...")
}
func (c camera) stop() {
   fmt.println("相机停止工作...")
}
//计算机
type computer struct {

}
func main()  {
   //定义一个usb接口数组,可以存放phone和camera的结构体变量
   //这里体现出多态数组
   var usbarr [3]usb
   usbarr[0] = phone{"vivo"}
   usbarr[1] = phone{"华为"}
   usbarr[2] = phone{"小米"}
   fmt.println(usbarr)
}

类型断言

类型断言:由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言
go语言系列-面向对象编程

案例演示

func main()  {
   //类型断言的其它案例
   var x interface{}
   var b2 float32 = 2.2
   x = b2 //空接口,可以接收任意类型
   //x => float32 [使用类型那个断言]
   y := x.(float32)    //转成具体类型
   fmt.printf("y 的类型是 %t 值是 = %v", y, y)
}
//输出:y 的类型是 float32 值是 = 2.2

对上面代码的说明:
	在进行类型断言时,如果类型不匹配,就会报panic,因此进行类型断言时,	要确保原来的空接口指向的就是断言的类型
如何在进行断言时,带上检测机制,如果成功就ok,否则也不要报panic
func main()  {
   //类型断言的其它案例
   var x interface{}
   var b2 float32 = 3.3
   x = b2 //空接口,可以接收任意类型
   //x => float32 [使用类型那个断言]
   if y, ok := x.(float32); ok {
      fmt.println("convert success")
      fmt.printf("y 的类型是%t 值是%v", y, y)
   } else {
      fmt.println("convert fail")
   }
   fmt.println("继续执行...")
   //y := x.(float32)    //转成具体类型
   //fmt.printf("y 的类型是 %t 值是 = %v", y, y)
}
//输出:y 的类型是float32 值是3.3继续执行...

类型断言的最佳实践

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

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

//声明/定义一个接口
type usb interface {
   //声明了两个没有实现的方法
   start()
   stop()
}
type phone struct {
   name string
}
//让phone实现usb接口的方法
func (p phone) start() {
   fmt.println("手机开始工作...")
}
func (p phone) stop() {
   fmt.println("手机停止工作...")
}

func (p phone) call()  {
   fmt.println("手机 在打电话...")
}

type camera struct {
   name string
}
//让camera实现usb接口的方法
func (c camera) start() {
   fmt.println("相机开始工作...")
}
func (c camera) stop() {
   fmt.println("相机停止工作...")
}
//计算机
type computer struct {

}
//编写一个方法working方法,接收一个usb接口类型变量
//只要是实现了usb接口(所谓实现usb接口,就是指实现了usb接口声明所有方法)
func (c computer) working(usb usb) { //usb变量会根据传入的实参,来判断到底是phone,还是camera  //usb接口变量就体现出多态的特点
   //通过usb接口变量来调用start和stop方法
   usb.start()
   //如果usb是指向phone结构体变量,则还需要调用call方法
   //类型断言...
   if phone, ok := usb.(phone); ok {
      phone.call()
   }
   usb.stop()
}
func main()  {
   //定义一个usb接口数组,可以存放phone和camera的结构体变量
   //这里体现出多态数组
   var usbarr [3]usb
   usbarr[0] = phone{"vivo"}
   usbarr[1] = phone{"华为"}
   usbarr[2] = phone{"小米"}
   fmt.println(usbarr)
   //遍历usbarr
   //phone还有一个特有的方法call(),请遍历usb数组,如果是phone变量
   //除了调用usb接口声明的方法外,还需要调用phone特有方法call => 类型断言
   var computer computer
   for _, v := range usbarr {
      computer.working(v)
      fmt.println()
   }
   //fmt.print(usbarr)
}
//[{vivo} {华为} {小米}]
//手机开始工作...
//手机 在打电话...
//手机停止工作...
//
//手机开始工作...
//手机 在打电话...
//手机停止工作...
//
//手机开始工作...
//手机 在打电话...
//手机停止工作...

写一函数,循环判断传入参数的类型:

/编写一个函数,可以判断输入的参数是什么类型
func typejudge(items... interface{}) {
   for index, x := range items {
      switch x.(type) {
      case bool:
         fmt.printf("第%v个参数是 bool 类型,值是%v\n", index, x)
      case float32:
         fmt.printf("第%v个参数是 float32 类型,值是%v\n", index, x)
      case float64:
         fmt.printf("第%v个参数是 float64 类型,值是%v\n", index, x)
      case int, int32, int64:
         fmt.printf("第%v个参数是 整数 类型,值是%v\n", index, x)
      case string:
         fmt.printf("第%v个参数是 string 类型,值是%v\n", index, x)
      default:
         fmt.printf("第%v个参数是   类型,值是%v\n", index, x)
      }
   }
}
func main()  {
   var n1 float32 = 1.1
   var n2 float64 = 2.3
   var n3 int32 = 30
   var name string = "tom"
   address := "北京"
   n4 := 300
   typejudge(n1, n2, n3, name, address, n4)
}
//第0个参数是 float32 类型,值是1.1
//第1个参数是 float64 类型,值是2.3
//第2个参数是 整数 类型,值是30
//第3个参数是 string 类型,值是tom
//第4个参数是 string 类型,值是北京
//第5个参数是 整数 类型,值是300

在前面代码的基础上,增加判断student类型和*student类型

package main

import (
	"fmt"
)

type student struct {
	name string
}

//编写一个函数,可以判断输入的参数是什么类型
func typejudge(items... interface{}) {
	for index, x := range items {
		switch x.(type) {
		case student:
			fmt.printf("第%v个参数是 student 类型,值是%v\n", index, x)
		case *student:
			fmt.printf("第%v个参数是 *student 类型,值是%v\n", index, x)
		case bool:
			fmt.printf("第%v个参数是 bool 类型,值是%v\n", index, x)
		case float32:
			fmt.printf("第%v个参数是 float32 类型,值是%v\n", index, x)
		case float64:
			fmt.printf("第%v个参数是 float64 类型,值是%v\n", index, x)
		case int, int32, int64:
			fmt.printf("第%v个参数是 整数 类型,值是%v\n", index, x)
		case string:
			fmt.printf("第%v个参数是 string 类型,值是%v\n", index, x)
		default:
			fmt.printf("第%v个参数是   类型,值是%v\n", index, x)
		}
	}
}
func main()  {
	var n1 float32 = 1.1
	var n2 float64 = 2.3
	var n3 int32 = 30
	var name string = "tom"
	address := "北京"
	n4 := 300
	var b bool = false
	stu1 := student{"zise"}
	stu2 := &student{"feizhzu"}
	typejudge(n1, n2, n3, name, address, n4, b, stu1, stu2)
}
//第0个参数是 float32 类型,值是1.1
//第1个参数是 float64 类型,值是2.3
//第2个参数是 整数 类型,值是30
//第3个参数是 string 类型,值是tom
//第4个参数是 string 类型,值是北京
//第5个参数是 整数 类型,值是300
//第6个参数是 bool 类型,值是false
//第7个参数是 student 类型,值是{zise}
//第8个参数是 *student 类型,值是&{feizhzu}