learn-tech/专栏/Go语言核心36讲/40io包中的接口和工具(上).md
2024-10-16 00:01:16 +08:00

13 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        40 io包中的接口和工具 (上)
                        我们在前几篇文章中主要讨论了strings.Builder、strings.Reader和bytes.Buffer这三个数据类型。

知识回顾

还记得吗当时我还问过你“它们都实现了哪些接口”。在我们继续讲解io包中的接口和工具之前我先来解答一下这个问题。

strings.Builder类型主要用于构建字符串它的指针类型实现的接口有io.Writer、io.ByteWriter和fmt.Stringer。另外它其实还实现了一个io包的包级私有接口io.stringWriter自Go 1.12起它会更名为io.StringWriter

strings.Reader类型主要用于读取字符串它的指针类型实现的接口比较多包括

io.Reader io.ReaderAt io.ByteReader io.RuneReader io.Seeker io.ByteScanner io.RuneScanner io.WriterTo

共有8个它们都是io包中的接口。

其中io.ByteScanner是io.ByteReader的扩展接口而io.RuneScanner又是io.RuneReader的扩展接口。

bytes.Buffer是集读、写功能于一身的数据类型它非常适合作为字节序列的缓冲区。 它的指针类型实现的接口就更多了。

更具体地说,该指针类型实现的读取相关的接口有下面几个。

io.Reader io.ByteReader io.RuneReader io.ByteScanner io.RuneScanner io.WriterTo

共有6个。而其实现的写入相关的接口则有这些。

io.Writer io.ByteWriter io.stringWriter io.ReaderFrom

共4个。此外它还实现了导出相关的接口fmt.Stringer。

前导内容io包中接口的好处与优势

那么,这些类型实现了这么多的接口,其动机(或者说目的)究竟是什么呢?

简单地说这是为了提高不同程序实体之间的互操作性。远的不说我们就以io包中的一些函数为例。

在io包中有这样几个用于拷贝数据的函数它们是

io.Copy io.CopyBuffer io.CopyN。

虽然这几个函数在功能上都略有差别但是它们都首先会接受两个参数用于代表数据目的地、io.Writer类型的参数dst以及用于代表数据来源的、io.Reader类型的参数src。这些函数的功能大致上都是把数据从src拷贝到dst。

不论我们给予它们的第一个参数值是什么类型的只要这个类型实现了io.Writer接口即可。

同样的无论我们传给它们的第二个参数值的实际类型是什么只要该类型实现了io.Reader接口就行。

一旦我们满足了这两个条件,这些函数几乎就可以正常地执行了。当然了,函数中还会对必要的参数值进行有效性的检查,如果检查不通过,它的执行也是不能够成功结束的。

下面来看一段示例代码:

src := strings.NewReader( "CopyN copies n bytes (or until an error) from src to dst. " + "It returns the number of bytes copied and " + "the earliest error encountered while copying.") dst := new(strings.Builder) written, err := io.CopyN(dst, src, 58) if err != nil { fmt.Printf("error: %v\n", err) } else { fmt.Printf("Written(%d): %q\n", written, dst.String()) }

我先使用strings.NewReader创建了一个字符串读取器并把它赋给了变量src然后我又new了一个字符串构建器并将其赋予了变量dst。

之后我在调用io.CopyN函数的时候把这两个变量的值都传了进去同时把给这个函数的第三个参数值设定为了58。也就是说我想从src中拷贝前58个字节到dst那里。

虽然变量src和dst的类型分别是strings.Reader和strings.Builder但是当它们被传到io.CopyN函数的时候就已经分别被包装成了io.Reader类型和io.Writer类型的值。io.CopyN函数也根本不会去在意它们的实际类型到底是什么。

为了优化的目的io.CopyN函数中的代码会对参数值进行再包装也会检测这些参数值是否还实现了别的接口甚至还会去探求某个参数值被包装后的实际类型是否为某个特殊的类型。

但是从总体上来看这些代码都是面向参数声明中的接口来做的。io.CopyN函数的作者通过面向接口编程极大地拓展了它的适用范围和应用场景。

换个角度看正因为strings.Reader类型和strings.Builder类型都实现了不少接口所以它们的值才能够被使用在更广阔的场景中。

换句话说如此一来Go语言的各种库中能够操作它们的函数和数据类型明显多了很多。

这就是我想要告诉你的strings包和bytes包中的数据类型在实现了若干接口之后得到的最大好处。

也可以说,这就是面向接口编程带来的最大优势。这些数据类型和函数的做法,也是非常值得我们在编程的过程中去效仿的。

可以看到前文所述的几个类型实现的大都是io代码包中的接口。实际上io包中的接口对于Go语言的标准库和很多第三方库而言都起着举足轻重的作用。它们非常基础也非常重要。

就拿io.Reader和io.Writer这两个最核心的接口来说它们是很多接口的扩展对象和设计源泉。同时单从Go语言的标准库中统计实现了它们的数据类型都各自有上百个而引用它们的代码更是都各自有400多处。

很多数据类型实现了io.Reader接口是因为它们提供了从某处读取数据的功能。类似的许多能够把数据写入某处的数据类型也都会去实现io.Writer接口。

其实有不少类型的设计初衷都是实现这两个核心接口的某个或某些扩展接口以提供比单纯的字节序列读取或写入更加丰富的功能就像前面讲到的那几个strings包和bytes包中的数据类型那样。

在Go语言中对接口的扩展是通过接口类型之间的嵌入来实现的这也常被叫做接口的组合。

我在讲接口的时候也提到过Go语言提倡使用小接口加接口组合的方式来扩展程序的行为以及增加程序的灵活性。io代码包恰恰就可以作为这样的一个标杆它可以成为我们运用这种技巧时的一个参考标准。

下面我就以io.Reader接口为对象提出一个与接口扩展和实现有关的问题。如果你研究过这个核心接口以及相关的数据类型的话这个问题回答起来就并不困难。

我们今天的问题是在io包中io.Reader的扩展接口和实现类型都有哪些它们分别都有什么功用

这道题的典型回答是这样的。在io包中io.Reader的扩展接口有下面几种。

io.ReadWriter此接口既是io.Reader的扩展接口也是io.Writer的扩展接口。换句话说该接口定义了一组行为包含且仅包含了基本的字节序列读取方法Read和字节序列写入方法Write。 io.ReadCloser此接口除了包含基本的字节序列读取方法之外还拥有一个基本的关闭方法Close。后者一般用于关闭数据读写的通路。这个接口其实是io.Reader接口和io.Closer接口的组合。 io.ReadWriteCloser很明显此接口是io.Reader、io.Writer和io.Closer这三个接口的组合。 io.ReadSeeker此接口的特点是拥有一个用于寻找读写位置的基本方法Seek。更具体地说该方法可以根据给定的偏移量基于数据的起始位置、末尾位置或者当前读写位置去寻找新的读写位置。这个新的读写位置用于表明下一次读或写时的起始索引。Seek是io.Seeker接口唯一拥有的方法。 io.ReadWriteSeeker显然此接口是另一个三合一的扩展接口它是io.Reader、io.Writer和io.Seeker的组合。

再来说说io包中的io.Reader接口的实现类型它们包括下面几项内容。

*io.LimitedReader此类型的基本类型会包装io.Reader类型的值并提供一个额外的受限读取的功能。所谓的受限读取指的是此类型的读取方法Read返回的总数据量会受到限制无论该方法被调用多少次。这个限制由该类型的字段N指明单位是字节。

*io.SectionReader此类型的基本类型可以包装io.ReaderAt类型的值并且会限制它的Read方法只能够读取原始数据中的某一个部分或者说某一段。-

这个数据段的起始位置和末尾位置,需要在它被初始化的时候就指明,并且之后无法变更。该类型值的行为与切片有些类似,它只会对外暴露在其窗口之中的那些数据。

*io.teeReader此类型是一个包级私有的数据类型也是io.TeeReader函数结果值的实际类型。这个函数接受两个参数r和w类型分别是io.Reader和io.Writer。-

其结果值的Read方法会把r中的数据经过作为方法参数的字节切片p写入到w。可以说这个值就是r和w之间的数据桥梁而那个参数p就是这座桥上的数据搬运者。

*io.multiReader此类型也是一个包级私有的数据类型。类似的io包中有一个名为MultiReader的函数它可以接受若干个io.Reader类型的参数值并返回一个实际类型为io.multiReader的结果值。-

当这个结果值的Read方法被调用时它会顺序地从前面那些io.Reader类型的参数值中读取数据。因此我们也可以称之为多对象读取器。

*io.pipe此类型为一个包级私有的数据类型它比上述类型都要复杂得多。它不但实现了io.Reader接口而且还实现了io.Writer接口。-

实际上io.PipeReader类型和io.PipeWriter类型拥有的所有指针方法都是以它为基础的。这些方法都只是代理了io.pipe类型值所拥有的某一个方法而已。-

又因为io.Pipe函数会返回这两个类型的指针值并分别把它们作为其生成的同步内存管道的两端所以可以说*io.pipe类型就是io包提供的同步内存管道的核心实现。

*io.PipeReader此类型可以被视为io.pipe类型的代理类型。它代理了后者的一部分功能并基于后者实现了io.ReadCloser接口。同时它还定义了同步内存管道的读取端。

注意,我在这里忽略掉了测试源码文件中的实现类型,以及不会以任何形式直接对外暴露的那些实现类型。

问题解析

我问这个问题的目的主要是评估你对io包的熟悉程度。这个代码包是Go语言标准库中所有I/O相关API的根基所以我们必须对其中的每一个程序实体都有所了解。

然而由于该包包含的内容众多因此这里的问题是以io.Reader接口作为切入点的。通过io.Reader接口我们应该能够梳理出基于它的类型树并知晓其中每一个类型的功用。

io.Reader可谓是io包乃至是整个Go语言标准库中的核心接口所以我们可以从它那里牵扯出很多扩展接口和实现类型。

我在本问题的典型回答中为你罗列和介绍了io包范围内的相关数据类型。

这些类型中的每一个都值得你认真去理解尤其是那几个实现了io.Reader接口的类型。它们实现的功能在细节上都各有不同。

在很多时候,我们可以根据实际需求将它们搭配起来使用。

例如对施加在原始数据之上的由Read方法提供的读取功能进行多层次的包装比如受限读取和多对象读取等以满足较为复杂的读取需求。

在实际的面试中只要应聘者能够从某一个方面出发说出io.Reader的扩展接口及其存在意义或者说清楚该接口的三五个实现类型那么就可以算是基本回答正确了。

比如,从读取、写入、关闭这一系列的基本功能出发,描述清楚:

io.ReadWriter io.ReadCloser io.ReadWriteCloser

这几个接口。

又比如说明白io.LimitedReader和io.SectionReader这两个类型之间的异同点。

再比如,阐述*io.SectionReader类型实现io.ReadSeeker接口的具体方式等等。不过这只是合格的门槛应聘者回答得越全面越好。

我在示例文件demo82.go中写了一些代码以展示上述类型的一些基本用法供你参考。

总结

我们今天一直在讨论和梳理io代码包中的程序实体尤其是那些重要的接口及其实现类型。

io包中的接口对于Go语言的标准库和很多第三方库而言都起着举足轻重的作用。其中最核心的io.Reader接口和io.Writer接口是很多接口的扩展对象或设计源泉。我们下一节会继续讲解io包中的接口内容。

你用过哪些io包中的接口和工具呢又有哪些收获和感受呢你可以给我留言我们一起讨论。感谢你的收听我们下次再见。

戳此查看Go语言专栏文章配套详细代码。