【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讲》

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

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

相关文章

CSS行内,内部,外部以及优先级

1.内联样式表&#xff1a; 将样式编写到style标签里 <style>.context {color: red;} </style> 2. 行内样式&#xff1a; 在 HTML 标签中使用 style 属性设置 CSS 样式 <div style"font-size: 18px;">行内样式</div> 3.外联样式&#xff1…

【数据结构】吃透单链表!!!(详细解析~)

目录 前言&#xff1a;一.顺序表的缺陷 && 介绍链表1.顺序表的缺陷2.介绍链表&#xff08;1&#xff09;链表的概念&#xff08;2&#xff09;链表的结构&#xff08;3&#xff09;链表的功能 二.单链表的实现1.创建节点的结构2.头文件函数的声明3.函数的实现&#xff…

iPhone开启“轻点唤醒”功能但点击屏幕无反应怎么解决?

iPhone的“轻点唤醒”功能启用时&#xff0c;用户只需手指轻触或点击手机屏幕即可快速唤醒设备&#xff0c;无需按压任何按钮。然而&#xff0c;有些用户在使用“轻点唤醒”功能唤醒屏幕时&#xff0c;遇到该功能失灵&#xff0c;无法正常唤醒屏幕的情况&#xff0c;这是怎么回…

Windows系统搭建Gitblit服务器

目录 介绍安装Java环境配置环境变量 下载Gitbilt配置Gitbilt 介绍 GitBlit是一个用于搭建和管理Git服务器的开源工具。它是使用Java语言编写的&#xff0c;并且需要Java环境来运行。 1、跨平台性&#xff1a;Java是一种具有跨平台性的编程语言&#xff0c;可以在多个操作系统…

读取xml

将json文件读取成字符串 public static String getJsonStr(File jsonFile){try {FileReader fileReader new FileReader(jsonFile);Reader reader new InputStreamReader(new FileInputStream(jsonFile),"utf-8");int ch 0;StringBuffer sb new StringBuffer();…

【工具】VirtualBox虚拟机安装Windows操作系统

前面的文章中介绍了VirtualBox虚拟机的安装,VirtualBox虚拟机中如何安装操作系统,是本文的重点,下面将进行详细介绍。 使用VirtualBox虚拟机安装Windows操作系统有很多好处,主要包括以下几点: 节省资源:通过虚拟化技术,一台物理计算机可以同时运行多个虚拟机,每个虚拟…

【Terraform学习】使用 Terraform 将 EC2 实例作为 Web 服务器启动(Terraform-AWS最佳实战学习)

使用 Terraform 将 EC2 实例作为 Web 服务器启动 实验步骤 前提条件 安装 Terraform&#xff1a; 地址 下载仓库代码模版 本实验代码位于 task_ec2 文件夹中。 变量文件 variables.tf 在上面的代码中&#xff0c;您将声明&#xff0c;aws_access_key&#xff0c;aws_secr…

使用ImageMagick实现多张图片拼接为gif(多线程版)

官网: https://imagemagick.org/ 直接上代码 ExecutorService es Executors.newFixedThreadPool(10); List<File> images getImageFiles(sceneDir); CountDownLatch cdl new CountDownLatch(images.size()); // 拷贝图片 for (File file : images) {System.out.prin…

Web3 游戏七月洞察:迈向主流采用的临界点?

作者: lesleyfootprint.network 2023 年 7 月&#xff0c;Web3 游戏领域出现了小幅增长&#xff0c;但对于许多项目来说&#xff0c;用户采用仍然是一个持续的挑战。根据 Footprint Analytics 的数据&#xff0c;活跃的区块链游戏数量略有增加&#xff0c;达到 2,471 个。然而…

容器化微服务:用Kubernetes实现弹性部署

随着云计算的迅猛发展&#xff0c;容器化和微服务架构成为了构建现代应用的重要方式。而在这个过程中&#xff0c;Kubernetes&#xff08;常简称为K8s&#xff09;作为一个开源的容器编排平台&#xff0c;正在引领着容器化微服务的部署和管理革命。本文将深入探讨容器化微服务的…

C++ 好用的格式化库--fmt

背景 fmt 库是一个开源的 C 格式化库&#xff0c;它提供了一种简洁、安全和高效的方式来进行字符串格式化。该库的设计目标是提供与 Python 的字符串格式化语法类似的功能&#xff0c;同时保持 C 的类型安全性和性能。 下载与安装 官网下载 fmt 官网地址&#xff1a;https:…

elementUI自定义上传文件 前端后端超详细过程

下面是使用Element UI自定义上传文件的前后端详细过程&#xff1a; 前端过程&#xff1a; 引入Element UI组件库&#xff1a;在前端项目中引入Element UI库&#xff0c;可以通过CDN引入或者通过npm安装并导入。 创建上传组件&#xff1a;在前端代码中创建一个上传组件&#x…

opencv 进阶17-使用K最近邻和比率检验过滤匹配(图像匹配)

K最近邻&#xff08;K-Nearest Neighbors&#xff0c;简称KNN&#xff09;和比率检验&#xff08;Ratio Test&#xff09;是在计算机视觉中用于特征匹配的常见技术。它们通常与特征描述子&#xff08;例如SIFT、SURF、ORB等&#xff09;一起使用&#xff0c;以在图像中找到相似…

Git相关命令

SSH密钥文件 Github里面S设置SH公钥有两者选择方式 账号下的每个仓库都设置一个公钥&#xff0c;因为GitHub官方要求每个仓库的公钥都不能相同&#xff0c;所以每个账号都要搞一个密钥&#xff08;很麻烦&#xff09;给账号分配一个公钥&#xff0c;然后这个公钥就可以在这个…

如何将pdf文件转换成word文档?

如何将pdf文件转换成word文档&#xff1f;PDF文档是我们日常办公中最为常用的电子文档格式的文件&#xff0c;也是在会议、教育培训以及商业营销中经常使用的文档格式。所以说PDF文档的功能较强&#xff0c;且应用场景较多。但是也有例外的时候&#xff0c;比如我们需要将PDF文…

Vue的五种方法实现加减乘除运算

五种方法的详细说明&#xff1a; 计算属性&#xff08;Computed Properties&#xff09;&#xff1a; 计算属性是Vue.js提供的一种便捷的属性&#xff0c;它根据依赖的数据动态计算出一个新的值。计算属性的值会被缓存&#xff0c;只有当依赖的数据发生变化时&#xff0c;才会…

前端面试总结心得

1.放在HTML里的哪一部分JavaScripts会在页面加载的时候被执行&#xff1f; A、文件头部位置&#xff1b;B、文件尾&#xff1b;C、<head>标签部分&#xff1b;D、<body>标签部分 &#xff08;正确答案D&#xff09; 2.队列和栈的区别是什么&#xff1f; 答案&am…

python判断ip所属地区 python 判断ip 网段

前言 IP地址是互联网中唯一标识一个设备的地址&#xff0c;有时候需要判断一个IP地址所属的地区&#xff0c;这就需要用到IP地址归属查询。本文将介绍Python如何通过IP地址查询所属地区并展示代码。 一、 IP地址归属查询 IP地址归属查询又称IP地址归属地查询、IP地址归属地定…

框架分析(2)-React

框架分析&#xff08;2&#xff09;-React 专栏介绍React核心思想关键特性和功能组件化开发单向数据流JSX语法强大的生态系统 优缺点分析优点缺点 专栏介绍 link 主要对目前市面上常见的框架进行分析和总结&#xff0c;希望有兴趣的小伙伴们可以看一下&#xff0c;会持续更新的…

火山引擎发布自研视频编解码芯片 压缩效率提升30%

8月22日&#xff0c;火山引擎视频云宣布其自研的视频编解码芯片已成功出片。经验证&#xff0c;该芯片的视频压缩效率相比行业主流硬件编码器可提升30%以上&#xff0c;未来将服务于抖音、西瓜视频等视频业务&#xff0c;并将通过火山引擎视频云开放给企业客户。 火山引擎总裁…