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

Go语言必知必会100问题-05 接口污染

接口污染

在Go语言中,接口是我们设计和编写代码的基石。然而,像很多概念一样,滥用它是不好的。接口污染是指用不必要的抽象来编写代码(刻意使用接口),使得代码更难以理解。这是具有不同习惯,特别是有其它语言开发经验的人会犯的一个常见错误。在深入讨论接口污染之前,让我们重新梳理一下Go语言的接口,然后分析何时使用接口以及在什么时候使用会存在污染问题。

接口

接口约定了对象的行为方法,用于创建多个对象可以实现的通用抽象,也就是说接口规范了对象的通用方法。Go语言中接口有点特别,不像其它语言通过类似于implements关键字显示的标记对象X实现了接口Y, 它是隐式实现的。

接口如此灵活强大的原因是什么呢?为了搞清楚这个问题,我们从标准库中选两个广泛使用的接口: io.Reader 和 io.Writer 进行举例说明。

io包为I/O操作提供了抽象,I/O有读写两类操作。如下图所示,io.Reader是从数据源读取数据接口, io.Writer是将数据写入目标接口。

在这里插入图片描述

io.Reader接口包含一个Read方法。如果一个结构体要实现io.Reader接口,则需要实现下面的Read方法,该方法需要一个字节切片作为入参,会将从数据源读取的数据填充到入参切片中,同时返回读取的字节数和错误信息。

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

io.Writer接口包含一个Write方法。 如果一个结构体要实现io.Writer接口,则需要实现下面的Write方法,该方法也需要一个字节切片作为入参,会将入参切片中的数据写入到目标中,并返回写入的字节数和错误信息。

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

因此,这两个接口都提供了对基本操作的抽象:

  • io.Reader 从源读取数据
  • iO.Writer 将数据写入到目标中

在编程时使用这两个接口合理性在什么地方呢?创建这些抽象意义在哪里呢?下面通过一个例子进行说明。假设我们需要实现将一个文件内容复制到另一个文件中的函数,我们可以创建一个特定的函数,将两个 *os.File作为输入, 或者可以选择使用io.Reader和io.Writer接口创建一个更通用的函数。

func copySourceToDest(source io.Reader, dest io.Writer) error {// ...
}

copySourceToDest函数可以使用*os.File作为入参(因为*os.File实现了io.Reader和io.Writer),也可以使用任何其他实现了这些接口的类型。例如,我们可以创建自己的io.Writer来将数据写入到数据库中,并且可以不用修改copySourceToDest代码。这样增加了函数的通用性,因此,上述函数是可重用的。

使用接口除了使函数更有通用性,还使得为这个函数编写单元测试更容易,因为我们不必写文件,可以使用标准库中strings包和bytes包提供的功能实现测试。下面程序中source变量是*strings.Buffer类型,dest变量是*bytes.Buffer类型,我们可以在不创建任何文件的情况下测试copySourceToDest的行为。

func TestCopySourceToDest(t *testing.T) {const input = "foo"source := strings.NewReader(input)dest := bytes.NewBuffer(make([]byte, 0))err := copySourceToDest(source, dest)if err != nil {t.FailNow()}got := dest.String()if got != input {t.Errorf("expected: %s, got: %s", input, got)}
}

在设计接口时,不要忘了接口的粒度(接口中包含多少方法), Go语言中有一句名言描述了接口粒度问题:

接口越大,抽象越弱

向接口中添加方法会降低它的可重用性。io.Reader和io.Writer具有强大的抽象,因为它们都包含1个方法,不能再变得更抽象了。可以组合细粒度的接口来创建更高级别的抽象。像下面的ReadWriter接口组合了Reader和Writer接口,兼有读取和写入功能。

type ReadWriter interface {ReaderWriter
}

NOTE:正如爱因斯坦所说,“一切事情应该力求简单,不过不能过于简单”。应用到接口上,表示找到接口最佳粒度不一定是一个简单的事情。

什么时候使用接口

编写Go程序的时候,在什么情况下该创建接口呢?本文将深入研究三个具体的场景,在这些场景中,可以看到使用接口可以带给我们更多的收益。注意,对于使用接口的场景,本文没法全部列举完,因为每个案例都依赖于上下文。虽然没法全部列举,但本文列举的三个场景将给我们在什么情况应该使用接口提供一个指引。

  • 共同行为
  • 解耦
  • 限制行为

第一个讨论的场景是在多种类型实现共同行为时使用接口。这种场景下,将共同行为抽取到接口中。如果我们查看标准库,可以找到许多此类场景的示例。例如,可以通过实现排序接口的定义的方法对集合元素进行排序。

  • Len方法,获取集合中元素的数量
  • Less方法,判断一个元素是否在另一个元素之前
  • Swap方法,将两个元素互换位置

因此,在sort包中定义了如下接口:

type Interface interface {Len() intLess(i, j int) boolSwap(i, j int)
}

该接口具有强大的复用性,因为它支持对任何基于索引的集合进行排序。在整个sort包中,可以找到很多种实现。例如,具体到某种类型,在某个时候当我们计算出了集合中元素的个数之后,我们需要对其进行排序,我们是否一定对实现类型感兴趣?采用的是什么排序算法,是归并排序还是快速排序?在很多情况下,作为调用方并不在乎。因此,排序行为可以被抽象化,我们可以依赖于sort.Interface.

找到合适的抽象来分解操作也会带来很多好处,例如,sort包提供了同样依赖于sort.Interface的工具函数,像检查一个集合是否已经是有序的。

func IsSorted(data Interface) bool {n := data.Len()for i := n - 1; i > 0; i-- {if data.Less(i, i-1) {return false}}return true
}

第二讨论的场景是对我们的代码实现进行解耦。如果我们依赖抽象而不是具体的实现,那么实现本身就可以被另一个实现替换,甚至不用更改当前的代码。这就是里氏替换原则(SOLID中的L)。 此外,解耦可以带来单元测试的便利性。假设我们必须实现一个CreateNewCustomer方法来创建一个新客户并保存它的信息,我们可以直接依赖具体的实现(比如mysql.Store结构), 代码如下。

type CustomerService struct {store mysql.Store
}func (cs CustomerService) CreateNewCustomer(id string) error {customer := Customer{id: id}return cs.store.StoreCustomer(customer)
}

现在,如果我们要对这个函数进行单元测试,由于CustomerService依赖于实际实现(MySQL)来存储客户信息。我们需要先启动MySQL数据库,才能对其进行测试(除非使用诸如go-sqlmock之类的替代方法)。尽管集成测试很有帮助,但它并不总是我们想要的。为了使得代码有更大的灵活性,应该将CustomerService与实际实现分离,可以通过如下接口完成:

type customerStorer interface {StoreCustomer(Customer) error
}type CustomerService struct {storer customerStorer
}func (cs CustomerService) CreateNewCustomer(id string) error {customer := Customer{id: id}return cs.storer.StoreCustomer(customer)
}

上述新版本存储客户信息是通过接口完成的,我们现在可以灵活地对其进行单元测试:

  • 采用集成测试对其具体实现进行测试
  • 通过mock(模拟接口的行为)进行测试
  • 联合前面两种进行测试

第三个讨论的场景是通过接口限制特定的行为,看起来有点违反直觉,可以结合下面的例子进行理解。假设我们已经实现了一个自定义配置包来处理动态配置,该包中定义了一个IntConfig结构体,用于存储int配置信息,该结构体对外暴露了Get和Set两个方法.

type IntConfig struct {// ...
}func (c *IntConfig) Get() int {// Retrieve configuration
}func (c *IntConfig) Set(value int) {// Update configuration
}

现在,假设我们获取到一个IntConfig对象,它包含一些特定的配置,例如阈值设定。但是,在我们的代码中,只对读取配置感兴趣,并且希望不要对其进行修改操作。如果不想修改上面的配置包中的代码,怎么限制执行这个配置是只读的呢?可以创建一个将行为限制为仅读取配置值的抽象(即接口)。

type intConfigGetter interface {Get() int
}

然后,在代码中,可以依赖 intConfigGetter 而不是具体的实现编码。配置getter被注入到NewFoo工厂方法中,这样做甚至能够做到不会影响使用这个函数的客户端,仍然可以传递一个IntConfig对象给NewFoo,因为IntConfig实现了接口intConfigGetter,并且能够实现在Bar方法中只能读取不能修改配置信息的目的。

type Foo struct {threshold intConfigGetter
}func NewFoo(threshold intConfigGetter) Foo {return Foo{threshold: threshold}
}func (f Foo) Bar()  {threshold := f.threshold.Get()// ...
}

通过上面的例子可以看到,出于各种原因,我们可以使用接口来限制对象的特定行为,像上面强制设置为只读语义。

接口污染

有其他语言经验的人,像C#或Java背景的人,在具体类型之前创建接口对他们来说是很自然的。然而,在Go项目中这是在过度使用接口,不是推荐做法。

正如我们所讨论的,接口是用来创建抽象的。当在编码中遇到抽象时,记住一句话“应该发现抽象,而不是创建抽象”,这是什么意思呢? 这句话想表达的意思是如果没有直接的原因,我们不应该首先在代码中创建抽象,不应该使用接口进行设计,而是等待具体的需求。也就是说,我们应该在需要时创建接口,而不是在我们预见到可能需要它时就创建。

过度使用接口,会产生什么问题呢?答案是它使代码流更加复杂。添加无用的间接层不会带来任何价值:创建了一个没有用的抽象,使代码更难阅读和理解。如果没有充分的理由添加接口并且不清楚接口如何使代码变得更好,我们应该主动对使用接口产生质疑,为什么不直接调用具体实现(非接口)呢?

NOTE:注意通过接口调用方法时的性能开销,需要在哈希表数据结构中查找到实际指向的具体类型,然而,这在很多情况下不是什么问题,因为这种开销很小。

总结,在编码的过程中使用接口应该谨慎,应该带着发现抽象,而不是创建抽象的目的。对于软件开发人员来说,根据当前情况猜测以后可能有什么需求,来构建完美的抽象,过度设计代码是很常见的,应该避免这样做,因为在大多数情况下,会用不必要的抽象污染当前的代码。使其阅读起来更加复杂。我们不要试图通过抽象解决所有问题,而是解决现在必须解决的问题。最后但同样重要的是,如果不清楚接口如何使代码变得更好,我们可能应该考虑删除它以使我们的代码更简单。

相关文章:

Go语言必知必会100问题-05 接口污染

接口污染 在Go语言中,接口是我们设计和编写代码的基石。然而,像很多概念一样,滥用它是不好的。接口污染是指用不必要的抽象来编写代码(刻意使用接口),使得代码更难以理解。这是具有不同习惯,特…...

FastBee商业版本源码获取下载

一、系统功能 系统功能功能说明开源版本商业版本产品管理产品详情、产品物模型、产品分类、设备授权、产品固件支持支持设备管理设备详情、设备分组、设备日志、设备分享、设备实时控制、实时状态、数据监测支持支持物模型管理属性(设备状态和监测数据)…...

Java实战:Spring Boot集成Elasticsearch全文搜索引擎

本文将详细介绍如何在Spring Boot应用程序中集成Elasticsearch全文搜索引擎。我们将探讨Elasticsearch的基本概念,以及如何使用Spring Boot和Spring Data Elasticsearch模块来实现全文搜索功能。此外,我们将通过具体的示例来展示如何在Spring Boot应用程…...

python 进程笔记二(通讯) (概念+示例代码)

1、为什么要掌握进程间通信 Python代码效率由于受制于GIL全局锁限制,多线程不能利用多核CPU来加速,而多进程方式却可以绕过GIL限制, 发挥多CPU加速的优势,达到提高程序的性能的目的。 然而进程间通信却是不得不考虑的问题。 进程不同于线程&a…...

电机控制-----电机极对数,相电感,相电阻以及磁链常数的测量

电机控制-----电机极对数,相电感,相电阻以及磁链常数的测量 我们在做电机控制的时候,拿到一个电机首先要知道它的参数,然后才能进行相应的开发,我这里介绍的是通过平常常用的手段去获得电机的参数:极对数&…...

SQL注入之oracle注入+SQLbypass+sqlmap实战

学习路还很长,切莫轻言放弃! 目录 Oracle数据库介绍 Oracle数据库和MySQL数据库的差别 Oracle数据库注入 SQLbypass姿势 sqlmap工具实战(kali自带) Oracle数据库介绍 Oracle数据库是全球最知名的关系型数据库管理系统(RDBMS&#xff09…...

【GPTs分享】GPTs分享之Write For Me

Write For Me 是一个专门定制的GPT版本,旨在为用户提供高质量的文本内容创作服务。它适用于各种写作需求,从商业计划、学术文章到创意故事等。下面是从简介、主要功能、使用案例、优点和局限性几个方面对Write For Me 的详细介绍。 简介 Write For Me …...

css4浮动+清除浮动

浮动 一.常见网页布局1.三种布局方式2.布局准则 二.浮动(float)1.好处2.概念3.三大特性4.使用5.常见网页布局模板6.注意点 三.清除浮动1.why2.本质3.语法4.四种way(后三个都是给父级添加)清除浮动总结 一.常见网页布局 1.三种布局…...

外包干了3个月,技术倒退明显...

先说情况,大专毕业,18年通过校招进入湖南某软件公司,干了接近6年的功能测试,今年年初,感觉自己不能够在这样下去了,长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试&#xf…...

STM32控制数码管从0显示到99

首先 先画电路图吧!打开proteus,导入相关器件,绘制电路图。如下:(记得要保存啊!发现模拟一遍程序就自动退出了,有bug,我是解决不了,所以就是要及时保存,自己重…...

【机器学习算法】KNN鸢尾花种类预测案例和特征预处理。全md文档笔记(已分享,附代码)

本系列文章md笔记(已分享)主要讨论机器学习算法相关知识。机器学习算法文章笔记以算法、案例为驱动的学习,伴随浅显易懂的数学知识,让大家掌握机器学习常见算法原理,应用Scikit-learn实现机器学习算法的应用&#xff0…...

Windows 自带的 Linux 子系统(WSL)安装与使用

WSL官网安装教程: https://learn.microsoft.com/zh-cn/windows/wsl/install Windows 自带的Linux子系统,比用VM什么的香太多了。可以自己看官方教程,也可以以下步骤完成。 如果中间遇到我没遇到的问题百度,可以在评论区评论&#…...

C语言--贪吃蛇

目录 1. 实现目标2. 需掌握的技术3. Win32 API介绍控制台程序控制台屏幕上的坐标COORDGetStdHandleGetConsoleCursorinfoCONSOLE_CURSOR_INFOSetConsoleCursorInfoSetConsoleCursorPositionGetAsyncKeyState 4. 贪吃蛇游戏设计与分析地图<locale.h>本地化类项setlocale函…...

原型设计工具Axure RP

Axure RP是一款专业的快速原型设计工具。Axure&#xff08;发音&#xff1a;Ack-sure&#xff09;&#xff0c;代表美国Axure公司&#xff1b;RP则是Rapid Prototyping&#xff08;快速原型&#xff09;的缩写。 下载链接&#xff1a;https://www.axure.com/ 下载 可以免费试用…...

HeadFirst读书笔记

一、设计模式入门 1、使用模式最好的方式“把模式装进脑子里&#xff0c;然后在你的设计和已有的应用中&#xff0c;寻找何处可以使用它们”。以往是代码复用&#xff0c;现在是经验复用。 2、软件开发的一个不变的真理就是变化。 二、设计原则 1、找出应用中可能需要变化之…...

【C++】---内存管理new和delete详解

一、C/C内存分布 C/C内存被分为6个区域&#xff1a; &#xff08;1&#xff09; 内核空间&#xff1a;存放内核代码和环境变量。 &#xff08;2&#xff09;栈区&#xff1a;向下增长&#xff08;存放非静态局部变量&#xff0c;函数参数&#xff0c;返回值等等&#xff09; …...

go-zero微服务入门教程

go-zero微服务入门教程 本教程主要模拟实现用户注册和用户信息查询两个接口。 准备工作 安装基础环境 安装etcd&#xff0c; mysql&#xff0c;redis&#xff0c;建议采用docker安装。 MySQL安装好之后&#xff0c;新建数据库dsms_admin&#xff0c;并新建表sys_user&#…...

蓝桥杯刷题--python-12

3768. 字符串删减 - AcWing题库 nint(input()) sinput() res0 i0 while(i<n): if s[i]x: ji1 while(j<n and s[j]x): j1 resmax(j-i-2,0) ij else: i1 print(res) 3777. 砖块 - AcWing题库 # https://www.a…...

LeetCode LCR 085.括号生成

正整数 n 代表生成括号的对数&#xff0c;请设计一个函数&#xff0c;用于能够生成所有可能的并且 有效的 括号组合。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”] 示例 2&#xff1a; 输入&#x…...

抖音视频评论数据提取软件|抖音数据抓取工具

一、开发背景&#xff1a; 在业务需求中&#xff0c;我们经常需要下载抖音视频。然而&#xff0c;在网上找到的视频通常只能通过逐个复制链接的方式进行抓取和下载&#xff0c;这种操作非常耗时。我们希望能够通过关键词自动批量抓取并选择性地下载抖音视频。因此&#xff0c;为…...

【web】云导航项目部署及环境搭建(复杂)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、项目介绍1.1项目环境架构LNMP1.2项目代码说明 二、项目环境搭建2.1 Nginx安装2.2 php安装2.3 nginx配置和php配置2.3.1 修改nginx文件2.3.2 修改vim /etc/p…...

软件测试人员必会的linux命令

文件和目录操作: ● ls:列出目录中的文件和子目录。 ● cd:改变当前工作目录。 ● mkdir:创建新目录。 ● rm:删除文件或目录。 ● cp:复制文件或目录。 ● mv:移动或重命名文件或目录。 文本查看和编辑: ● cat:查看文件内容。 ● more或less:分页查看文件内…...

Mac使用K6工具压测WebSocket

commend空格 打开终端&#xff0c;安装k6 brew install k6验证是否安装成功 k6 version设置日志级别为debug export K6_LOG_LEVELdebug执行脚本&#xff08;进入脚本所在文件夹下&#xff09; k6 run --vus 100 --duration 10m --out csvresult.csv script.js 脚本解释&…...

小程序--vscode配置

要在vscode里开发微信小程序&#xff0c;需要安装以下两个插件&#xff1a; 安装后&#xff0c;即可使用vscode开发微信小程序。 注&#xff1a;若要实现鼠标悬浮提示&#xff0c;则需新建jsconfig.json文件&#xff0c;并进行配置&#xff0c;即可实现。 jsconfig.json内容如…...

linux僵尸进程

僵尸进程&#xff08;Zombie Process&#xff09;是指在一个进程终止时&#xff0c;其父进程尚未调用wait()或waitpid()函数来获取该进程的终止状态信息&#xff0c;导致进程的资源&#xff08;如进程表中的记录&#xff09;仍然保留在系统中的一种状态。 当一个进程结束时&am…...

【web | CTF】攻防世界 Web_php_unserialize

天命&#xff1a;这条反序列化题目也是比较特别&#xff0c;里面的漏洞知识点&#xff0c;在现在的php都被修复了 天命&#xff1a;而且这次反序列化的字符串数量跟其他题目不一样 <?php class Demo { // 初始化给变量内容&#xff0c;也就是当前文件&#xff0c;高亮显示…...

Vue3中的select 的option是多余的?

背景&#xff1a; 通过Vue3中填充一个下拉框&#xff0c;在打开页面时要指定默认选中&#xff0c;并在选项改变时把下拉框的选中值显示出来 问题&#xff1a; 填充通常的作法是设置 <option v-for"option in cities" :value"option.value" >&a…...

考研408深度分析+全年规划

408确实很难&#xff0c;他的难分两方面 一方面是408本身的复习难度&#xff0c;我们都知道&#xff0c;408的考察科目有四科&#xff0c;分别是数据结构&#xff0c;计算机组成原理&#xff0c;操作系统和计算机网络。大家回想一下自己在大学本科时候学习这些专业课的难度&am…...

【算法笔记】ch01_01_0771 宝石与石头

笔记介绍&#xff1a; 本项目是datawhale发布的LeetCode 算法笔记&#xff08;Leetcode-Notes&#xff09;课程完成笔记&#xff0c;根据推荐题目循序渐进练习算法题目。主要用python进行书写相关代码&#xff0c;会介绍解题思路及跑通解法。 0771. 宝石与石头 题目大意 描…...

jQuery瀑布流画廊,瀑布流动态加载

jQuery瀑布流画廊&#xff0c;瀑布流动态加载 效果展示 手机布局 jQuery瀑布流动态加载 HTML代码片段 <!-- mediabanner --><div class"mediabanner"><img src"img/mediabanner.jpg" class"bg"/><div class"text&qu…...

wordpress精简化教程/seo网站关键词优化方式

问题 1&#xff1a;C 中的类可以定义多个对象&#xff0c;那么对象构造的顺序是怎样的&#xff1f; 问题 2&#xff1a;对象构造顺序会带来什么影响呢&#xff1f; 对象构造往往与构造函数相关联&#xff0c;构造函数体有可能是非常复杂的程序逻辑组成&#xff0c;不同类的构造…...

名片式网站模板/制作网页的流程

VRRP工作原理 简介&#xff1a;VRRP&#xff1a;&#xff08;Virtual Router Redundancy Protocol&#xff09;虚拟路由器冗余协议主用路由器master&#xff1a;负责转发发给虚拟路由器的数据包&#xff0c;并响应ARP请求的路由器。若一台拥有与虚拟路由器有相同IP地址的VRRP路…...

犀牛做网站的公司/西青seo

括号检查 题目 现有一字符串 仅由 (,),{,},[,] 六种括号组成,若字符串满足以下条件之一,则为无效字符串 任意类型的左右括号数量不相等 存在未按正确顺序(先左后右)闭合的括号, 输出括号的最大嵌套深度 若字符串无效则输出 0 0 <= 字符串长度 <= 100000 输入 一个只…...

公司注册后怎么做网站/自己怎么搭建网站

说明&#xff1a; EXP-00091: 出口可疑的统计。 EXP-00091: Exporting questionable statistics. 产生&#xff1a; 在数据库的服务器端和客户端字符集不同的情况下&#xff0c;导出 (dump) 数据库表时&#xff0c;会产生这个错误。虽然产生这个错误&#xff0c;但好像对导…...

服装高端网站建设/内部优化

流媒体解决方案 Live555 流媒体平台框架 EasyDarwin 实时流媒体播放服务器程序DarwinStreamingSrvr 流媒体实时传输开发包 jrtplib 多媒体处理工具 ffmpeg 多媒体编码工具包Libav Flash流媒体服务器 Red5 流媒体服务器 Open Streaming Server FMS流媒体服务器 Wowza流媒体服务器…...

外贸做网站推广/沈阳seo排名外包

Java的重要性Java语言的三大特点&#xff0c;面向对象、良好的跨平台性和健壮性&#xff0c;这三大特点使Java被广大编程人员接收并且使用。Java的核心机制有Java虚拟机和垃圾回收机制这两种&#xff0c;Java虚拟机通过解析器&#xff0c;使Java编写的程序能在任何操作系统下运…...