Go 面向对象,封装、继承、多态

Go 面向对象,封装、继承、多态

经典OO(Object-oriented 面向对象)的三大特性是封装、继承与多态,这里我们看看Go中是如何对应的。

1. 封装

封装就是把数据以及操作数据的方法“打包”到一个抽象数据类型中,这个类型封装隐藏了实现的细节,所有数据仅能通过导出的方法来访问和操作。这个抽象数据类型的实例被称为对象。经典OO语言,如Java、C++等都是通过类(class)来表达封装的概念,通过类的实例来映射对象的。熟悉Java的童鞋一定记得**《Java编程思想》**一书的第二章的标题:“一切都是对象”。在Java中所有属性、方法都定义在一个个的class中。

Go语言没有class,那么封装的概念又是如何体现的呢?来自OO语言的初学者进入Go世界后,都喜欢“对号入座”,即Go中什么语法元素与class最接近!于是他们找到了struct类型。

Go中的struct类型中提供了对真实世界聚合抽象的能力,struct的定义中可以包含一组字段(field),如果从OO角度来看,你也可以将这些字段视为属性,同时,我们也可以为struct类型定义方法(method),下面例子中我们定义了一个名为Point的struct类型,它拥有一个导出方法Length:

type Point struct {x, y float64
}func (p Point) Length() float64 {return math.Sqrt(p.x * p.x + p.y * p.y)
}

我们看到,从语法形式上来看,与经典OO声明类的方法不同,Go方法声明并不需要放在声明struct类型的大括号中。Length方法与Point类型建立联系的纽带是一个被称为receiver参数的语法元素。

那么,struct是否就是对应经典OO中的类呢? 是,也不是!从数据聚合抽象来看,似乎是这样, struct类型可以拥有多个异构类型的、代表不同抽象能力的字段(比如整数类型int可以用来抽象一个真实世界物体的长度,string类型字段可以用来抽象真实世界物体的名字等)。

但从拥有方法的角度,不仅是struct类型,Go中除了内置类型的所有其他具名类型都可以拥有自己的方法,哪怕是一个底层类型为int的新类型MyInt:

type MyInt intfunc(a MyInt)Add(b int) MyInt {return a + MyInt(b)
}

2. 继承

就像前面说的,Go设计者在Go诞生伊始就重新评估了对经典OO的语法概念的支持,最终放弃了对诸如类、对象以及类继承层次体系的支持。也就是说:在Go中体现封装概念的类型之间都是“路人”,没有亲爹和儿子的关系的“牵绊”

谈到OO中的继承,大家更多想到的是子类继承了父类的属性与方法实现。Go虽然没有像Java extends关键字那样的显式继承语法,但Go也另辟蹊径地对“继承”提供了支持。这种支持方式就是类型嵌入(type embedding),看一个例子:

package mainimport "fmt"type P struct {A intb string
}func (P) M1() {fmt.Println("P M1")
}func (P) M2() {fmt.Println("P M2")
}type Q struct {c [5]intD float64
}func (Q) M2() {fmt.Println("Q M2")
}
func (Q) M3() {fmt.Println("Q M3")
}func (Q) M4() {fmt.Println("Q M3")
}type T struct {PQE int
}// M2 重写方法:在 T 中重写 M2 方法,明确调用哪个嵌入结构体的 M2。
func (t T) M2() {t.P.M2() // 或者 t.Q.M2()
}func main() {var t Tt.M1()//需要显式调用t.P.M2()t.Q.M2()// 或重写方法t.M2()t.M3()t.M4()println(t.A, t.D, t.E)
}

我们看到类型T通过嵌入P、Q两个类型,“继承”了P、Q的导出方法(M1~M4)和导出字段(A、D)。

不过实际Go中的这种“继承”机制并非经典OO中的继承,其外围类型(T)与嵌入的类型(P、Q)之间没有任何“亲缘”关系。P、Q的导出字段和导出方法只是被提升为T的字段和方法罢了,其本质是一种组合,是组合中的代理(delegate)模式的一种实现。T只是一个代理(delegate),对外它提供了它可以代理的所有方法,如例子中的M1~M4方法。当外界发起对T的M1方法的调用后,T将该调用委派给它内部的P实例来实际执行M1方法。

以经典OO理论话术去理解就是T与P、Q的关系不是is-a,而是has-a的关系

组合大于继承

其实这种继承更应该被称为组合。Go 更愿意将模块分成互相独立的小单元,分别处理不同方面的需求,最后以匿名嵌入的方式组合到一起,共同实现对外接口。也就是组合大于继承的思想。

组合没有父子依赖,不会破坏封装。且整体和局部松耦合,可任意增加来实现扩展。各单元持有单一职责,互不关联,自由灵活组合,实现和维护更加简单。

匿名嵌套

匿名嵌套在编译时会根据嵌套类型生成包装方法包装方法实际是调用嵌套类型的原始方法

拓:匿名嵌套的多种玩法

  • struct 匿名嵌套 struct(上面已经展示过了)
  • interface 匿名嵌套 interface
  • struct 匿名嵌套 interface

interface 匿名嵌套 interface

接口可嵌入其他匿名接口,相当于将其声明的方法集导入

当然,注意只有实现了两个接口的全部的方法,才算实现大接口哈。

Go 标准库中经典用法如下:

type Reader interface {Read(p []byte) (n int, err error)
}type Writer interface {Write(p []byte) (n int, err error)
}type ReadWriter interface {ReaderWriter
}

struct 匿名嵌套 interface

编译器自动为 struct 的方法集加上 interface 的所有方法
(后面是我猜的)如我们通过 struct.M () 调用 interface 的 M 方法,编译器实际为 struct 生成包装方法 struct.interface.M()

注意:struct 中的 interface 要记得赋值哈,不然调用时会显示 interface nil panic。

type I interface {M()
}type A struct {I
}type B struct {
}func (B) M() {print("B")
}func main() {var a A = A{I: B{}}a.M() // B// 当然 A 也是 I 接口类型var i I = A{I: B{}}i.M() // A
}

我们同时验证以下匿名嵌套的同名覆盖问题:

type I interface {M()
}type A struct {I
}type B struct {
}// 多加这个:验证匿名方法同名覆盖
func (A) M() {print("A")
}func (B) M() {print("B")
}func main() {var a A = A{I: B{}}a.M() // A
}

Go 标准库中经典用法如下:

context 包中:

type valueCtx struct {Context  // 匿名接口key, val interface{}
}// 创建 valueCtx
func WithValue(parent Context, key, val interface{}) Context {return &valueCtx{parent, key, val}
}// 实际重写了 Value() 接口,其他父 context 的方法依旧可以调用
func (c *valueCtx) Value(key interface{}) interface{} {if c.key == key {return c.val}return c.Context.Value(key)
}

3. 多态

经典OO中的多态是尤指运行时多态,指的是调用方法时,会根据调用方法的实际对象的类型来调用不同类型的方法实现。

下面是一个C++中典型多态的例子:

#include <iostream>class P {public:virtual void M() = 0;
};class C1: public P {public:void M();
};void C1::M() {std::cout << "c1.M()\n";
}class C2: public P {public:void M();
};void C2::M() {std::cout << "c2.M()\n";
}int main() {C1 c1;C2 c2;P *p = &c1;p->M(); // c1.M()p = &c2;p->M(); // c2.M()
}

这段代码比较清晰,一个父类P和两个子类C1和C2。父类P有一个虚拟成员函数M,两个子类C1和C2分别重写了M成员函数。在main中,我们声明父类P的指针,然后将C1和C2的对象实例分别赋值给p并调用M成员函数,从结果来看,在运行时p实际调用的函数会根据其指向的对象实例的实际类型而分别调用C1和C2的M。

显然,经典OO的多态实现依托的是类型的层次关系。那么对应没有了类型层次体系的Go来说,它又是如何实现多态的呢?Go使用接口来解锁多态

和经典OO语言相比,Go更强调行为聚合与一致性,而非数据。因此Go提供了对类似duck typing的支持,即基于行为集合的类型适配,但相较于ruby等动态语言,Go的静态类型机制还可以保证应用duck typing时的类型安全。

Go的接口类型本质就是一组方法集合(行为集合),一个类型如果实现了某个接口类型中的所有方法,那么就可以作为动态类型赋值给接口类型。通过该接口类型变量的调用某一方法,实际调用的就是其动态类型的方法实现。看下面例子:

type MyInterface interface {M1()M2()M3()
}type P struct {
}func (P) M1() {}
func (P) M2() {}
func (P) M3() {}type Q int 
func (Q) M1() {}
func (Q) M2() {}
func (Q) M3() {}func main() {var p Pvar q Qvar i MyInterface = pi.M1() // P.M1i.M2() // P.M2i.M3() // P.M3i = qi.M1() // Q.M1i.M2() // Q.M2i.M3() // Q.M3
}

Go这种无需类型继承层次体系、低耦合方式的多态实现,是不是用起来更轻量、更容易些呢!

Go 通过接口来实现多态。

Go 的接口类型本质就是一组方法集合 (行为集合),一个类型如果实现了某个接口类型中的所有方法,那么就可以作为动态类型赋值给接口类型(注意:定义一个接口变量,该变量本质是个 Struct 类型的变量哦)。

Go 的接口是特别重要的东西,通过学习 Go 接口的底层实现可以学到很多东西,例如动态语言的实现,方法动态派发实现等。

4. Gopher的“OO思维”

到这里,来自经典OO语言阵营的小伙伴们是不是已经找到了当初在入门Go语言时“感觉到别扭”的原因了呢!这种“别扭”就在于Go对于OO支持的方式与经典OO语言的差别:秉持着经典OO思维的小伙伴一上来就要建立的继承层次体系,但Go没有,也不需要。

要转变为正宗的Gopher的OO思维其实也不难,那就是“prefer接口,prefer组合,将习惯了的is-a思维改为has-a思维”。

5. 小结

是时候给出一些结论性的观点了:

  • Go支持OO,只是用的不是经典OO的语法和带层次的类型体系;
  • Go支持OO,只是用起来需要换种思维;
  • 在Go中玩转OO的思维方式是:“优先接口、优先组合”。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/79316.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

无线网络设备中AP和AC是什么?有什么区别?

无线网络设备中AP和AC是什么&#xff1f;有什么区别&#xff1f; 一. 什么是AP&#xff1f;二. 什么是AC&#xff1f;三. AP与AC的关系 前言 肝文不易&#xff0c;点个免费的赞和关注&#xff0c;有错误的地方请指出&#xff0c;看个人主页有惊喜。 作者&#xff1a;神的孩子都…

Android SDK

Windows纯净卸载Android SDK 1.关闭所有安卓相关的程序 Android StudioEmulators 如模拟器Command prompts using SDK 如appium服务 2.移除SDK相关目录 # Delete your SDK directory F:\android_sdk\android-sdk-windows# Also check and remove if present: $env:LOCALAPP…

Android耗电优化全解析:从原理到实践的深度治理指南

引言 在移动应用性能优化体系中&#xff0c;耗电优化是用户体验的核心指标之一。据Google官方统计&#xff0c;超过60%的用户会因为应用耗电过快而选择卸载应用。本文将从耗电统计原理、监控手段、治理策略三个维度展开&#xff0c;结合Android系统源码与实际代码示例&#xf…

QMK自定义4*4键盘固件创建教程:最新架构详解

QMK自定义4*4键盘固件创建教程&#xff1a;最新架构详解 前言 通过本教程&#xff0c;你将学习如何在QMK框架下创建自己的键盘固件。QMK是一个强大的开源键盘固件框架&#xff0c;广泛用于DIY机械键盘的制作。本文将详细介绍最新架构下所需创建的文件及其功能。 准备工作 在…

DAMA第10章深度解析:参考数据与主数据管理的核心要义与实践指南

引言 在数字化转型的浪潮中&#xff0c;数据已成为企业的核心资产。然而&#xff0c;数据孤岛、冗余和不一致问题严重制约了数据价值的释放。DAMA&#xff08;数据管理协会&#xff09;提出的参考数据&#xff08;Reference Data&#xff09;与主数据&#xff08;Master Data&…

力扣题解:2、两数相加

个人认为&#xff0c;该题目可以看作合并两个链表的变种题&#xff0c;本题与21题不同的是&#xff0c;再处理两个结点时&#xff0c;对比的不是两者的大小&#xff0c;而是两者和是否大于10&#xff0c;加法计算中大于10要进位&#xff0c;所以我们需要声明一个用来标记是否进…

深度学习部署包含哪些步骤?

深度学习部署包含哪些步骤&#xff1f; 阶段说明示例工具模型导出把 .pt、.h5 等格式模型导出为通用格式&#xff08;如ONNX&#xff09;PyTorch, TensorFlow, ONNX推理优化减小模型体积、加速推理&#xff08;量化、剪枝&#xff09;TensorRT, ONNX Runtime系统集成将模型嵌入…

路由策略和策略路由的区别以及配置案例

区别 路由策略&#xff1a;路由策略是通过ACL等方式控制路由发布&#xff0c;让对方学到适当路由条目&#xff0c;比如有20条路由&#xff0c;只想让某个路由器学到10条&#xff0c;可以通过路由策略进行过滤。 策略路由&#xff1a;策略路由是通过定义策略和应用&#xff0c…

LeetCode 热题 100 64. 最小路径和

LeetCode 热题 100 | 64. 最小路径和 大家好&#xff0c;今天我们来解决一道经典的动态规划问题——最小路径和。这道题在 LeetCode 上被标记为中等难度&#xff0c;要求找到从网格的左上角到右下角的路径&#xff0c;使得路径上的数字总和为最小。 问题描述 给定一个包含非负…

JavaSE核心知识点02面向对象编程02-06(泛型)

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;笑口常开&#x1f7ea;生日快乐⬛早点睡觉 &#x1f4d8;博主相关 &#x1f7e7;博主信息&#x1f7e8;博客首页&#x1f7eb;专栏推荐&#x1f7e5;活动信息 文章目录 JavaSE核心知识点02面向对象编程02-06&#…

LVGL对象的盒子模型和样式

文章目录 &#x1f9f1; LVGL 对象盒子模型结构&#x1f50d; 组成部分说明&#x1f3ae; 示例代码&#x1f4cc; 总结一句话 &#x1f9f1; 一、样式的本质&#xff1a;lv_style_t 对象&#x1f3a8; 二、样式应用的方式&#x1f9e9; 三、样式属性分类&#xff08;核心&#…

Github上如何准确地搜索开源项目

Github上如何准确地搜索开源项目&#xff1a; 因为寻找项目练手是最快速掌握技术的途径&#xff0c;而Github上有最全最好的开源项目。 就像我的毕业设计“机器翻译”就可以在Github上查找开源项目来参考。 以下搜索针对&#xff1a;项目名的关键词&#xff0c;关注数限制&a…

正点原子IMX6U开发板移植Qt时出现乱码

移植Qt时出现乱码 1、前言2、问题3、总结 1、前言 记录一下正点原子IMX6U开发板移植Qt时出现乱码的解决方法&#xff0c;方便自己日后回顾&#xff0c;也可以给有需要的人提供帮助。 2、问题 用正点原子IMX6U开发板移植Qt时移植Qt后&#xff0c;sd卡里已经存储了Qt的各种库&…

python-django项目启动寻找静态页面html顺序

目录结构 settings模块 urls模块 views模块 1.settings文件下没有DIR目录,按照各app注册顺序寻找静态页面 启动效果&#xff0c;直接返回注册的app即app01下的templates文件夹下的html页面 2.settings文件添加上DIR目录 启动效果&#xff0c;会优先去找项目下的templates文件…

MySQL索引详解(上)(结构/分类/语法篇)

一、索引概述 索引本质是帮助MySQL高效获取数据的排序数据结构&#xff08;类似书籍目录&#xff09;&#xff0c;通过减少磁盘I/O次数提升查询效率。其核心价值体现在大数据量场景下的快速定位能力&#xff0c;但同时带来存储和维护成本。 核心特点&#xff1a; 优点&#…

数据集-目标检测系列- 烟雾 检测数据集 smoke >> DataBall

数据集-目标检测系列- 消防 浓烟 检测数据集 smoke>> DataBall 数据集-目标检测系列- 烟雾 检测数据集 smoke &#xff1e;&#xff1e; DataBall * 相关项目 1&#xff09;数据集可视化项目&#xff1a;gitcode: https://gitcode.com/DataBall/DataBall-detections-10…

docker + K3S + Jenkins + Harbor自动化部署

最近公司在研究自动化部署的一套流程&#xff0c;下面记录一下配置流程 需要提前准备好Jenkins Harbor Git(其他管理工具也可以) 我这里的打包编译流程是Jenkins上配置打包任务-->自动到git目录下找打包文件---->项目编译后打镜像包------>打完镜像包将镜像上传到…

《用MATLAB玩转游戏开发:从零开始打造你的数字乐园》基础篇(2D图形交互)-《打砖块:向量反射与实时物理模拟》MATLAB教程

《用MATLAB玩转游戏开发&#xff1a;从零开始打造你的数字乐园》基础篇&#xff08;2D图形交互&#xff09;-《打砖块&#xff1a;向量反射与实时物理模拟》MATLAB教程 &#x1f3ae; 文章目录 《用MATLAB玩转游戏开发&#xff1a;从零开始打造你的数字乐园》基础篇&#xff08…

Redisson 看门狗机制

何为看门狗 看门狗机制的主要作用是自动续期锁&#xff0c;确保在节点完成任务之前&#xff0c;锁不会过期。具体来说&#xff0c;当一个节点获取到锁后&#xff0c;看门狗会定期检查该锁的过期时间&#xff0c;并在必要时延长锁的过期时间&#xff0c;确保节点可以顺利完成任…

[架构之美]linux常见故障问题解决方案(十九)

[架构之美]linux下常见故障问题解决方案 一&#xff0c;文本文件忙 问题一&#xff1a;rootwh-VMware-Virtual-Platform:/home/hail# cp /root/containerd/bin/* /usr/bin/ cp: 无法创建普通文件 ‘/usr/bin/containerd’: 文本文件忙 在Linux系统中遇到“文本文件忙”错误时…