[TOC]

面向对象编程

结构体

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

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

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语言系列-面向对象编程 go 第1张

*注意:从猫结构体到变量,就是创建一个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语言系列-面向对象编程 go 第2张

字段/属性

基本介绍

​ 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语言系列-面向对象编程 go 第3张

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

方式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语言系列-面向对象编程 go 第4张 看下面代码,分析原因

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 第5张 go语言系列-面向对象编程 go 第6张

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

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语言系列-面向对象编程 go 第7张 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语言系列-面向对象编程 go 第8张
  2. struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用就是序列化和反序列化

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

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语言系列-面向对象编程 go 第10张

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

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

案例2

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

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

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

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

方法的声明(定义)

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语言系列-面向对象编程 go 第12张

  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语言系列-面向对象编程 go 第13张

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语言系列-面向对象编程 go 第14张

面向对象编程应用实例

步骤

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

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

如果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语言系列-面向对象编程 go 第16张

//定义一个结构体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语言系列-面向对象编程 go 第17张

//编写一个学生考试系统
//小学生
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 第18张 在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语言系列-面向对象编程 go 第19张 对上面的代码小结 (1)当我们直接通过b访问字段或方法时,其执行流程如下比如b.Name (2)编译器会先看b对应的类型有没有Name,如果有,则直接调用B类型的Name字段 (3)如果没有就去看B中嵌入的匿名结构体A有没有声明Name字段,如果有就调用,如果没有继续查找...如果都找不到就报错 3) 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分 go语言系列-面向对象编程 go 第20张

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

  3. 如果一个struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字 go语言系列-面向对象编程 go 第22张 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语言系列-面向对象编程 go 第23张 多重继承细节

  1. 若嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分 go语言系列-面向对象编程 go 第24张 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语言系列-面向对象编程 go 第25张 小结说明:

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

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

接口使用的应用场景

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

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

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

注意事项和细节

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

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语言系列-面向对象编程 go 第27张 一个接口(比如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语言系列-面向对象编程 go 第28张

实现接口 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 第29张 实现接口可以看作是对继承的一种补充 go语言系列-面向对象编程 go 第30张 接口和继承解决的问题不同

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

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

接口比继承更加灵活 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语言系列-面向对象编程 go 第31张

案例演示

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}
扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄