字符操作和字节操作类型的接口
首先,了解一下strings.Builder、strings.Reader和bytes.Buffer这三个数据类型中实现的接口。
strings.Builder类型
strings.Builder类型主要用于构建字符串,它的指针类型实现的接口有:
- io.Writer
- io.ByteWriter
- fmt.Stringer
- io.stringWriter,io包的包级私有接口
strings.Reader类型
strings.Reader类型主要用于读取字符串,它的指针类型实现的接口有:
- io.Reader
- io.ReaderAt
- io.ByteReader
- io.RuneReader
- io.Seeker
- io.ByteScanner,这个是io.ByteReader接口的扩展
- io.RuneScanner, 这个是io.RuneReader接口的扩展
- io.WriterTo
bytes.Buffer类型
bytes.Buffer是集读、写功能于一身的数据类型,它非常适合作为字节序列的缓冲区。它的指针类型实现的接口非常多。
该指针类型实现的读取相关的接口有:
- io.Reader
- io.ByteReader
- io.RuneReader
- io.ByteScanner
- io.RuneScanner
- io.WriterTo
该指针类型实现的写入相关的接口有:
- io.Writer
- io.ByteWriter
- io.stringWriter,io包的包级私有接口
- io.ReaderFrom
另外,还有一个导出相关的接口:fmt.Stringer
验证代码
下面的代码对公开的接口进行了验证:
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
func main() {
b1 := new(strings.Builder)
_ = interface{}(b1).(io.Writer)
_ = interface{}(b1).(io.ByteWriter)
_ = interface{}(b1).(fmt.Stringer)
b2 := strings.NewReader("")
_ = interface{}(b2).(io.Reader)
_ = interface{}(b2).(io.ReaderAt)
_ = interface{}(b2).(io.ByteReader)
_ = interface{}(b2).(io.RuneReader)
_ = interface{}(b2).(io.Seeker)
_ = interface{}(b2).(io.ByteScanner)
_ = interface{}(b2).(io.RuneScanner)
_ = interface{}(b2).(io.WriterTo)
b3 := bytes.NewBuffer([]byte{})
_ = interface{}(b3).(io.Reader)
_ = interface{}(b3).(io.ByteReader)
_ = interface{}(b3).(io.RuneReader)
_ = interface{}(b3).(io.ByteScanner)
_ = interface{}(b3).(io.RuneScanner)
_ = interface{}(b3).(io.WriterTo)
_ = interface{}(b3).(io.Writer)
_ = interface{}(b3).(io.ByteWriter)
_ = interface{}(b3).(io.ReaderFrom)
_ = interface{}(b3).(fmt.Stringer)
}
io包
上面的这些类型实现了这么多的接口,目的是为了提高不同程序实体之间的互操作性。
在io包中,有如下几个用于拷贝数据的函数:
- io.Copy
- io.CopyBuffer
- io.CopyN
这几个函数在功能上略有差别,但是首先都会接收2个参数:
- dst,io.Writer类型,表示数据目的
- src,io.Reader类型,表示数据来源
而这些函数的功能大致上也是把数据从src拷贝到dst。用了接口之后,不论给予参数值是什么类型的,只要实现了接口就行。只要实现了接口,这些函数几乎就可以正常执行了。当然,在函数中还会对必要的参数值进行有效性的检查,如果检查不通过,它的执行也是不能够成功结束的。
io.CopyN函数举例
来看下面的示例代码:
package main
import (
"fmt"
"io"
"os"
"strings"
)
func main() {
src := strings.NewReader("Happy New Year")
dst := new(strings.Builder)
written, err := io.CopyN(dst, src, 5)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
}
fmt.Println(written, dst.String())
}
首先,使用了strings.NewReader创建了一个字符串读取器,并把它赋值给了变量src,然后有new了一个字符串构建器,并将其赋予了变量dst。
之后,调用了io.CopyN函数的时候,把两个变量的值都传递了进去,同时还指定了第三个参数int64类型,就是要从src中拷贝多少个字节到dst里。
虽然,变量src和dst类型分别是strings.Reader和strings.Builder,但是当它们被传到io.CopyN函数的时候,就已经分别被包装成了io.Reader类型和io.Writer类型的值。而io.CopyN函数也根本不会去在意它们的实际类型到底是什么。为了优化的目的,io.CopyN函数中的代码会对参数值进行再包装,也会检测这些参数值是否还实现了别的接口,甚至还会去探求某个参数值被包装后的实际类型,是否未某个特殊的类型。但是总体上来看,这些代码都是面向参数声明中的接口来做的。
面向接口编程
在上面的示例中,通过面向接口编程,极大地拓展了它的适用范围和应用场景。换个角度来看,正式因为strings.Reader类型和strings.Builder类型都实现了不少接口,所以他们的值才能够被使用在更广阔的场景中。比如strings包和bytes包中的数据类型在实现了若干接口之后得到了很多好处,这就是面向接口编程带来的优势。
在Go语言中,对接口的扩展是通过类型之间的嵌入来实现的,这也常被叫做接口的组合。这个在讲接口的时候也提过,Go语言提倡使用小接口加接口组合的方式,来扩展程序的行为以及增加程序的灵活性。io代码包恰恰就可以作为这样的一个标杆,它可以成为我们运用这种技巧是的一个参考标准。
接口扩展和实现
以io.Reader接口为对象,来了解一下接口扩展和实现,以及各自的功用。
在io包中,io.Reader的扩展接口有下面几种:
- io.ReadWriter,既是io.Reader的扩展接口,也是io.Writer的扩展接口。该接口定了以一组行为,包含且仅包含了基本的字节序列读取方法Read,和字节序列写入方法Write。
- io.ReadCloser,io.Reader接口和io.Closer接口的组合。除了包含基本的字节序列读取方法之外,还拥有一个基本的关闭方法Close。关闭方法一般用于关闭数据读写的通路。
- 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.SectionReader
- *io.teeReader
- *io.multiReader
- *io.pipe
- *io.PipeReader
这里忽略掉了测试源码文件中的实现类型,以及不会以任何形式直接对外暴露的那些实现类型。
*io.LimitedReader
结构体类型如下:
type LimitedReader struct {
R Reader // underlying reader
N int64 // max bytes remaining
}
此类型的基本类型会包装io.Reader类型的值,并提供一个额外的受限读取的功能。所谓的受限读取指的是,此类型的读取方法和Read返回的总数据量会受到限制,无论该方法被调用多少次。这个限制由该类型的字段N表明,单位是字节。
*io.SectionReader
结构体类型如下:
type SectionReader struct {
r ReaderAt
base int64
off int64
limit int64
}
此类型的基本类型会包装io.ReaderAt类型的值,并且会限制它的Read方法,只能够读取原始数据中的某一个部分,或者说一段。这个数据段的起始位置和末尾位置,需要在它被初始化的时候就指明,并且之后无法变更。该类型值的行为与切片有些类似,它只会对外暴露在其窗口之中的那些数据。
*io.teeReader
结构体类型如下:
type teeReader struct {
r Reader
w Writer
}
func TeeReader(r Reader, w Writer) Reader {
return &teeReader{r, w}
}
此类型是一个包级私有的数据类型,也是io.TeeReader函数结果值的实际类型,这个函数接受两个参数r和w。*teeReader的Read方法会把r中的数据经过作为方法参数的字节切片p写入到w。可以说,这是一个r和w之间的数据桥梁,而那个参数p就是这座桥上的数据搬运者。
*io.multiReader
结构体类型如下:
type multiWriter struct {
writers []Writer
}
func MultiReader(readers ...Reader) Reader {
r := make([]Reader, len(readers))
copy(r, readers)
return &multiReader{r}
}
此类型也是一个包级私有的数据类型。通过io.MultiReader函数,接受若干个io.Reader类型的参数值,返回一个实例类型为*io.multiWriter的结果值。它的Read方法被调用时,会顺序的从前面的那些io.Reader类型的参数值中读取数据。因此,也可以称之为多对象读取器。
*io.pipe
结构体类型如下:
type pipe struct {
wrMu sync.Mutex // Serializes Write operations
wrCh chan []byte
rdCh chan int
once sync.Once // Protects closing done
done chan struct{}
rerr atomicError
werr atomicError
}
func Pipe() (*PipeReader, *PipeWriter) {
p := &pipe{
wrCh: make(chan []byte),
rdCh: make(chan int),
done: make(chan struct{}),
}
return &PipeReader{p}, &PipeWriter{p}
}
此类型为一个包级私有的数据类型,它比较复杂。不但实现了io.Reader接口,而且还实现了io.Writer接口。io.PipeReader类型和io.PipeWriter类型拥有的所有指针方法都是以它为基础的。这些方法都只是代理了io.pipe类型值所拥有的某一个方法而已。又因为,io.Pipe函数会返回这两个类型的指针值并分别把它们作为其生成的同步内存管理的两端,所以*io.pipe类型就是io包提供的同步内存管道的核心实现。
*io.PipeReader
结构体类型如下:
type PipeReader struct {
p *pipe
}
此类型可以被视为io.pipe类型的代理类型。它代理了io.pipe中一部分功能,并基于io.pipe实现了io.ReadCloser接口。同时,它还定义了同步内存管道的读取端。
集中示例展示
上面所讲的每一个类型都写了一小段代码,展示了这些类型的一些基本用法:
package main
import (
"fmt"
"io"
"os"
"strings"
"sync"
)
// 统一定义一个方法来处理错误,这样不会看到很多 if err != nil {} 这种
func executeIfNoErr(err error, f func()) {
if err != nil {
fmt.Fprintf(os.Stderr, "\tERROR: %v\n", err)
return
}
f()
}
func main() {
comment := "Make the plan. " +
"Execute the plan. " +
"Expect the plan to go off the rails. " +
"Throw away the plan."
fmt.Println("原生string类型:")
reader1 := strings.NewReader(comment)
buf1 := make([]byte, 4)
n, err := reader1.Read(buf1)
var offset1, index1 int64
executeIfNoErr(err, func() {
fmt.Printf("\tRead(%d): %q\n", n, buf1[:n])
offset1 = int64(5)
index1, err = reader1.Seek(offset1, io.SeekCurrent)
})
executeIfNoErr(err, func() {
fmt.Printf("\t偏移量: %d, %d\n", offset1, index1)
n, err = reader1.Read(buf1)
})
executeIfNoErr(err, func() {
fmt.Printf("\tRead(%d): %q\n", n, buf1[:n])
})
reader1.Reset(comment)
num2 := int64(15)
fmt.Printf("LimitReader类型,限制数据量(%d):\n", num2)
reader2 := io.LimitReader(reader1, num2)
buf2 := make([]byte, 4)
for i := 0; i < 6; i++ {
n, err := reader2.Read(buf2)
executeIfNoErr(err, func() {
fmt.Printf("\tRead(%d): %q\n", n, buf2[:n])
})
}
reader1.Reset(comment)
offset3 := int64(33)
num3 := int64(37)
fmt.Printf("SectionReader类型,起始偏移量(%d),到末端的长度(%d):\n", offset3, num3)
reader3 := io.NewSectionReader(reader1, offset3, num3)
buf3 := make([]byte, 15)
for i := 0; i < 5; i++ {
n, err := reader3.Read(buf3)
executeIfNoErr(err, func() {
fmt.Printf("\tRead(%d): %q\n", n, buf3[:n])
})
}
reader1.Reset(comment)
writer4 := new(strings.Builder)
fmt.Printf("teeReader类型,write4现在应该为空(%q):\n", writer4)
reader4 := io.TeeReader(reader1, writer4)
buf4 := make([]byte, 33)
for i := 0; i < 5; i++ {
n, err := reader4.Read(buf4)
executeIfNoErr(err, func() {
fmt.Printf("\tRead(%d): %q\n", n, buf4[:n])
fmt.Printf("\tWrite: %q\n", writer4)
})
}
reader5a := strings.NewReader("Make the plan.")
reader5b := strings.NewReader("Execute the plan.")
reader5c := strings.NewReader("Expect the plan to go off the rails.")
reader5d := strings.NewReader("Throw away the plan.")
fmt.Println("multiWriter类型,一共4个readers:")
reader5 := io.MultiReader(reader5a, reader5b, reader5c, reader5d)
buf5 := make([]byte, 15)
for i := 0; i < 10; i++ {
n, err := reader5.Read(buf5)
executeIfNoErr(err, func() {
fmt.Printf("\tRead(%d): %q\n", n, buf5[:n])
})
}
fmt.Println("pipe类型:")
pReader, pWriter := io.Pipe()
_ = interface{}(pReader).(io.ReadCloser) // 验证是否实现了 io.ReadCloser 接口
_ = interface{}(pWriter).(io.WriteCloser)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
n, err := pWriter.Write([]byte(comment))
defer pWriter.Close()
executeIfNoErr(err, func() {
fmt.Printf("\tWrite(%d)\n", n)
})
}()
go func() {
defer wg.Done()
buf6 := make([]byte, 15)
for i := 0; i < 10; i++ {
n, err := pReader.Read(buf6)
executeIfNoErr(err, func() {
fmt.Printf("\tRead(%d): %q\n", n, buf6[:n])
})
}
}()
wg.Wait()
fmt.Println("所有示例完成")
}
io包中的接口
前面的内容,主要讲的是io.Reader的扩展接口和实现类型。当然,io代码包中的核心接口不止io.Reader一个。这里基于它引出的一条主线只是io包类型体系中的一部分。这里再换个角度来对io包做进一步的了解。
可以把没有嵌入其他接口并且只定义了一个方法的接口叫做简单接口。在io包中,这样的接口共有11个。
另外,有的接口有着众多的扩展接口和实现类型,可以称为核心接口,io包中的核心接口只有3个:
- io.Reader
- io.Writer
- io.Closer
可以把io包中的简单接口分为四大类。这四大类接口分别针对于四种操作:读取、写入、关闭和读写位置设定。前三种操作属于基本的I/O操作。
关于读取操作,已经重点讲过核心接口的io.Reader。它在io包中有5个扩展接口,并有6个实现类型。这个包中针对读取操作的接口还有不少。
io.ByteReader
type ByteReader interface {
ReadByte() (byte, error)
}
简单接口,定义了一个读取方法ReadByte。这个读取方法能够读取下一个单一的字节。
RuneReader
type RuneReader interface {
ReadRune() (r rune, size int, err error)
}
简单接口,定义了一个读取方法ReadRune。这个读取方法能够读取下一个单一的Unicode字符。
io.ByteScanner
type ByteScanner interface {
ByteReader
UnreadByte() error
}
该接口内嵌了简单接口io.ByteReader,并定义了额外的UnreadByte方法。它就抽象了可以读取和读回退单个字节的功能集。
io.RuneScanner
type RuneScanner interface {
RuneReader
UnreadRune() error
}
该接口内嵌了简单接口io.RuneReader,并定义了额外的UnreadRune方法。它抽象了可以读取和读回退单个Unicode字符的功能集。
io.ReaderAt
type ReaderAt interface {
ReadAt(p []byte, off int64) (n int, err error)
}
简单接口,定义了一个ReadAt方法。这是一个纯粹的只读方法,它只去读取其所属值中包含的字节,而不对这个值进行任何改动。比如,它绝对不能去修改已读计数的值。这也是io.ReaderAt接口与其实现类型之间最重要的一个约定。因此,如果仅仅并发的调用某一个值的ReadAt方法,那么安全性应该是可以得到保障的。
io.WriterTo
type WriterTo interface {
WriteTo(w Writer) (n int64, err error)
}
简单接口,定义了一个WriteTo的读取方法。该方法接受一个io.Writer类型的参数值,会把其所属值中的数据读出,并写入到这个参数值中。
io.ReaderFrom
type ReaderFrom interface {
ReadFrom(r Reader) (n int64, err error)
}
简单接口,定义了一个ReadFrom的写入方法。该方法接受一个io.Reader类型的参数值,会从该参数值中读取出数据,并写入到其所属值中。
写入操作相关接口
从上面这些接口中,可以看出,在io包中与写入操作有关的接口都与读取操作相关的接口有着一定的对应关系。下面就说是写入操作有关的接口。
io.Write
io.Write是核心接口。基于它的扩展接口如下:
- io.ReadWriter,实现类型有*io.pipe
- io.ReadWriteCloser
- io.ReadWriteSeeker,在io包中没有这个接口的实现,它的实现类型主要集中在net包中。
- io.WriteCloser
- io.WriteSeker
io.ByteWriter和io.WriterAt
这两个是写入操作相关的简单接口。在io包中,没有他们的实现类型。
顺便提一下这个数据类型:*io.File。这个类型不但是io.WriterAt接口的实现类型,同时还实现了io.ReadWriteCloser接口和io.ReadWriteSeeker接口。就是说,该类型支持的I/O操作非常丰富。
io.Seeker
这个接口是一个读写位置设定相关的简单接口,也仅仅定义了一个Seek方法。该方法主要用于寻找并设定下一次读取或写入时的起始索引位置,在strings包里讲过。
在io包中,有几个基于io.Seeker的扩展接口:
- io.ReadSeeker
- io.ReadWriteSeeker
- io.WriteSeeker,基于io.Writer和io.Seeker的扩展接口
这两个类型的指针:strings.Reader和io.SectionReader,都实现了io.Seeker接口。顺便提一下,这两个类型的指针也都是io.ReaderAt接口的实现类型。
io.Closer
这是关闭操作相关的接口,非常通用,它的扩展接口和实现类型都不少。单从名称上就能看出io包中哪些接口是它的扩展接口。它的实现类型,在io包中只有io.PipeReader和io.PipeWriter。
总结
本篇是为了能够使我们牢记io包中有着网状关系的接口和数据类型。如果暂时未能牢记,至少可以作为深刻记忆它们的开始。
在之后需要思考和时间的是:在什么时候应该编写哪些数据类型实现io包中的哪些接口,并以此得到最大的好处。
有疑问加站长微信联系(非本文作者)