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代码包恰恰就可以作为这样的一个标杆,它可以成为我们运用这种技巧时的一个参考标准。

package main

import (
"bytes"
"fmt"
"io"
"strings"
) func main() {
// 示例1。
builder := new(strings.Builder)
_ = interface{}(builder).(io.Writer)
_ = interface{}(builder).(io.ByteWriter)
_ = interface{}(builder).(fmt.Stringer) // 示例2。
reader := strings.NewReader("")
_ = interface{}(reader).(io.Reader)
_ = interface{}(reader).(io.ReaderAt)
_ = interface{}(reader).(io.ByteReader)
_ = interface{}(reader).(io.RuneReader)
_ = interface{}(reader).(io.Seeker)
_ = interface{}(reader).(io.ByteScanner)
_ = interface{}(reader).(io.RuneScanner)
_ = interface{}(reader).(io.WriterTo) // 示例3。
buffer := bytes.NewBuffer([]byte{})
_ = interface{}(buffer).(io.Reader)
_ = interface{}(buffer).(io.ByteReader)
_ = interface{}(buffer).(io.RuneReader)
_ = interface{}(buffer).(io.ByteScanner)
_ = interface{}(buffer).(io.RuneScanner)
_ = interface{}(buffer).(io.WriterTo) _ = interface{}(buffer).(io.Writer)
_ = interface{}(buffer).(io.ByteWriter)
_ = interface{}(buffer).(io.ReaderFrom) _ = interface{}(buffer).(fmt.Stringer) // 示例4。
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())
}
}

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

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

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

1、io.ReadWriter:此接口既是io.Reader的扩展接口,也是io.Writer的扩展接口。换句话说,该接口定义了一组行为,包含且仅包含了基本的字节序列读取方法Read,和字节序列写入方法Write。

2、io.ReadCloser:此接口除了包含基本的字节序列读取方法之外,还拥有一个基本的关闭方法Close。后者一般用于关闭数据读写的通路。这个接口其实是io.Reader接口和io.Closer接口的组合。

3、io.ReadWriteCloser:很明显,此接口是io.Reader、io.Writer和io.Closer这三个接口的组合。

4、io.ReadSeeker:此接口的特点是拥有一个用于寻找读写位置的基本方法Seek。更具体地说,该方法可以根据给定的偏移量基于数据的起始位置、末尾位置,或者当前读写位置去寻找新的读写位置。这个新的读写位置用于表明下一次读或写时的起始索引。Seek是io.Seeker接口唯一拥有的方法。

5、io.ReadWriteSeeker:显然,此接口是另一个三合一的扩展接口,它是io.Reader、io.Writer和io.Seeker的组合。

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

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

2、*io.SectionReader:此类型的基本类型可以包装io.ReaderAt类型的值,并且会限制它的Read方法,只能够读取原始数据中的某一个部分(或者说某一段)。这个数据段的起始位置和末尾位置,需要在它被初始化的时候就指明,并且之后无法变更。该类型值的行为与切片有些类似,它只会对外暴露在其窗口之中的那些数据。

3、*io.teeReader:此类型是一个包级私有的数据类型,也是io.TeeReader函数结果值的实际类型。这个函数接受两个参数r和w,类型分别是io.Reader和io.Writer。其结果值的Read方法会把r中的数据经过作为方法参数的字节切片p写入到w。可以说,这个值就是r和w之间的数据桥梁,而那个参数p就是这座桥上的数据搬运者。

4、*io.multiReader:此类型也是一个包级私有的数据类型。类似的,io包中有一个名为MultiReader的函数,它可以接受若干个io.Reader类型的参数值,并返回一个实际类型为io.multiReader的结果值。当这个结果值的Read方法被调用时,它会顺序地从前面那些io.Reader类型的参数值中读取数据。因此,我们也可以称之为多对象读取器。

5、io.pipe:此类型为一个包级私有的数据类型,它比上述类型都要复杂得多。它不但实现了io.Reader接口,而且还实现了io.Writer接口。实际上,io.PipeReader类型和io.PipeWriter类型拥有的所有指针方法都是以它为基础的。这些方法都只是代理了io.pipe类型值所拥有的某一个方法而已。又因为io.Pipe函数会返回这两个类型的指针值并分别把它们作为其生成的同步内存管道的两端,所以可以说,io.pipe类型就是io包提供的同步内存管道的核心实现。

6、*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.ReadWriter;
  • io.ReadCloser;
  • io.ReadWriteCloser;

这几个接口。

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

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

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

package main

import (
"fmt"
"io"
"strings"
"sync"
"time"
) func main() {
comment := "Package io provides basic interfaces to I/O primitives. " +
"Its primary job is to wrap existing implementations of such primitives, " +
"such as those in package os, " +
"into shared public interfaces that abstract the functionality, " +
"plus some other related primitives." // 示例1。
fmt.Println("New a string reader and name it \"reader1\" ...")
reader1 := strings.NewReader(comment)
buf1 := make([]byte, 7)
n, err := reader1.Read(buf1)
var offset1, index1 int64
executeIfNoErr(err, func() {
fmt.Printf("Read(%d): %q\n", n, buf1[:n])
offset1 = int64(53)
index1, err = reader1.Seek(offset1, io.SeekCurrent)
})
executeIfNoErr(err, func() {
fmt.Printf("The new index after seeking from current with offset %d: %d\n",
offset1, index1)
n, err = reader1.Read(buf1)
})
executeIfNoErr(err, func() {
fmt.Printf("Read(%d): %q\n", n, buf1[:n])
})
fmt.Println() // 示例2。
reader1.Reset(comment)
num1 := int64(7)
fmt.Printf("New a limited reader with reader1 and number %d ...\n", num1)
reader2 := io.LimitReader(reader1, 7)
buf2 := make([]byte, 10)
for i := 0; i < 3; i++ {
n, err = reader2.Read(buf2)
executeIfNoErr(err, func() {
fmt.Printf("Read(%d): %q\n", n, buf2[:n])
})
}
fmt.Println() // 示例3。
reader1.Reset(comment)
offset2 := int64(56)
num2 := int64(72)
fmt.Printf("New a section reader with reader1, offset %d and number %d ...\n", offset2, num2)
reader3 := io.NewSectionReader(reader1, offset2, num2)
buf3 := make([]byte, 20)
for i := 0; i < 5; i++ {
n, err = reader3.Read(buf3)
executeIfNoErr(err, func() {
fmt.Printf("Read(%d): %q\n", n, buf3[:n])
})
}
fmt.Println() // 示例4。
reader1.Reset(comment)
writer1 := new(strings.Builder)
fmt.Println("New a tee reader with reader1 and writer1 ...")
reader4 := io.TeeReader(reader1, writer1)
buf4 := make([]byte, 40)
for i := 0; i < 8; i++ {
n, err = reader4.Read(buf4)
executeIfNoErr(err, func() {
fmt.Printf("Read(%d): %q\n", n, buf4[:n])
})
}
fmt.Println() // 示例5。
reader5a := strings.NewReader(
"MultiReader returns a Reader that's the logical concatenation of " +
"the provided input readers.")
reader5b := strings.NewReader("They're read sequentially.")
reader5c := strings.NewReader("Once all inputs have returned EOF, " +
"Read will return EOF.")
reader5d := strings.NewReader("If any of the readers return a non-nil, " +
"non-EOF error, Read will return that error.")
fmt.Println("New a multi-reader with 4 readers ...")
reader5 := io.MultiReader(reader5a, reader5b, reader5c, reader5d)
buf5 := make([]byte, 50)
for i := 0; i < 8; i++ {
n, err = reader5.Read(buf5)
executeIfNoErr(err, func() {
fmt.Printf("Read(%d): %q\n", n, buf5[:n])
})
}
fmt.Println() // 示例6。
fmt.Println("New a synchronous in-memory pipe ...")
pReader, pWriter := io.Pipe()
_ = interface{}(pReader).(io.ReadCloser)
_ = interface{}(pWriter).(io.WriteCloser) comments := [][]byte{
[]byte("Pipe creates a synchronous in-memory pipe."),
[]byte("It can be used to connect code expecting an io.Reader "),
[]byte("with code expecting an io.Writer."),
} // 这里添加这个同步工具纯属为了保证下面示例中的打印语句都能够执行完成。
// 在实际使用中没有必要这样做。
var wg sync.WaitGroup
wg.Add(2) go func() {
defer wg.Done()
for _, d := range comments {
time.Sleep(time.Millisecond * 500)
n, err := pWriter.Write(d)
if err != nil {
fmt.Printf("write error: %v\n", err)
break
}
fmt.Printf("Written(%d): %q\n", n, d)
}
pWriter.Close()
}()
go func() {
defer wg.Done()
wBuf := make([]byte, 55)
for {
n, err := pReader.Read(wBuf)
if err != nil {
fmt.Printf("read error: %v\n", err)
break
}
fmt.Printf("Read(%d): %q\n", n, wBuf[:n])
}
}()
wg.Wait()
} func executeIfNoErr(err error, f func()) {
if err != nil {
fmt.Printf("error: %v\n", err)
return
}
f()
}

总结

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

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

笔记源码

https://github.com/MingsonZheng/go-core-demo

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

Go语言核心36讲(Go语言实战与应用十八)--学习笔记的更多相关文章

  1. Go语言核心36讲(Go语言实战与应用二)--学习笔记

    24 | 测试的基本规则和流程(下) Go 语言是一门很重视程序测试的编程语言,所以在上一篇中,我与你再三强调了程序测试的重要性,同时,也介绍了关于go test命令的基本规则和主要流程的内容.今天我 ...

  2. Go语言核心36讲(Go语言基础知识三)--学习笔记

    03 | 库源码文件 在我的定义中,库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用(只要遵从 Go 语言规范的话). 这里的"其他代码" ...

  3. Go语言核心36讲(Go语言实战与应用一)--学习笔记

    23 | 测试的基本规则和流程 (上) 在接下来的日子里,我将带你去学习在 Go 语言编程进阶的道路上,必须掌握的附加知识,比如:Go 程序测试.程序监测,以及 Go 语言标准库中各种常用代码包的正确 ...

  4. Go语言核心36讲(Go语言实战与应用三)--学习笔记

    25 | 更多的测试手法 在本篇文章,我会继续为你讲解更多更高级的测试方法.这会涉及testing包中更多的 API.go test命令支持的,更多标记更加复杂的测试结果,以及测试覆盖度分析等等. 前 ...

  5. Go语言核心36讲(Go语言实战与应用四)--学习笔记

    26 | sync.Mutex与sync.RWMutex 从本篇文章开始,我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包.这会涉及这些代码包的标准用法.使用禁忌.背后原理以及周边的知识. ...

  6. Go语言核心36讲(Go语言实战与应用十二)--学习笔记

    34 | 并发安全字典sync.Map (上) 我们今天再来讲一个并发安全的高级数据结构:sync.Map.众所周知,Go 语言自带的字典类型map并不是并发安全的. 前导知识:并发安全字典诞生史 换 ...

  7. Go语言核心36讲(Go语言实战与应用十四)--学习笔记

    36 | unicode与字符编码 在开始今天的内容之前,我先来做一个简单的总结. Go 语言经典知识总结 在数据类型方面有: 基于底层数组的切片: 用来传递数据的通道: 作为一等类型的函数: 可实现 ...

  8. Go语言核心36讲(Go语言实战与应用二十二)--学习笔记

    44 | 使用os包中的API (上) 我们今天要讲的是os代码包中的 API.这个代码包可以让我们拥有操控计算机操作系统的能力. 前导内容:os 包中的 API 这个代码包提供的都是平台不相关的 A ...

  9. Go语言核心36讲(Go语言实战与应用二十四)--学习笔记

    46 | 访问网络服务 前导内容:socket 与 IPC 人们常常会使用 Go 语言去编写网络程序(当然了,这方面也是 Go 语言最为擅长的事情).说到网络编程,我们就不得不提及 socket. s ...

随机推荐

  1. 2021年1月-第02阶段-前端基础-HTML+CSS进阶-VS Code 软件

    软件安装 VSCode软件 能够安装 VS Code 能够熟练使用 VS Code 软件 能够安装 VS Code 最常用的插件 1. VS Code简介 1.1 VS Code 简介 Visual ...

  2. PAT (Basic Level) Practice (中文)1031 查验身份证 (15分)

    1031 查验身份证 (15分) 一个合法的身份证号码由17位地区.日期编号和顺序编号加1位校验码组成.校验码的计算规则如下: 首先对前17位数字加权求和,权重分配为: {7,9,10,5,8,4,2 ...

  3. 保护模式篇——PAE分页

    写在前面   此系列是本人一个字一个字码出来的,包括示例和实验截图.由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新. 如有好的建议,欢迎反馈.码字不易, ...

  4. Noip模拟16 2021.7.15

    题目真是越来越变态了 T1 Star Way To Heaven 首先,你要看出这是一个最小生成树的题(妙吧?) 为什么可以呢? 我们发现从两点连线的中点过是最优的,但是上下边界怎么办呢? 我们把上下 ...

  5. 方阵里面的dp

    打了一场luogu的信心赛,惊讶地发现我不会T2,感觉像这样在矩阵里面的dp看起来很套路的样子,但是仔细想想还是有很多需要注意的细节. 又想到之前貌似也考过一些类似的题目 然而我并没有改 ,于是打算补 ...

  6. PCB板HDI板几阶是什么意思

    http://blog.sina.com.cn/s/blog_55ff6d5d0102xxvx.html

  7. 该如何有效的提高C/C++语言编程能力

    很多答案都谈到算法的重要性,我的答案主要集中在C++上,只是一些个人经验. 其实我以前也有这样的困惑,感觉完了不知道怎么用.而且我也不是学计算机的,也没有从事相关工作,所以大概有十年的时间都没写什么程 ...

  8. shell IO重定向

    I/O重定向 默认情况下,有3个"文件"处于打开状态,stdin,stdout,stderr:重定向的解释:捕捉一个文件,命令,程序,脚本或者脚本中的代码块的输出,然后将这些输出作 ...

  9. 痞子衡嵌入式:借助Serial Plot软件测量i.MXRT系列FlexSPI驱动Flash页编程执行时间

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是i.MXRT系列FlexSPI驱动Flash页编程执行时间. 痞子衡之前写过一篇文章 <串行NOR Flash的页编程模式对于量产 ...

  10. 我的笔记本电脑瞬间扩大一个T的容量!

    前言 不知道有多少人在家里搭建中央存储设备的,也就是NAS.这个东西在我日常生活中,存储了大量的个人资料,家人们的照片,技术的资料,还有各种高清影视剧.搭配公网的IP,可以真正做到,任何时候任何地点的 ...