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

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



斯坦尼斯拉夫·阿凡纳谢耶夫(以下简称“ SA”): -下午好!我叫Stas。我来自Minsk,来自Juno公司。感谢您在这个雨天来到这里,找到了离开家的力量。

今天,我想与您谈谈基于io.Reader / io.Writer接口构建管道Go的主题。我今天要谈论的通常是io.Reader / io.Writer接口的概念,为什么需要它们,如何正确使用它们以及最重要的是如何正确实现它们。

我们还将讨论基于这些接口的各种实现来构建管道。我们将讨论现有方法,讨论它们的优缺点。我会提到各种陷阱(数量众多)。

在开始之前,我们必须回答这个问题:为什么根本需要这些接口?举起双手,与Go紧密合作(每天,隔天)...



太好了!我们仍然有一个Go社区。我认为你们中的许多人已经使用过这些接口,至少听说过它们。您甚至可能不了解它们,但您当然应该已经听说了一些有关它们的信息。

首先,这些接口是所有形式的输入输出操作的抽象。其次,这是一个非常方便的API,它使您可以从多维数据集构建管道作为构造函数,而无需真正考虑实现的内部细节。至少那是原本打算的。

io.Reader


这是一个非常简单的界面。它仅包含一种方法-Read方法。从概念上讲,io.Reader接口的实现可以是网络连接-例如,尚无数据,但可以



在其中出现:它可以是内存中的缓冲区,其中数据已经存在并且可以完全读出。它也可以是文件描述符-如果文件很大,我们可以分段读取。

io.Reader接口的概念性实现是对某些数据的访问。 Read方法支持我编写的所有案例。它只有一个参数-这是切片字节。
这里要讲一点。那些最近去过Go或来自其他某种技术的人,这些技术没有类似的API(我是其中之一),这种签名有点令人困惑。 Read方法似乎以某种方式读取了此切片。实际上,事实恰恰相反:Reader接口实现读取内部数据,并用该实现所拥有的数据填充此切片。

Read方法可以根据请求读取的最大数据量等于此切片的长度。常规实现会返回请求时可以返回的尽可能多的数据,或者返回适合此片的最大数据量。这表明读者可以分块阅读:至少按字节阅读,至少十个-可以随意阅读。然后调用Reader的客户端根据Read方法的返回值思考如何生存。

Read方法返回两个值:

  • 减去的字节数;
  • 如果发生错误。

这些价值影响客户的进一步行为。幻灯片上有一个gif,它显示,显示了此过程,我刚刚描述了它:





Io.Reader-如何?


数据满足Reader接口的方式有两种。



首先是最简单的。如果您有某种切片字节,并且想要使其满足Reader接口,则可以采用一些已经满足该接口的标准库的实现。例如,从字节包读取器。在上面的幻灯片上,您可以看到此阅读器的创建方式的签名。

还有一种更复杂的方法-自己实现Reader接口。文档中大约有30行带有棘手的规则,必须遵守限制。在我们讨论所有这些之前,对我来说变得很有趣:“在什么情况下没有足够的标准实现(标准库)?什么时候是我们需要自己实现Reader接口的时刻?”

为了回答这个问题,我在Github上使用了上千个最受欢迎的存储库(按星数),将它们添加并找到了Reader接口的所有实现。在幻灯片上,我有一些统计数据(分类),说明人们何时实现此接口。

  • 最受欢迎的类别是连接。这是现有类型的专有协议和包装器的实现。因此,布拉德·菲茨帕特里克(Brad Fitzpatrick)有一个Camlistore项目-有一个以StatTrackingConn形式出现的示例,通常是一个普通的包装程序,它包装了net包中的con类型(向该类型添加度量)。
  • 第二受欢迎的类别是自定义缓冲区。在这里,我喜欢一个唯一的示例:x / net程序包中的dataBuffer。它的独特之处在于它存储切成块的数据,并且在减去时将它们穿过这些块。如果块中的数据结束了,它将继续到下一个块。同时,他考虑了长度,他可以填写传输的切片的位置。
  • 另一类是各种进度条,计算在发送度量时减去的字节数...

根据这些数据,我们可以说经常需要实现io.Reader接口。然后,让我们开始讨论文档中的规则。

文件规则


正如我所说,规则列表和一般文档非常庞大。对于仅包含三行的接口,30行就足够了。

第一个最重要的规则涉及返回的字节数。它必须严格大于或等于零且小于或等于发送的切片的长度。它为什么如此重要?



由于这是一个相当严格的合同,因此客户可以信任来自实施的金额。标准库中有包装程序(例如,bytes.Buffer和bufio)。标准库中有这么一刻:有些实现信任包装的Reader,有些不信任(我们将在后面讨论)。

Bufio一点都不信任任何东西-它检查所有内容。Bytes.Buffer绝对信任到达他的所有内容。现在,我将演示与此相关的事件。...

现在,我们将考虑三种可能的情况-这是三个已实现的读者。它们是非常综合的,有助于理解。我们将使用ReadAll帮助程序阅读所有这些Reader。他的签名显示在幻灯片的顶部:



io.Reader#1。例子1


ReadAll是使用Reader接口的某种实现的助手,读取所有接口并返回读取的数据以及错误。

我们的第一个示例是Reader,它将始终返回-1和nil作为错误,例如NegativeReader。让我们运行它,看看会发生什么:



您知道,无缘无故的恐慌是愚蠢的迹象。但是在这种情况下谁是傻瓜-我还是byte.Buffer-取决于观点。编写此程序包并遵循该程序包的人有不同的观点。

这里发生了什么? Bytes.Buffer接受了一个负数的字节,没有检查它是否为负数,并尝试沿他接收的上限切断内部缓冲区-我们走出了切片边界。

在此示例中有两个问题。首先是不禁止签名返回负数,并且禁止使用文档。如果签名具有Uint,则将出现经典溢出(当签名数字被解释为未签名时)。这是一个非常棘手的错误,肯定会在星期五晚上完成,当时您已经在家里组装了。因此,在这种情况下,恐慌是首选。

第二个“要点”是堆栈跟踪根本不了解发生了什么。显然,我们已经超出了范围的界限-那又如何呢?当您拥有这样的多层管道并且发生了这样的错误时,尚无法立即弄清发生了什么。因此,在这种情况下,标准库的bufio也会“恐慌”,但它的作用更为优美。他立即写道:“我减去了一个负数的字节。我什么也不会做-我不知道该怎么办。”

还有bytes.Buffer尽其所能地恐慌。我向Golang发布了一个问题,要求我添加人为错误。第三天,我们讨论了这一决定的前景。原因是这样的:从历史上看,不同的人在不同的时间做出了不协调的决定。现在,我们有了以下内容:在一种情况下,我们根本不信任该实现(我们检查了所有内容),而在另一种情况下,我们完全信任,我们无法从那里得到实现。这是一个未解决的问题,我们将进一步讨论。

io.Reader#1。例子2


以下情况:我们的阅读器将始终返回0和nil作为结果。从合同的角度来看,这里的一切都是合法的-没有问题。唯一需要注意的是:文档说,除了发送的切片的长度为零的情况外,不建议实现返回值0和nil。

在现实生活中,这样的阅读器会造成很多麻烦。那么,我们回到问题,我们应该信任读者吗?例如,bufio中内置了一个检查:它顺序地准确读取Reader 100次-如果返回这样的一对值100次,则仅返回NoProgress。

bytes.Buffer中没有类似的内容。如果运行此示例,我们将得到一个无限循环(ReadAll在后台使用bytes.Buffer,而不是Reader本身):



io.Reader#1。例子2


再举一个例子。它也很综合,但是对理解很有用:



在这里,我们总是返回1和nil。似乎这里也没有问题-从合同的角度来看,一切都是合法的。有一个细微差别:如果我在计算机上运行此示例,则该示例将在30秒后冻结...

这是由于以下事实:读取此Reader的客户端(即bytes.Buffer)永远不会获得数据结尾的信号-它读取,减法...加号,他每次都会得到一个减法字节。对他来说,这意味着重新定位的缓冲区在某个时刻结束,它仍在运行-情况重复出现,并且一直运行到无穷大直到破裂。

io.Reader#2。错误返回


我们来介绍实现Reader接口的第二条重要规则-这是错误返回。该文档指出了实现应返回的三个错误。其中最重要的是EOF。

EOF是数据结束的标志,当数据用完时,实现应返回EOF。从概念上讲,这通常不是错误,而是错误。

还有另一个错误称为UnexpectedEOF。如果在读取Reader时突然无法再读取数据,则认为它将返回UnexpectedEOF。但是实际上,此错误仅在标准库的一个位置(在ReadAtLeast函数中)使用。



另一个错误是我们已经谈到的NoProgress。文档说:这是接口实现很烂的标志。

io.reader#3


该文档针对如何正确返回错误规定了一系列情况。在下面您可以看到三种可能的情况:



我们可以同时返回错误,减去字节数,然后分别返回。但是,如果您的数据突然在Reader中用完,并且您现在无法返回[结束符号] EOF(标准库的许多实现都以这种方式工作),则假定您将EOF返回到下一个连续调用(也就是说,您必须放手)顾客)。

对于客户而言,这意味着没有更多数据了-不要再找我了。如果您返回nil,并且客户需要数据,那么他应该再次找您。

io.Reader。失误


读者认为,通常来说,这些是主要的重要规则。仍然有一些小问题,但是它们并不那么重要,也不会导致这种情况:



在阅读与Reader相关的所有内容之前,我们需要回答以下问题:重要的是,自定义实现中是否经常发生错误?为了回答这个问题,我转向了1000个存储库的后台处理程序(那里有大约550个自定义实现)。我用眼睛看了前一百。当然,这不是超级分析,而是什么……我

确定了两个最流行的错误:
  • 从不返回EOF;
  • 对包装好的Reader的信任度过高。

同样,从我的角度来看,这是一个问题。对于那些正在观看io软件包的人来说,这不是问题。我们将再次讨论。

我想回到细微差别。请参阅:



客户端永远不要将对0和nil解释为EOF。这是错误!对于Reader来说,这个价值只是放任客户的机会。因此,我提到的两个错误似乎无关紧要,但是足以想象您的产品中有多层管道,中间有一个小巧的“ bagul”,那么“地下敲门”将不会花费很长时间-可以保证!

据读者说,基本上一切。这些是基本的实施规则。

作家


在管道的另一端,我们有io.Writer,这是我们通常在其中写入数据的地方。一个非常相似的接口:它也包含一个方法(写),它们的签名是相似的。从语义的角度来看,Writer接口更易于理解:我想说的是,它是书面的。



Write方法采用一个切片字节并将其完整写入。他还具有一组必须遵循的规则。

  1. 其中第一个涉及返回的写入字节数。我要说的不是那么严格,因为我没有找到一个单一的例子来说明何时会导致一些严重的后果,例如导致恐慌。这不是很严格,因为有以下规则...
  2. 每当写入的数据量小于发送的数据量时,要求Writer实现返回错误。即,不支持部分记录。这意味着写入多少字节不是很重要。
  3. 另一个规则:Writer绝不应修改发送的切片,因为客户端仍将使用此切片。
  4. 作家不应该持有这个片(读者有相同的规则)。如果您需要在实现中进行某些操作的数据,只需复制这张幻灯片即可。



通过读者和作家,仅此而已。

树状图


特别是对于本报告,我生成了一个实现图,并以树状图的形式进行了设计。现在想要的人可以遵循以下QR码:



该树状图具有io包的所有接口的所有实现。仅需要了解此树状图即可:在管道中可以将哪些内容与哪些内容粘贴在一起,可以在何处读取,可以在何处写入。我仍会在报告过程中参考它,因此请参考QR码。

流水线


我们讨论了什么是io.Writer。现在,我们来讨论标准库中用于构建管道的API。让我们从基础开始。也许对任何人来说都没有意思。但是,这非常重要。

我们将从标准输入流(来自Stdin)中读取数据:



Stdin在Go中由os包中类型为file的全局变量表示。如果查看树状图,您会注意到该文件类型也实现了Reader和Writer接口。

现在,我们对Reader感兴趣。我们将使用已经使用过的相同ReadAll助手读取Stdin。

值得一提的是关于此帮助程序的细微差别:ReadAll会将Reader读到末尾,但它会根据我们所讨论的末尾的标志来确定EOF的末尾。
现在,我们将限制从Stdin读取的数据量。为此,标准库中有一个LimitedReader的实现:



我希望您注意LimitedReader如何限制要读取的字节数。有人会认为这种实现方式,即包装器,会减去包装在阅读器中的所有内容,然后根据需要提供尽可能多的内容。但是一切工作都有些不同...

LimitedReader沿上边界修剪作为参数提供给它的切片。然后,他将裁剪后的切片传递给Reader,然后将其包裹起来。这清楚地展示了如何在io.Reader接口实现中调节读取数据的长度。

错误返回文件结尾


另一个有趣的地方:请注意此实现如何返回EOF错误!这里使用返回的命名值,它们由我们从包装的Reader中获取的值分配。

如果碰巧包装的Reader中有更多的数据,我们会分配包装的Reader的值-例如10个字节和nil-因为包装的Reader中仍有数据。但是变量n减小(倒数第二行),它表示我们已经到达了“底端”,即我们所需的终点。

在下一次迭代中,客户应该再次来-在第一个条件下,他将收到EOF。我提到的就是这种情况。

即将继续...


一点广告:)


感谢您与我们在一起。你喜欢我们的文章吗?想看更多有趣的资料吗?通过下订单或向您的朋友推荐给开发人员的基于云的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