[Golang]面向对象营养餐,一文管够(封装,继承,多态)

Posted @书生

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Golang]面向对象营养餐,一文管够(封装,继承,多态)相关的知识,希望对你有一定的参考价值。

Go中也存在着面向对象编程,不过与传统的OOP(面向对象编程)语言相比,它舍弃了使用类,采用更加灵活的结构体
实现继承也采用使用匿名字段的方式,摒弃了传统意义的继承,并且通过接口可以实现多态。
在Go中,也摒弃了虚方法,构造函数,析构函数的概念。

注: Go中面向对象的三大特性,使用结构体实现封装,采用匿名字段实现继承,通过接口实现多态

封装

结构体:

结构体相当于一个存放相同或不同数据类型变量的集合,结构体的变量相当于我们封装时事物的属性,我们称之为字段;例如:我们用结构体来描述一个人的特征:姓名,年龄,性别,身份证号

type Person struct{
      name string
      age  int
      sex  string
      id   string  
}

注:
Person就是上面结构体的名称,相当于该结构体的类型,首字母大写,可在其他包访问,小写尽在本包访问,注意在本包中,结构体名称需要保证唯一;
结构体内的字段名称(eg:name,age,sex,id)在本结构体内必须是唯一的;
相同数据类型的字段可以写在同一行
eg:

type Person struct{
     name,sex,id string
     age  int
}

结构体的定义

type 结构体名称 struct {
    //字段名称 数据类型
}

注:
结构体在定义后,仅仅是完成了对内存布局的一种描述,并不占用内存空间,只有实例化后才会占用内存空间,也就是说只有实例化后,我们才能访问对应的结构体变量

实例化结构体

注:
当我们实例化出结构体变量的时候,即使不给对应的字段进行赋值,各个字段也拥有该数据类型的默认值

  1. 先声明变量,再给各个字段依次赋值
     var p person   //先声明结构体person的变量p
    fmt.Printf("%+v\\n",p)
    //倘若指声明了变量,不进行赋值 {name: sex: id: age:0}
    //name sex id的数据类型为string 默认值为"" 打印的时候显示不出来
    //给各个字段进行赋值
	p.name = "Andy"
	p.sex = "man"
	p.id = "123456789"
	p.age = 18
    fmt.Printf("%+v\\n",p)
    //{name:Andy sex:man id:123456789 age:18}
  1. 声明变量的时候,直接赋值
var p1 person = person{"Andy","man","123456789",18}
//初始化字段的值必须和结构体声明的字段顺序一致,否则初始化的结构体可能和你预想的不同
//你也可以使用下面的这种形式,字段:字段的值
var p2 person = person{name:"Andy",sex:"man",id:"123456789",age:18}

总体来说,实例化结构体仅仅有上面的两种形式,下面的两种形式其实和上方有些类似
3. 采用内置函数new得到一个结构体指针,使用结构体指针,初始化结构体的字段

var p3 *person = new(person)
//实际中采用指针的方式访问字段需要先解引用:(*p3).name="Andy"
//但是我们可以省略解引用的步骤,想使用变量一样使用指针,
//这是因为Go的编译器做了优化,会替我们自动转换为正确的形式
	p3.name = "Andy"
	p3.sex = "man"
	p3.id = "1234567"
	p3.age = 17
var p4 *person = &person{name: "Andy", sex: "man", 
id: "123456789", age: 18}

内存分布

结构体的所有字段是连续的

type person struct {
	name string
	age  int
	sex  string
}

func main() {
	var p person = person{"shusheng", 17, "man"}
	fmt.Println(unsafe.Sizeof(p.name))
	fmt.Println(unsafe.Sizeof(p.age))
	fmt.Println(unsafe.Sizeof(p.sex))
    fmt.Printf("%p  ",&p)
    fmt.Println(&(p.name), &(p.age), &(p.sex))

}
16
8
16
0xc42005c150  0xc42005c150 0xc42005c160 0xc42005c168

在这里插入图片描述

匿名结构体与匿名字段

匿名结构体
匿名结构体就是没有名字的结构体,无须通过type关键字定义就可以直接使用。创建匿名结构体时,同时要创建对象。匿名结构体由结构体定义和键值对初始化两部分组成

p := struct {
		name, sex string
		age       int
	}{"Andy", "man", 18}
//定义匿名结构体的形式
结构体变量名 := struct{
    结构体字段
}{字段对应的初始值}

匿名字段
所谓的匿名字段就是在结构体中没有字段名称,仅有数据类型,并且该数据类型的匿名字段仅有一个,匿名字段其实会默认使用类型作为字段名

type person struct{
    name string
    string
}
var p1 person = person{"ee","S"}
	fmt.Printf("%+v\\n", p1)
        var p2 person
        p2.name = "shusheng"
        p2.string = "golang"
	fmt.Printf("%+v\\n", p2)
//{name:ee string:S}
//{name:shusheng string:golang}

方法

方法其本质就是我们所说的函数,只不过在Go中,方法绑定了具体的类型,只有特定类型的对象才能够调用这个方法
通过上面,我们就可以直到方法和函数就有一定的区别的

1.含义,作用:
函数是用于实现特定功能的代码集合
方法是一类事物实现特定行为的代码集合
2.调用
函数调用的时候没有指定的参与者
方法必须通过其绑定类型的对象进行调用
3.函数名一定不能重复,方法名可以重复,需要绑定不同的类型

方法定义的基础语法

func (绑定变量名 绑定变量数据类型) 方法名 (形参列表) (返回值列表){
      方法体
}

eg:

type person struct {
	name string
	string
}
//为结构体person绑定一个方法Run
func (p person) Run() {
	fmt.Println(p.name, "正在跑步")
}
//使用
var p2 person
	p2.name = "shusheng"
	p2.string = "golang"
	fmt.Printf("%+v\\n", p2)
	p2.Run()

注:
我们不仅可以为结构体绑定方法,也可以为非结构体绑定方法,既可以为任何类型(除了非本地类型(其他包的类型),包括内建类型:eg:int 类型不能有方法)绑定方法,但是可以重新type,绑定方法(eg:type INT int可以为INT类型绑定方法)
绑定变量可以是个指针类型

func (p *person) Run() {
	fmt.Println(p.name, "正在跑步")
}

继承

我们仅需要在结构体中包含父类的匿名结构体

type person struct {
	name string
	sex string
}
type student struct{
        person //包含匿名结构体,即继承了person
        id int
}
//
var s student = student{person{"ss","man"},7 }
	fmt.Printf("%+v\\n", s)
//{person:{name:ss sex:man} id:7}

当我们在结构体中包含了匿名结构体,匿名结构体实现的字段和方法都会被继承下来,我们可以调用继承下来的方法

type person struct {
	name string
	sex string
}
type student struct{
        person
        id int
}

func (p person) Run() {
	fmt.Println(p.name, "正在跑步")
}

func main() {

        var s student = student{person{"ss","man"},7 }
	    fmt.Printf("%+v\\n", s)
        s.Run() //调用方法Run
}
//ss 正在跑步

方法重写:

方法重写是指一个包含了匿名字段的struct也实现了该匿名字段实现的方法,我们再次调用这个方法时,会采用“就近原则”,调用我们这个结构体的方法

type person struct {
	name string
	sex string
}
type student struct{
        person
        id int
}

func (p person) Run() {
	fmt.Println(p.name, "正在跑步")
}
//结构体student也实现了方法Run
func (s student) Run() {
	fmt.Println(s.name, "正在跑步.....")
}
func main() {

        var s student = student{person{"ss","man"},7 }
	    fmt.Printf("%+v\\n", s)
        s.Run() //调用方法Run
}

结果:

{person:{name:ss sex:man} id:7}
ss 正在跑步.....

多态:

多态是指代码可以根据类型的具体实现采取不同行为的能力

接口:

面向对象语言中,接口用于定义对象的行为。接口只指定对象应该做什么,实现这种行为的方式(实现细节)由对象来决定。
在Go语言中 ,接口定义了一组方法 ,如果某个对象实现了该接口的所有方法,则此对象就实现了该接口,Go语言的类型都是隐式实现接口的。任何定义了接口中所有方法的类型都被称为隐式地实现了该接口

接口的定义:

type 接口名 interface{
    该接口实现的方法
}

接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值,这个赋值会把用户定义的类型的值存入接口类型的值。

//定义了一个接口phone,该接口定义了一个方法call
type phone interface {
	call()
}
//定义了两个结构体类型Andorid和Iphone都实现了方法call
type Andorid struct {
	brand string
}

type Iphone struct {
	brand string
}

func (a Andorid) call() {
	fmt.Println("Andorid", a.brand, "is calling...")
}

func (i Iphone) call() {
	fmt.Println("Iphone", i.brand, "is calling...")
}
//定义了一个函数calling 调用时需要传入参数 phone接口类型的变量
func calling(p phone) {
	p.call()
}

func main() {
//我们将实现了phone接口中所有方法的结构体类型Andorid和Iphone
//的变量赋值给phone接口类型变量,编译能够通过
	var a Andorid = Andorid{"OPPO"}
	calling(a)
	var i Iphone = Iphone{"iphone4"}
	calling(i)
}
/*
结果:
Andorid OPPO is calling...
Iphone iphone4 is calling...
多态是指代码可以根据类型的具体实现采取不同行为的能力
根据上面的定义,我们发现通过接口就能够实现多态,
大家可以好好体会一下上面的代码
*/

注:
接口变量可以接收一个任何实现了该接口方法的变量,那么一种类型就可以同时实现多个接口的方法

空接口

正式由于接口具有 接口变量可以接收一个任何实现了该接口方法的变量 的特性,所以空接口(不包含任何方法的接口)可以接收任何类型的变量。

类型断言

我们在上面讨论了,可以使用接口来实现多态,即使用接口变量来调用接口内定义的方法,但是如果我们想要使用接口变量来访问brand的话,必定编译出现错误(p.brand ,当然在实现多态的时候,不会这么用),这就需要我们使用类型断言。

type phone interface {
	call()
}
//定义了两个结构体类型Andorid和Iphone都实现了方法call
type Andorid struct {
	brand string
}

type Iphone struct {
	brand string
}

func (a Andorid) call() {
	fmt.Println("Andorid", a.brand, "is calling...")
}

func (i Iphone) call() {
	fmt.Println("Iphone", i.brand, "is calling...")
}
//定义了一个函数calling 调用时需要传入参数 phone接口类型的变量
func calling(p phone) {
	p.call()
}

类型断言主要用于知道接口存放的变量的数据类型

  1. Comma-ok 断言
value,flag := 接口变量.(实际数据类型)
实际数据类型相符的话,flag为true,value是实际类型的变量
实际数据类型不服的话,flag为false
也就是说value和flag都是变量,
value存放实际数据类型的值,flag是一个bool类型变量
func calling(p phone) {
	if value, flag := p.(Iphone); flag {
		fmt.Println(value.brand)
	}

	p.call()
}
  1. switch-type
var x interface{}
switch i:=x.(type){
    case int:
         //int类型元素
    case float32:
         //float32类型元素
    default:
        //未知类型元素
}
原文链接:
https://blog.csdn.net/weixin_43519514/article/details/117092328

进阶:接口的实现

参考《Go语言实战》
接口值是一个两个字长度的数据结构,第一个字包含一个指向内部表的指针。这个内部表叫作 iTable,包含了所存储的值的类型信息。iTable 包含了已存储的值的类型信息以及与这个值相关联的一组方法。第二个字是一个指向所存储值的指针。将类型信息和指针组合在一起,就将这两个值组成了一种特殊的关系。

type phone interface {
	call()
}
//定义了一个结构体类型Andorid实现了方法call
type Andorid struct {
	brand string
}
//定义了一个函数calling 调用时需要传入参数 phone接口类型的变量
func calling(p phone) {
	p.call()
}

func main() {
//我们将实现了phone接口中所有方法的结构体类型Andorid和Iphone
//的变量赋值给phone接口类型变量,编译能够通过
	var a Andorid = Andorid{"OPPO"}
	calling(a)
}

在这里插入图片描述
注:
如果要实现接口,接收者的类型一定是值类型,不能为指针,编译器会报错

./XXX.go:28:11: cannot use p (type person) as 
type RUN in argument to running:

person does not implement RUN 
(run method has pointer receiver)

注:如果本篇博客有任何错误和建议,欢迎伙伴们留言,你快说句话啊!

以上是关于[Golang]面向对象营养餐,一文管够(封装,继承,多态)的主要内容,如果未能解决你的问题,请参考以下文章

golang学习路径03面向对象封装继承多态

golang学习路径03面向对象封装继承多态

Golang面向对象编程(下)

『GoLang』面向对象

(三十)golang--面向对象

GoLang中面向对象的三大特性