Stas Afanasyev。朱诺 基于io.Reader / io.Writer的管道。第2部分

在报告中,我们将讨论io.Reader / io.Writer的概念,为什么需要它们,如何正确实现它们以及在这方面存在哪些陷阱,以及基于标准和自定义io.Reader / io.Writer实现的管道。 。



Stas Afanasyev。朱诺 基于io.Reader / io.Writer的管道。第1部分

错误“信任”


另一个细微差别:在此实现中有一个“包bag”。此错误已由开发人员确认(我已写信给他们)。也许有人知道这个“包ul”是什么?幻灯片上的倒数第二行:



与包装的Reader的信任度过高相关:如果Reader返回的字节数为负,则我们希望通过减去的字节数获得的限制增加了。在某些情况下,这是一个非常严重的错误,您无法立即理解。

我在问题中写道:让我们做点什么,让我们解决它!然后出现了一系列问题……首先,他们告诉我,如果现在在此处添加此检查,则必须在所有位置添加此检查,并且这些地方很多。如果我们想将其转移到客户端,那么我们需要确定一些规则,客户端将通过这些规则来验证数据(也可能有五个或两个)。事实证明,所有这些都需要复制。

我同意这不是最佳选择。然后,让我们来看看一些一致的版本!为什么我们有一个标准库的实现不信任任何事物,而其他人却绝对信任所有事物?

总的来说,当我在撰写我的公民意见并进行深思熟虑时,我们以评论结束了这个问题:“我们不会做任何事情。再见!他们让我看起来像个傻瓜……礼貌地,当然,你找不到错。

总的来说,我们现在有一个问题。其原因在于不清楚谁应该验证包装的Reader的数据。无论是客户,还是我们完全信任合同……我们有一个解决方案!如果还有时间,我会讲的。

让我们继续下一个案例。

发球机


我们看了一个如何包装Reader数据的示例。下一个管道示例是替代Writer中的Reader数据。有两种情况。

第一种情况。我们需要从Reader读取数据,以某种方式(透明地)将其复制到Writer并与Reader一起使用。有一个TeeReader的实现。它在上层实现片段中提供:



类似于Unix上的Tee团队。我想你们当中很多人都听说过。
请注意,此实现检查从包装的Reader读取的字节数。看到第二行的条件了吗?因为当您编写这样的实现时,从直觉上很清楚:如果数字为负,您会感到恐慌。这是我们信任包装好的读者的另一个地方!我提醒您,这些都是标准库。

让我们继续讲一个案例,例如,如何使用它。我们将在下面的代码段中做什么?我们将使用标准的HTTP客户端从golang.org下载robot.txt文件。

如您所知,http客户端向我们返回了一个响应结构,其中“正文”字段是Reader接口的实现。应该通过说这是ReadCloser接口的实现来阐明。但是ReadCloser只是从Reader和Closer构建的接口。也就是说,这是一个Reader,通常可以关闭。

在此示例中(在下面的代码段中),我们收集TeeReader,它将从此Body中读取数据并将其写入文件。不幸的是,今天的文件创建仍在幕后,因为一切都不合适。但是,如果再次查看树状图,文件类型将实现Writer接口,也就是说,我们可以对其进行写入。这很明显。

我们组装了TeeReader并使用ReadAll进行阅读。一切都按预期工作:我们减去生成的Body,将其写入文件,然后在Assad中查看。

初学者方式


第二种情况。我们只需要从Reader中读取数据并将其写入Writer。解决方案显而易见……

当我刚开始使用Go时,我在幻灯片上解决了诸如此类的问题:



我找到了缓冲区,用Reader的数据填充了缓冲区,然后将填充的切片传输到Writer。一切都很简单。

两点。首先,不能保证一次调用Read方法会减去整个Reader,因为可能会剩下数据(以一种好的方式,应该循环执行)。

第二点是该路径不是最佳路径。这是写在我们面前的漂亮样板代码。

为此,标准库中有一个特殊的帮助器家族-这些是Copy,CopyN和CopyBuffer。

io。复制。WriterTo和ReaderFrom


io.Copy基本上执行上一张幻灯片中的操作:它分配了32 KB的默认缓冲区,并将数据从Reader写入Writer(此Copy的签名显示在上面的代码片段中):



除了此模板例程之外,它还包含一系列棘手的优化。在讨论这些优化之前,我们需要熟悉另外两个接口:

  • WriterTo;
  • ReadFrom。

假设情况。您的阅读器使用内存缓冲区。他已经将其重新放置,写入,从那里读取内容,也就是说,它下面的一个位置已经被重新放置。您想从外部阅读该阅读器。

我们已经看到了这是如何发生的:创建一个缓冲区,传递该缓冲区,该缓冲区被传递给Read方法;与内存配合使用的Reader将其从复制的片段中剔除出来...但这不再是最佳选择-该位置已重新放置。为什么要再做一次?



5-6年前的某个地方(有一个指向更改列表的链接)创建了两个接口:WriteTo和ReadFrom,它们在本地实现。 Reader实现WriteTo,Writer实现ReadFrom。事实证明,具有已复制数据的切片的Reader可以避免其他位置,可以接受Write to Writer方法并在内部传递可用的缓冲区。

这就是bytes.Buffer和bufio的实现方式。如果再次查看树状图,您会发现这两个接口不是很流行。它们只是为那些与内部缓冲区一起使用的类型实现的-内存已在其中重定位。这不会帮助您每次都避免口才,仅当您已经在处理重定位的作品时才如此。

ReaderFrom以类似的方式工作(仅由Writer实现)。ReaderFrom读取整个Reader,并将其作为一个参数(在EOF之前),并写入Writer的内部实现中的某个位置。

CopyBuffer实现


此代码片段显示了copyBuffer帮助器的实现。此不可导出的copyBuffer在io.Copy,CopyN和CopyBuffer的支持下使用。

这里有一个细微的差别值得一提。 CopyN最近进行了优化-与该逻辑无关。这正是我之前提到的优化:在创建32 KB的额外缓冲区之前,先进行检查-也许数据源实现了WriterTo接口,并且不需要此额外缓冲区吗?

如果没有发生,我们检查:也许Writer实现了ReaderFrom来在没有这种中介的情况下将它们连接起来?如果这没有发生,那么最后的希望仍然存在:也许我们得到了某种可以使用的重定位缓冲区?



io.Copy就是这样工作的。

有一个问题,这是一个半建议,一个半错误-目前尚不清楚。它已经挂了一年半了。听起来像这样:CopyBuffer在语义上是不正确的。

不幸的是,这个copyBuffer没有签名,但是看起来就像这个不可导出的方法一样。

当您希望避免出现其他位置而调用copyBuffer时,将一些重定位的切片字节传递到该位置,以下逻辑起作用:如果Reader或Writer具有WriterTo和ReaderFrom接口,则无法保证可以避免该位置。这被接受为提案,并承诺在Go 2.0中考虑它。现在,您只需要知道。

使用io.Pipe。PipeReader和pipeWriter


另一种情况:您需要以某种方式在Reader中从Writer获取数据。漂亮的生活案例。

想象一下,您已经有一些数据,它们实现了Reader接口-一切由此可见。您需要压缩此数据,“对其进行调整”,然后将其发送到S3。细微差别是什么?..
在compess包中使用gzip类型的人都知道gzip'er本身只是一个代理:它将数据放入自身,实现Writer接口,将数据写入,将对它们进行处理,然后然后我必须将它们放到某个地方。在构造函数上,它需要Writer接口的实现。

因此,这里我们需要某种中间Writer,在其中我们将丢弃在第一阶段中已存档的已压缩数据。我们的下一步是将这些数据上传到S3。并且标准的AWS客户端接受io.Reader接口作为数据源。



幻灯片显示了管道-它显示了外观:我们需要覆盖数据以从Reader到Writer,从Writer到Reader覆盖。怎么做?

标准库具有很酷的功能io.Pipe。它返回两个值:pipeReader和pipeWriter。这对有着千丝万缕的联系。想象一下用绳子放在杯子里的“婴儿电话”:在一个杯子里说话而没有人在另一端听话是没有意义的...



这个io.Pipe有什么作用?直到没有人写入数据时,它才会读取。反之亦然,除非没有人在另一端读取此数据,否则他不会写任何东西。这是一个示例实现:



我们将在此处执行相同的操作。我们将读取之前读取的robot.txt文件,我们将使用gzip对其进行压缩并将其发送到S3。

  • 在第一行,将创建一个对-pipeReader,pipeWriter。接下来,我们必须至少运行一个goroutine,该例程将从一端(一种管道)读取数据。在此gorutin中,使用数据源(源-pipeReader)运行上载器。
  • 在下一步中,我们需要压缩数据。我们压缩数据并将其写入pipeWriter(它将是管道的另一端),并且已经运行的goroutine在管道的另一端接收数据并读取它。准备好整个三明治后,剩下的就是点燃灯芯...
  • 请参阅:io.Copy的最后一行将数据从主体写入我们创建的gzip(即从Reader到Writer)。所有这一切都按预期进行。

这个例子可以用另一种方式解决。如果您使用同时实现Reader和Writer的任何实现。您将首先向其中写入数据,然后再读取它们。
这清楚地演示了如何使用io.Pipe。

其他实施


这基本上就是我的全部。我们来谈谈我想谈论的有趣的实现。



我没有说任何有关MultiReader的信息,也没说有关MultiWriter的信息。这是标准库的另一个不错的实现,它使您可以连接不同的实现。例如,MultiWriter同时写入所有Writer,而MultiReader依次读取Reader。

另一个实现称为limio。它允许您设置减法极限。您可以设置每秒需要读取阅读器的速度(以字节为单位)。

另一个有趣的实现只是可视化的阅读进度-进度栏(来自某人)。这称为ioprogress。

我怎么都这么说那是什么意思



  • 如果您突然需要实现Reader和Writer接口,请正确执行。尚无任何决定负责执行的人-我们将假定每个人都信任合同。因此,您需要无可挑剔地遵守它。
  • 如果您的案例使用的是重新定位的缓冲区,请不要忘记ReaderFrom和WriterTo接口。
  • 如果您无所适从并且需要示例,请参阅标准库,您可以依靠许多不错的实现。那里有文档。
  • 如果您完全无法理解某些内容,请随时写问题。那里的人足够,可以迅速做出回应,非常有礼貌并且能胜任帮助您。



这就是我的全部。谢谢你的到来!

问题


观众提问(B):-我想一个简单的问题。请告诉我们一些生活中的用例:使用了哪些用例,为什么?您说Reader / Writer返回读取的长度。您是否曾经遇到过任何问题?您什么时候要求阅读(不仅存在ReadAll),但没有任何作用?

SA: -我必须坦白地说,我从来没有遇到过这种情况,因为我一直在使用标准库的实现。但是,假设这种情况当然是可能的。对于特定情况,我们通常会收集多层管道,如果假设允许此类错误,则整个管道将散开……

问:-这不是一个错误。接下来,让我们介绍一下我的小经验。我在Booking.com上遇到了问题:他们使用了我编写的驱动程序,但遇到了问题-某些东西无法正常工作。我们做了一个标准的二进制协议。在本地,一切正常,每个人都很好,但是事实证明,他们的数据中心网络非常糟糕。然后,Reader并没有真正返回所有内容(坏的网卡,还有其他东西)。

CA: -但是,如果他没有退还所有东西,那么他就不应该退还结局(end)的标志,客户应该再次来。根据所描述的合同,Reader不应……让我们说,当然,Reader决定何时要来,何时不想要,但是,如果他想阅读所有内容,则必须等待EOF。

在:“但这恰恰是由于联系。”这正是标准net程序包中出现的问题。

CA: -他退还了EOF?

问: -他没有归还所有东西-他只是不阅读所有东西。我告诉他:“读取接下来的20个字节。”他读。而且我不会阅读所有内容。

SA: -从理论上讲,这是可能的,因为它只是描述通信协议的接口。有必要观察并特别拆卸外壳。在这里,我只能回答您,从理论上讲,如果客户没有收到他想要的一切,他应该又来了。您要求他提供20字节的片,他为您减去了15个字节,但EOF没来-您应该再去一次...

问: -这种情况下有io.ReadFull。它是专门设计用来读取切片到底的。

CA:-是的关于ReadFull,我什么也没说。

问: -当“读取”未填满整个片时,这是完全正常的情况。您需要为此做好准备。

SA: -这是非常值得期待的情况!

问: -感谢您的报告-这很有趣。我在一个小的,简单的代理中使用Readers,该代理可以读取http并以其他方式编写。我使用Close Reader解决一个问题-关闭我一直都在阅读的内容。我是否需要盲目地相信合同?您说可能有问题。还是添加其他检查?从理论上讲,这个站点上可能不会完全包含某些内容。我需要做这些额外的检查,而不信任合同吗?

CA:-我会这样说:如果您的应用程序可以容忍这些错误(例如,如果您完全信任合同),那么可能就不会。但是,如果您不希望自己陷入“恐慌”(如我在byte.Buffer中的负读数所示),那么我仍然会进行检查。
但这取决于您。我能向您推荐什么?我认为只是权衡利弊。如果突然得到负数字节会发生什么?

问: -感谢您的报告。不幸的是,我对Go一无所知。如果发生“恐慌”,有什么办法可以截获此信息并获取有关在什么地方,如何偏见的信息,以避免在周五晚上出现问题?

CA: -是的。相对而言,“恢复”机制使您可以“捕捉”恐慌并将其带出而不会掉落。



在:-有关使用Writer和Reader的实现的建议与实现Web套接字时返回的错误如何一致。我不会举一个具体的例子,但是文件尾总是在那里使用吗?据我所记得,该消息以其他含义结尾...

SA: -这是一个好问题,因为我无话可说。必须注意!如果EOF没有到来,那么客户如果想得到一切,就必须再次参加。

问: -管道能够装配多长时间?是否有内部想法认为管道不值得收集超过五个参与者或分支机构?您用这些管道(读,写)成功构建了多棵树?

CA:-在我的实践中,大约连续进行五个通话是最佳选择,因为调试起来比较困难,请牢记流向何处以及流向何方。获得了漂亮的分支结构。但是我会说最大为5-7。

问: -5-7-在这种情况下?

SA: -例如,正在读取某些数据。您需要保证,并且您登录的内容也需要进行调整。已抵押-然后您读取此数据-您需要将其发送回某个存储(假设是)。在Writer接口实现的任何存储中。使用此管道,会发生5到6个步骤,尽管在其中一个步骤中,它仍然分支到一边,您将继续使用Reader。

在:-根据初学者的方法,您有一张有趣的幻灯片。您是否可以指出另外2-3个有趣的观点,但现在最好不要这样做,而现在要另辟?径?

SA: -有了那张幻灯片,我想确切地显示如何做,而无需阅读Reader。从来没有想到过像初学者这样的事情……这可能是主要错误,与读者一起工作时应避免的主要模式。
演示者:-我自己补充说,对于初学者来说,阅读io包的所有文档,在其中的所有接口上并理解它们都是非常重要的。因为实际上有很多,并且您经常开始做自己的事,尽管它已经存在并且已经正确实现(“正确”-考虑到所有功能)。
领导者的问题:-如何生活得更远?

CA: -好问题!我答应告诉我们是否有时间。作为对该错误的讨论的结果,LimitedReader做出了以下决定:在某种意义上制作一个Reader-避孕套,以防止外部威胁,包装一些您不信任的Reader,以防止任何感染进入您的系统。

在此Reader中,您将执行所有您无法执行的检查:例如,否定读取,对字节数进行实验(假设您发送了一个10字节的切片,而您又得到了15个字节-如何对此进行反应?)...阅读器,您可以实施一组此类检查。我说:“也许让我们添加到标准库中,因为它对每个人都有用”?

我得到的答案是,似乎没有任何意义-这是您可以实现的简单操作。所有。我们生活。我们信任合同工。但是我不相信。



问: -当我们与读者,作家一起工作时,就有机会碰上gzip炸弹……我们对ReadAll和WriteAll有多大的信任?或者,但是,实现缓冲区读取并仅与缓冲区一起使用?

CA:-ReadAll本身仅在内部使用bytes.Buffer。当您想使用此东西时,建议您进入并查看如何实现这些“胆量”。再次,这取决于您的要求:如果您对我所显示的此类错误无法忍受,则需要查看是否检查了包装的Reader中的内容。如果未选中,则使用例如bufio(全部选中)。或执行我刚才所说的:某个代理阅读器,根据您的要求列表,该阅读器将检查此数据,然后将其返回给客户端或将其返回给客户端。




一点广告:)


感谢您与我们在一起。你喜欢我们的文章吗?想看更多有趣的资料吗?通过下订单或向您的朋友推荐给开发人员的基于云的VPS,最低价格为4.99美元这是我们为您发明的入门级服务器 独特类似物:关于VPS(KVM)E5-2697 v3(6核)的全部真相10GB DDR4 480GB SSD 1Gbps从$ 19还是如何划分服务器?(RAID1和RAID10提供选件,最多24个内核和最大40GB DDR4)。

阿姆斯特丹的Equinix Tier IV数据中心的戴尔R730xd便宜2倍吗?在荷兰,我们有2台Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100电视戴尔R420-2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB-$ 99起!阅读有关如何构建基础设施大厦的信息。使用Dell R730xd E5-2650 v4服务器花费一欧元9000欧元的c类?

All Articles