当前位置: 首页 > news >正文

【golang】接口类型(interface)使用和原理

接口类型的类型字面量与结构体类型的看起来有些相似,它们都用花括号包裹一些核心信息。只不过,结构体类型包裹的是它的字段声明,而接口类型包裹的是它的方法定义。

接口类型声明中的这些方法所代表的就是该接口的方法集合。一个接口的方法集合就是它的全部特征。

对于任何数据类型,只要它的方法集合中完全包含了一个接口的全部特征(即全部的方法),那么它就一定是这个接口的实现类型。比如下面这样:

type Pet interface {SetName(name string)Name() stringCategory() string
}

上面代码声明了一个接口类型Pet,它包含了 3 个方法定义,方法名称分别为SetName、Name和Category。这 3 个方法共同组成了接口类型Pet的方法集合。

只要一个数据类型的方法集合中有这3个方法,那么它就一定是Pet接口的实现类型。这是一种无侵入式的接口实现方式。这种方式还有一个专有名词,叫"Duck typing",中文常译作"鸭子类型"

怎样判定一个数据类型的某一个方法实现的就是某个接口类型中的某个方法呢?

这有两个充分必要条件,一个是“两个方法的签名需要完全一致”,另一个是“两个方法的名称要一模一样”。显然,这比判断一个函数是否实现了某个函数类型要更加严格一些。

type Pet interface {SetName(name string)Name() stringCategory() string
}type Dog struct {name string // 名字。
}func (dog *Dog) SetName(name string) {dog.name = name
}func (dog Dog) Name() string {return dog.name
}func (dog Dog) Category() string {return "dog"
}

上面代码声明的类型Dog附带了3个方法。其中有两个值方法,分别是NameCategory,另外还有一个指针方法SetName

也就是说,它拥有Dog类型附带的所有值方法和指针方法。又由于这 3 个方法恰恰分别是Pet接口中某个方法的实现,所以*Dog类型就成为了Pet接口的实现类型。

dog := Dog{"little pig"}
var pet Pet = &dog

正因为如此,可以声明并初始化一个Dog类型的变量dog,然后把它的指针值赋给类型为Pet的变量pet

对于一个接口类型的变量来说,例如上面的变量pet,我们赋给它的值可以被叫做它的实际值(也称动态值),而该值的类型可以被叫做这个变量的实际类型(也称动态类型)

动态类型这个叫法是相对于静态类型而言的。对于变量pet来讲,它的静态类型就是Pet,并且永远是Pet,但是它的动态类型却会随着我们赋给它的动态值而变化。

比如,只有把一个 * Dog类型的值赋给变量pet之后,该变量的动态类型才会是 * Dog。如果还有一个Pet接口的实现
类型 * Fish,并且我又把一个此类型的值赋给了pet,那么它的动态类型就会变为* Fish。

在我们给一个接口类型的变量赋予实际的值之前,它的动态类型是不存在的。

当我们为一个接口变量赋值时会发生什么?

为了突出问题,我把Pet接口的声明简化了一下。

type Pet interface {Name() stringCategory() string
}
type Dog struct {name string // 名字。
}
func (dog *Dog) SetName(name string) {dog.name = name
}
func (dog Dog) Name() string {return dog.name
}
func (dog Dog) Category() string {return "dog"
}

现在,我先声明并初始化了一个Dog类型的变量dog,这时它的name字段的值是"littlepig"。然后,我把该变量赋给了一个Pet类型的变量pet。最后我通过调用dog的方法,SetName把它的name字段的值改成了"monster"。

dog := Dog{"little pig"}
var pet Pet = dog
dog.SetName("monster")

在以上代码执行后,pet变量的字段name的值会是什么?

答案是:pet变量的字段name的值依然是"little pig"。

问题解析

首先,由于dog的SetName方法是指针方法,所以该方法持有的接收者就是指向dog的指针值的副本,因而其中对接收者的name字段的设置就是对变量dog的改动。那么当dog.SetName(“monster”)执行之后,dog的name字段的值就一定是"monster"。如果你理解到了这一层,那么请小心前方的陷阱。

为什么dog的name字段值变了,而pet的却没有呢?这里有一条通用的规则需要你知晓:如果我们使用一个变量给另外一个变量赋值,那么真正赋给后者的,并不是前者持有的那个值,而是该值的一个副本。

例如,我声明并初始化了一个Dog类型的变量dog1,这时它的name是"little pig"。然后,我在把dog1赋给变量dog2之后,修改了dog1的name字段的值。这时,dog2的name字段的值是什么?

dog1 := Dog{"little pig"}
dog2 := dog1
dog1.name = "monster"

答案是:这时的dog2的name仍然会是"little pig"。

当你知道了这条通用规则之后,确实可以把前面那道题做对。不过,如果当我问你为什么的时候你只说出了这一个原因,那么,我只能说你仅仅答对了一半。

另外一半就需要从接口类型值的存储方式和结构说起了。我在前面说过,接口类型本身是无法被值化的。在我们赋予它实际的值之前,它的值一定会是nil,这也是它的零值。

一旦它被赋予了某个实现类型的值,它的值就不再是nil了。不过要注意,即使我们像前面那样把dog的值赋给了pet,pet的值与dog的值也是不同的。这不仅仅是副本与原值的那种不同。

当我们给一个接口变量赋值的时候,该变量的动态类型会与它的动态值一起被存储在一个专用的数据结构中。

严格来讲,这样一个变量的值其实是这个专用数据结构的一个实例,而不是我们赋给该变量的那个实际的值。所以我才说,pet的值与dog的值肯定是不同的,无论是从它们存储的内容,还是存储的结构上来看都是如此。不过,我们可以认为,这时pet的值中包含了dog值的副本。

我们就把这个专用的数据结构叫做iface吧,在 Go 语言的runtime包中它其实就叫这个名字。

iface的实例会包含两个指针,一个是指向类型信息的指针,另一个是指向动态值的指针。这里的类型信息是由另一个专用数据结构的实例承载的,其中包含了动态值的类型,以及使它实现了接口的方法和调用它们的途径,等等。

总之,接口变量被赋予动态值的时候,存储的是包含了这个动态值的副本的一个结构更加复杂的值。

接口变量的值在什么情况下才真正为nil?

先看段代码:

    var dog1 *Dogfmt.Println("The first dog is nil. [wrap1]")dog2 := dog1fmt.Println("The second dog is nil. [wrap1]")var pet Pet = dog2if pet == nil {fmt.Println("The pet is nil. [wrap1]")} else {fmt.Println("The pet is not nil. [wrap1]")}

上述代码中,先声明了一个 * Dog 类型的变量dog1,并且没有对它进行初始化。这时该变量的值是什么?显然是nil。然后我把该变量赋给了dog2,后者的值此时也必定是nil。

问题来了:当我把dog2赋给Pet类型的变量pet之后,变量pet的值会是什么?答案是nil吗?

当我们把dog2的值赋给变量pet的时候,dog2的值会先被复制,不过由于在这里它的值是nil,所以就没必要复制了。

然后,Go 语言会用我上面提到的那个专用数据结构iface的实例包装这个dog2的值的副本,这里是nil。

虽然被包装的动态值是nil,但是pet的值却不会是nil,因为这个动态值只是pet值的一部分而已。

便说一句,这时的pet的动态类型就存在了,是 * Dog。我们可以通过fmt.Printf函数和占位符%T来验证这一点,另外reflect包的TypeOf函数也可以起到类似的作用。

我们把nil赋给了pet,但是pet的值却不是nil。

这很奇怪对吗?其实不然。在 Go 语言中,我们把由字面量nil表示的值叫做无类型的nil。这是真正的nil,因为它的类型也是nil的。虽然dog2的值是真正的nil,但是当我们把这个变量赋给pet的时候,Go 语言会把它的类型和值放在一起考虑。

这时 Go 语言会识别出赋予pet的值是一个 * Dog类型的nil。然后,Go 语言就会用一个iface的实例包装它,包装后的产物肯定就不是nil了。

只要我们把一个有类型的nil赋给接口变量,那么这个变量的值就一定不会是那个真正的nil。因此,当我们使用判等符号==判断pet是否与字面量nil相等的时候,答案一定会是false。

那么,怎样才能让一个接口变量的值真正为nil呢?要么只声明它但不做初始化,要么直接把字面量nil赋给它。

怎样实现接口之间的组合?

接口类型间的嵌入也被称为接口的组合。我在前面讲过结构体类型的嵌入字段,这其实就是在说结构体类型间的嵌入。

接口类型间的嵌入要更简单一些,因为它不会涉及方法间的“屏蔽”。只要组合的接口之间有同名的方法就会产生冲突,从而无法通过编译,即使同名方法的签名彼此不同也会是如此。因此,接口的组合根本不可能导致“屏蔽”现象的出现。

与结构体类型间的嵌入很相似,我们只要把一个接口类型的名称直接写到另一个接口类型的成员列表中就可以了。比如:

type Animal interface {ScientificName() stringCategory() string
}
type Pet interface {AnimalName() string
}

接口类型Pet包含了两个成员,一个是代表了另一个接口类型的Animal,一个是方法Name的定义。它们都被包含在Pet的类型声明的花括号中,并且都各自独占一行。此时,Animal接口包含的所有方法也就成为了Pet接口的方法。

Go 语言团队鼓励我们声明体量较小的接口,并建议我们通过这种接口间的组合来扩展程序、增加程序的灵活性。
这是因为相比于包含很多方法的大接口而言,小接口可以更加专注地表达某一种能力或某一类特征,同时也更容易被组合在一起。

Go 语言标准库代码包io中的ReadWriteCloser接口和ReadWriter接口就是这样的例子,它们都是由若干个小接口组合而成的。以io.ReadWriteCloser接口为例,它是由io.Reader、io.Writer和io.Closer这三个接口组成的。

这三个接口都只包含了一个方法,是典型的小接口。它们中的每一个都只代表了一种能力,分别是读出、写入和关闭。我们编写这几个小接口的实现类型通常都会很容易。并且,一旦我们同时实现了它们,就等于实现了它们的组合接口io.ReadWriteCloser。

即使我们只实现了io.Reader和io.Writer,那么也等同于实现了io.ReadWriter接口,因为后者就是前两个接口组成的。可以看到,这几个io包中的接口共同组成了一个接口矩阵。它们既相互关联又独立存在。

文章学习自郝林老师的《Go语言36讲》

相关文章:

【golang】接口类型(interface)使用和原理

接口类型的类型字面量与结构体类型的看起来有些相似,它们都用花括号包裹一些核心信息。只不过,结构体类型包裹的是它的字段声明,而接口类型包裹的是它的方法定义。 接口类型声明中的这些方法所代表的就是该接口的方法集合。一个接口的方法集…...

【Linux操作系统】Linux系统编程中的共享存储映射(mmap)

在Linux系统编程中,进程之间的通信是一项重要的任务。共享存储映射(mmap)是一种高效的进程通信方式,它允许多个进程共享同一个内存区域,从而实现数据的共享和通信。本文将介绍共享存储映射的概念、原理、使用方法和注意…...

2235.两整数相加:19种语言解法(力扣全解法)

【LetMeFly】2235.两整数相加:19种语言解法(力扣全解法) 力扣题目链接:https://leetcode.cn/problems/add-two-integers/ 给你两个整数 num1 和 num2,返回这两个整数的和。 示例 1: 输入:num…...

中国剩余定理及扩展

目录 中国剩余定理解释 中国剩余定理扩展——求解模数不互质情况下的线性方程组: 代码实现: 互质: 非互质: 中国剩余定理解释 在《孙子算经》中有这样一个问题:“今有物不知其数,三三数之剩二&#x…...

数据在内存中的存储(deeper)

数据在内存中的存储(deeper) 一.数据类型的详细介绍二.整形在内存中的存储三.浮点型在内存中的存储 一.数据类型的详细介绍 类型的意义: 使用这个类型开辟内存空间的大小(大小决定了使用范围)如何看待内存空间的视角…...

算法修炼Day52|● 300.最长递增子序列 ● 674. 最长连续递增序列 ● 718. 最长重复子数组

LeetCode:300.最长递增子序列 300. 最长递增子序列 - 力扣(LeetCode) 1.思路 dp[i]的状态表示以nums[i]为结尾的最长递增子序列的个数。 dp[i]有很多个,选择其中最大的dp[i]Math.max(dp[j]1,dp[i]) 2.代码实现 1class Solution {2 pub…...

使用 HTML、CSS 和 JavaScript 创建实时 Web 编辑器

使用 HTML、CSS 和 JavaScript 创建实时 Web 编辑器 在本文中,我们将创建一个实时网页编辑器。这是一个 Web 应用程序,允许我们在网页上编写 HTML、CSS 和 JavaScript 代码并实时查看结果。这是学习 Web 开发和测试代码片段的绝佳工具。我们将使用ifram…...

百望云联合华为发布票财税链一体化数智解决方案 赋能企业数字化升级

随着数据跃升为数字经济关键生产要素,数据安全成为整个数字化建设的重中之重。为更好地帮助企业发展,中央及全国和地方政府相继出台了多部与数据相关的政策法规,鼓励各领域服务商提供具有自主创新的软件产品与服务,帮助企业在合规…...

实现两个栈模拟队列

实现两个栈模拟队列 思路:可以想象一下左手和右手,两个栈:stack1(数据所在的栈) ,stack2(临时存放)。 入队:需要将入队 num 加在 stack1 的栈顶即可; 出队&am…...

无涯教程-TensorFlow - 单词嵌入

Word embedding是从离散对象(如单词)映射到向量和实数的概念,可将离散的输入对象有效地转换为有用的向量。 Word embedding的输入如下所示: blue: (0.01359, 0.00075997, 0.24608, ..., -0.2524, 1.0048, 0.06259) blues: (0.01396, 0.11887, -0.48963, ..., 0.03…...

Facebook AI mBART:巴别塔的硅解

2018年,谷歌发布了BERT(来自transformers的双向编码器表示),这是一种预训练的语言模型,在一系列自然语言处理(NLP)任务中对SOTA结果进行评分,并彻底改变了研究领域。类似的基于变压器…...

BDA初级分析——SQL清洗和整理数据

一、数据处理 数据处理之类型转换 字符格式与数值格式存储的数据,同样是进行大小排序, 会有什么区别? 以rev为例,看看字符格式与数值格式存储时,排序会有什么区别? 用cast as转换为字符后进行排序 SEL…...

汽车后视镜反射率测定仪

后视镜是驾驶员坐在驾驶室座位上直接获取汽车后方、侧方和下方等外部信息的工具。它起着“第三只眼睛”的作用。后视镜按安装位置划分通常分为车外后视镜、监视镜和内后视镜。外后视镜观察汽车后侧方监视镜观察汽车前下方内后视镜观察汽车后方及车内情况。用途不一样镜面结构也…...

Redis学习笔记

redis相关内容 默认端口6379 默认16个数据库,初始默认使用0号库 使用select 切换数据库 统一密码管理,所有库密码相同 dbsize:查看当前库key的数量 flushdb:清空当前库 flushall:清空全部库 redis是单线程 多路…...

韩顺平Linux 四十四--

四十四、rwx权限 权限的基本介绍 输入指令 ls -l 显示的内容如下 -rwxrw-r-- 1 root 1213 Feb 2 09:39 abc0-9位说明 第0位确定文件类型(d , - , l , c , b) l 是链接,相当于 windows 的快捷方式- 代表是文件是普通文件d 是目录,相…...

【支付宝小程序】分包优化教程

🦖我是Sam9029,一个前端 Sam9029的CSDN博客主页:Sam9029的博客_CSDN博客-JS学习,CSS学习,Vue-2领域博主 🐱‍🐉🐱‍🐉恭喜你,若此文你认为写的不错,不要吝啬你的赞扬,求收…...

语言基础2 矩阵和数组

语言基础2 矩阵和数组 矩阵和数组是matlab中信息和数据的基本表示形式 可以创建常用的数组和网格 合并现有的数组 操作数组的形状和内容 以及使用索引访问数组元素 用到的函数列表如下 一 创建 串联和扩展矩阵 矩阵时按行和列排列的数据元素的二维数据元素的二维矩…...

springMVC中过滤器抛出异常,自定义异常捕获

在过滤器中引入org.springframework.web.servlet.HandlerExceptionResolver AutowiredQualifier("handlerExceptionResolver")private HandlerExceptionResolver resolver; // doFilter中处理if (条件1) {if (条件2) {resolver.resolveException(request, response, …...

图像检索技术研究:深度度量与深度散列在相似性学习中的应用比较与实践 - 使用Python与Jupyter环境

引言 在计算机视觉领域,图像检索是一个长期存在并持续受到研究者关注的重要话题。随着大数据时代的到来,如何高效、准确地从海量数据中检索到相似的图像成为一个巨大的挑战。传统的检索方法在大数据环境下表现不佳,而深度学习技术的崛起为图…...

CSS加载失败的6个原因

有很多刚刚接触 CSS 的新手有时会遇到 CSS 加载失败这个问题,但测试时,网页上没有显示该样式的问题,这就说明 CSS 加载失败了。出现这种状况一般是因为的 CSS 路径书写错,或者是在浏览器中禁止掉了 CSS 的加载,可以重新…...

脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)

一、数据处理与分析实战 (一)实时滤波与参数调整 基础滤波操作 60Hz 工频滤波:勾选界面右侧 “60Hz” 复选框,可有效抑制电网干扰(适用于北美地区,欧洲用户可调整为 50Hz)。 平滑处理&…...

.Net框架,除了EF还有很多很多......

文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...

练习(含atoi的模拟实现,自定义类型等练习)

一、结构体大小的计算及位段 (结构体大小计算及位段 详解请看:自定义类型:结构体进阶-CSDN博客) 1.在32位系统环境,编译选项为4字节对齐,那么sizeof(A)和sizeof(B)是多少? #pragma pack(4)st…...

c++ 面试题(1)-----深度优先搜索(DFS)实现

操作系统:ubuntu22.04 IDE:Visual Studio Code 编程语言:C11 题目描述 地上有一个 m 行 n 列的方格,从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子,但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

高等数学(下)题型笔记(八)空间解析几何与向量代数

目录 0 前言 1 向量的点乘 1.1 基本公式 1.2 例题 2 向量的叉乘 2.1 基础知识 2.2 例题 3 空间平面方程 3.1 基础知识 3.2 例题 4 空间直线方程 4.1 基础知识 4.2 例题 5 旋转曲面及其方程 5.1 基础知识 5.2 例题 6 空间曲面的法线与切平面 6.1 基础知识 6.2…...

Spring Boot面试题精选汇总

🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...

【python异步多线程】异步多线程爬虫代码示例

claude生成的python多线程、异步代码示例,模拟20个网页的爬取,每个网页假设要0.5-2秒完成。 代码 Python多线程爬虫教程 核心概念 多线程:允许程序同时执行多个任务,提高IO密集型任务(如网络请求)的效率…...

C++ Visual Studio 2017厂商给的源码没有.sln文件 易兆微芯片下载工具加开机动画下载。

1.先用Visual Studio 2017打开Yichip YC31xx loader.vcxproj,再用Visual Studio 2022打开。再保侟就有.sln文件了。 易兆微芯片下载工具加开机动画下载 ExtraDownloadFile1Info.\logo.bin|0|0|10D2000|0 MFC应用兼容CMD 在BOOL CYichipYC31xxloaderDlg::OnIni…...

Reasoning over Uncertain Text by Generative Large Language Models

https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829 1. 概述 文本中的不确定性在许多语境中传达,从日常对话到特定领域的文档(例如医学文档)(Heritage 2013;Landmark、Gulbrandsen 和 Svenevei…...

逻辑回归暴力训练预测金融欺诈

简述 「使用逻辑回归暴力预测金融欺诈,并不断增加特征维度持续测试」的做法,体现了一种逐步建模与迭代验证的实验思路,在金融欺诈检测中非常有价值,本文作为一篇回顾性记录了早年间公司给某行做反欺诈预测用到的技术和思路。百度…...