• 【Hello Go】Go语言面向对象


    严格来说 Go语言既不是一个面向对象的语言 也不是一个面向过程的语言

    所以说Go语言中并没有封装 继承 多态的概念 但是它通过一些别的方式实现了这些特性

    • 封装: 通过方法实现
    • 继承: 通过匿名字段实现
    • 多态: 通过接口实现

    匿名组合 – 继承

    匿名字段

    一般情况下 定义结构体的时候是字段和类型名是一一对应的 但是实际上Go语言也支持我们只写类型 而不写字段名的方式 被称为嵌入字段

    当匿名字段也是一个结构体的时候 那么这个结构体所拥有的全部字段都被隐式的引入了当前这个结构体

    type Person struct {
    	name string 
    	sex byte 
    	age int
    }
    
    
    type  Student Struct {
    	Person // 匿名字段 默认了Student就包含了Person的所有字段
    	id int 
    	address string
    } 
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    初始化

    这里的初始化其实就是结构体初始化 代码表示如下

    	s1 := Student{Person{"mike" , 'm', 18} , 1 , "address"}
    
    • 1

    成员操作

    成员这里也没有什么好说的

    我们可以直接通过 . 操作符去访问成员变量

    如果是匿名字段 我们可以通过 . 操作符先访问它再访问它的成员变量

    同名字段

    如果说我们使用嵌入字段的时候有重复的字段

    type Person struct {
    	name string 
    	sex byte 
    	age int
    }
    
    
    type  Student struct {
    	Person // 匿名字段 默认了Student就包含了Person的所有字段
    	name string
    	id int 
    	address string
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    那么此时 我们使用 . 赋值就只能给最外层的变量赋值 如果想要给嵌入字段的变量赋值就需要通过 . 操作符先访问该字段

    其他匿名字段

    非结构体类型和指针类型

    所有的内置类型和自定义类型都可以作为匿名字段

    声明和初始化方式和上面并无区别

    方法 – 封装

    概述

    面向对象编程中 一个对象其实也就是一个简单的值或者是一个变量

    在这个对象中会包含一些函数 这个都带有接受者的函数 我们称之为方法

    本质上 一个方法则是一个和特殊类型关联的函数

    一个面向对象的程序会使用方法来表达其属性和对应的操作 这样使用这个对象的用户就不必去直接操作对象 而是使用方法去完成这些操作


    在Go语言中 我们可以给任意自定义类型(包括内置类型 但是不包括指针类型) 添加相应的方法

    方法总是绑定对象实例 并且隐式的将实例作为第一实参 方法的语法如下

    func (receiver ReceiverType) funcName(parameters) (results)
    
    • 1
    • 参数 recevier 可以任意命名 如果方法中未使用 则可省略方法名
    • 参数 recevier 类型可以是T或者是 *T
    • 不支持重载方法

    为类型添加方法

    基础类型作为接受者

    我们下面将int 重新命名 之后给他重写了一个方法

    之后我们就可以在函数中调用该方法来实现各种功能

    package main
    
    import "fmt"
    
    type Myint int
    
    // 在函数定义嵌 在其名字之间放上一个变量 即是一个方法
    func (a Myint) Add(b Myint) (result Myint) {
    	return a + b
    }
    
    func main() {
    	var a Myint = 1
    
    	fmt.Println("a.Add(2) = ", a.Add(2)) // 答案是3
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    我们从上面的例子就能看出 面向对象只是换了一种语法形式来表达 方法是函数的语法糖

    因为receviver其实就是方法所接受的第一个参数

    ** 需要注意的是 虽然方法的名字一模一样 但是如果接受者不一样 那么方法就不一样**

    结构体作为接受者

    方法里面可以访问接收者的字段 调用方法通过点(.)来访问 就像struct里面访问字段一样

    type Person struct {
    	name string
    	sex  byte
    	age  int
    }
    
    func (P Person) Print()  {
    	fmt.Println(P.age , P.name , P.sex)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    之后如果我们定义了一个Person对象 我们就可以使用Print方法

    值语义和引用语义

    这里和之前的结构体传参一样 如果我们使用的是值传递 那么就算我们在方法里面修改内部的变量 也不会有什么改变(因为本质就是一个临时拷贝)

    如果是指针传递 那么效果就类似于C++中的引用传递

    方法集

    类型的方法集是指可以被该类型的值调用的所有方法的集合

    用实例value 和 pointer 调用方法 不受方法集的约束 编译器总是查找全部方法 并且自动转化为 recevier接受参数

    *T类型方法集

    一个指向自定义类型的指针 它的方法集是由该类型定义的所有方法组成 无论这些方法接受的是一个值还是一个指针

    如果指针上调用一个接受值的方法 那么Go语言会自动将该指针解引用 并且将值作为参数传递给方法的接受者

    类型*T方法集包含全部recevierT +*T的方法


    类型T的方法集

    和指针方法集对应的是 如果我们只有一个值 仍然可以借助于Go语言的传值的地址能力调用指针方法

    总结下

    • Go语言很 “聪明” 它知道你调用的方法是传值还是指针的
    • Go语言有值和地址的转化能力 所以说我们无需手动转换

    匿名字段

    方法的继承

    如果说匿名字段实现了一个方法 那么包含这个匿名字段的struct也能调用该方法

    type Person struct {
    	name string
    	sex  byte
    	age  int
    }
    
    func (P Person) Print()  {
    	fmt.Println(P.age , P.name , P.sex)
    }
    
    type Student struct{
    	Person 
    	id int
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    此时我们Student结构体也可以调用Person的方法

    方法的重写

    由于局部性原则 值和指针会优先访问最近的作用域

    所以说如果我们结构体和嵌入字段有相同的方法 那么我们调用该方法时就会优先使用当前结构体的方法

    如果我们想要使用嵌入字段的方法 那么我们应该先使用 . 操作符指定该字段 之后在调用方法

    表达式

    类似于我们可以对于函数进行赋值和传递一样 方法也可以进行赋值和传递

    根据调用者不同 方法分为两种形式 方法值和方法表达式 这两者都可以像普通函数那样赋值和传参 区别在于方法值绑定实例 而方法表达式则须显式传参


    首先我们来了解下方法表达式和值传参的语法

    值传参

    pFunc1 := p.PrintInfoPointer //方法值,隐式传递 receiver
    
    • 1

    方法表达式

    pFunc1 := (*Person).PrintInfoPointer
    
    • 1

    他们的区别就是调用方式

    如果我们使用值传参我们可以直接调用

    如果是方法表达式 则我们需要传递参数 pFunc1(&p)

    接口 – 多态

    概述

    在Go语言中 接口 是一个自定义类型 接口类型具体描述了一系列方法的集合

    接口类型是一种抽象的类型 它不会暴露出自己所代表的对象的值的结构和这个对象支持的基础操作的集合 他们只会展示出自己的方法 因此我们无法实例化接口类型

    Go语言通过接口实现了鸭子类型 – 当一只鸟看起来像鸭子 走起来像鸭子 吃起来也像鸭子 那么我们就认为这只鸟是鸭子

    我们不关心对象是什么类型 是不是鸭子 只关心行为

    接口的使用

    接口定义

    接口定义的语法如下

    type Humaner interface {
    	Sayhi()
    }
    
    • 1
    • 2
    • 3
    • 接口命名习惯以er结尾
    • 接口只有方法声明 没有实现 没有数据字段
    • 接口可以匿名嵌入到其他接口 或者结构中
    接口的实现

    接口是用来定义行为的类型 这些被定义的行为不由接口直接实现 而是通过方法由用户定义的类型实现 一个实现了这些方法的具体类型 就是该接口的实例

    如果用户定义的类型 实现了接口类型定义的一组方法 那么这个用户定义类型的值就可以赋给该接口类型的值

    这个赋值会把用户定义类型的值存入接口类型的值

    type Humaner interface {
    	Sayhi()
    }
    
    type Student struct {
    	name string
    	id   int
    }
    
    type Teacher struct {
    	name string
    	id   int
    }
    
    
    // 两个自定类型都实现自己的Sayhi方法
    func (s *Student) Sayhi() {
    	fmt.Println("student say hi")
    }
    
    func (t* Teacher) Sayhi() {
    	fmt.Println("teacher say hi")
    }
    
    
    // 普通函数 参数为Humaner类型的变量i
    func WhoSayhi(i Humaner) {
    	i.Sayhi()
    }
    
    func main() {
    	t := &Teacher{"mike" , 1}
    	s := &Student{"KK" , 2}
    
    	WhoSayhi(t)
    	WhoSayhi(s)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    解释下上面的代码

    • 我们首先定义了一个接口 Human
    • 接着我们实现了两个结构体 Student 和 Teacher
    • 我们给两个结构体 Student 和 Teacher 各实现了一个抽象方法
    • 接着实现一个普通函数 函数的参数就是Human 函数内部让对象调用sayhi
    • 最后我们分别将student和teacher定义的对象传入其中 我们会惊喜的发现 我们通过结构实现了多态

    接口组合

    接口嵌入

    如果说interface1作为interface2的一个嵌入字段 那么在interface2中就隐式的包含了interface1里面的所有函数

    接口转换

    超集接口可以转化为子集接口 而子集接口不能转化为超集接

    就拿上面的interface1和interface2举例 in1是子集 in2是超集

    所以in2能转化为in1 反之则不行

    空接口

    空接口(interface{})不包含任何的方法 正因为如此 所有的类型都实现了空接口 因此 空接口可以存储任意类型的数值 它有点类似于C语言的void* 类型

    当函数中可以接受任意对象实例的时候 我们会将其声明为interface{} 最典型的例子就是标准库中的Print系列函数

    func Println(args ...interface{})
    
    • 1

    类型查询

    我们知道interface类型的变量可以存储任意类型的数值 那么我们怎么反向知道存储的是什么类型呢? 目前常用的有两种方法

    • comma-ok 断言
    • switch 测试
    comma-ok 断言

    Go语言中有一个语法 可以直接判断是否是该类型的变量

    代码表示如下

    value, ok = element.(T)
    • 1

    标识符含义如下

    • value : 变量的值
    • ok : bool类型的值
    • element : interface变量
    • T : 断言的类型

    如果element中确实存储了T类型的变量 那么ok返回true 否则返回false

    switch测试

    switch测试没有什么好讲解的 就是我们拿到value之后一个个测类型即可

  • 相关阅读:
    全球观之地理部分
    【PAT(甲级)】1050 String Subtraction(用map<char,int>标记字符)
    static和extern
    口袋参谋:99.9999%商家都在用的,下拉词搜索工具!
    机器学习-集成学习LightGBM
    关于 Invalid bound statement (not found): 错误的解决
    对Transformer中Add&Norm层的理解
    react 函数组件 +类组件注意事项(基础)
    svelte初探-中
    架构设计系列4:如何设计高性能架构
  • 原文地址:https://blog.csdn.net/meihaoshy/article/details/134473646