16.输入输出
16.1 简单的内存缓冲区
内存缓冲区可以将数据临时存放在内存中,以供程序读写。当应用程序退出后,数据就会丢失。相较于磁盘文件,直接在内存中读写数据具有更高的效率,适合在程序需要时存放少量数据,不宜使用过于庞大的数据,毕竟内存空间是有限的。
最简单的内存缓冲区就是使用byte类型的数组/切片。例如:
// 初始化[]byte实例,长度为0
var buffer = make([]byte, 0)
// 向缓冲区添加数据
buffer = append(buffer, []byte("一二三四五")...)
buffer = append(buffer, []byte("六七八九十")...)
// 输出
fmt.Printf("缓冲区中的数据: %v\n", buffer)
fmt.Printf("转换为字符串后: %s\n", string(buffer))
运行结果如下:
缓冲区中的数据:[228 184 128 228 186 140 228 184 137 229 155 155 228 186 148 229 133 173 228 184 131 229 133 171 228 185 157 229 141 129]
转换为字符串后:一二三四五六七八九十
调用make函数创建[]byte实例时设置其长度为0,这是为了防止出现空字节。因为长度不为0的[]byte实例会使用数值0来初始化元素列表,当调用append函数后会把新的数据追加到原数据的末尾,这会导致最终的缓冲区出现一段空白内容。假设初始化[]byte实例时设置长度为3,那么它会产生3字节——0、0、0,随后追加4字节——1、2、3、4,最后整个缓冲区变为0、0、0、1、2、3、4,即出现了3个空字节。
16.2 与输入/输出有关的接口类型
io包中提供了一组接口类型,以便于开发人员对输入/输出行为进行封装。直接对byte类型的数组/切片进行处理不具备规范性和统一性,代码散乱,不利于代码的重复利用和维护。
实现io包中的接口可以使读写行为形成独立的功能模块,开发人员可以“拿来就用”,不必去关心其内部是如何实现的,只需要调用相关的方法即可。
常用的接口类型有:
(1)Reader:公开Read方法,实现从数据流中读取部分字节。
(2)Writer:公开Write方法,实现将字节序列写入数据流中。
(3)Closer:公开Close方法,当读写操作完成后关闭数据流,清除对象引用或清理内存。
(4)Seeker:公开Seek方法,可以修改当前位置。调用Seek方法后再调用Read或Write方法,就会从新设置的位置开始处理。
(5)ReadWriter:Reader与Writer的组合,即公开Read和Write方法,同时支持读与写操作。
(6)ReadCloser:Reader与Closer的组合,同时公开Read和Close方法。
(7)WriteCloser:Writer与Closer的组合,同时公开Write和Close方法。
(8)ReadWriteCloser:Reader、Writer、Closer的组合,同时公开Read、Write、Close方法。
(9)ReadWriteSeeker:Reader、Writer、Seeker的组合,同时公开Read、Write、Seek方法。
16.2.1 实现读写功能
下面的示例实现了Write和Read方法——支持写入数据和读取数据功能。
步骤1: 定义一个新的结构体,命名为myBuffer,它包含两个字段。
type myBuffer struct {
// 记录当前读写位置
curPos int
// 封装的内部数据缓冲区
innerBuf []byte
}
myBuffer内部通过一个[]byte类型的字段来存储写入的数据。curPos字段存储当前的位置,如果为0,则表示当前位置在缓冲区的开始位置,当调用完Write或Read方法后,curPos字段的值会增加。
步骤2: 实现Write方法,用于写入数据。
func (b *myBuffer) Write(p []byte) (n int, err error) {
if p == nil || len(p) == 0 {
n = 0
err = nil
return
}
// 如果内部缓冲区未初始化
if b.innerBuf == nil {
b.innerBuf = make([]byte, len(p))
// 将当前位置移到开始位置
b.curPos = 0
}
// 分析一下内部缓冲区是否可容纳新写入的数据
avdSpc := cap(b.innerBuf) - len(b.innerBuf)
if avdSpc < len(p) {
// 重新分配空间
newBuf := make([]byte, b.curPos+len(p))
// 复制原缓冲区中的数据
copy(newBuf, b.innerBuf[:b.curPos])
// 替换旧的缓冲区
b.innerBuf = newBuf
}
n = copy(b.innerBuf[b.curPos:], p)
b.curPos += n
return
}
Write方法的代码需要完成三件事:首先,检查参数p传递的内容是否有效;接着,检查内部的缓冲区(即innerBuf字段所引用的对象)是否有足够的容量来存放新写入的数据,如果容量不够,就创建一个新的[]byte实例并复制旧的数据;最后,将新的数据存入缓冲区中。
copy是标准库中的内置函数,其签名如下:
func copy(dst, src []Type) int
该函数的功能是复制切片实例的元素。dst参数为目标对象(接收元素的切片实例),src参数为源对象(被复制元素的切片实例)。copy函数的返回值为成功复制的元素个数。
步骤3: 实现Read方法,从缓冲区中读出数据。
func (b *myBuffer) Read(p []byte) (n int, err error) {
if p == nil || len(p) == 0 {
n = 0
err = nil
return
}
if b.innerBuf == nil {
err = errors.New("错误:未发现可用的数据")
return
}
// 计算缓冲区剩余的字节数
avalidBytes := len(b.innerBuf) - b.curPos
if avalidBytes == 0 {
// 可用字节数为0,表示已到达末尾
err = io.EOF
return
}
// 复制字节序列
n = copy(p, b.innerBuf[b.curPos:])
b.curPos += n
return
}
读取数据时,要注意验证当前位置是否已经到达缓冲区的末尾,如果是,则表明无数据可读,此时err返回值应当引用一个io.EOF对象。EOF是一个有着特殊用途的错误信息,它表示读写位置已到达缓冲区(或文件)的末尾,后面没有可读的内容。
步骤4: 定义一个Reset方法,它的作用是把curPos字段的值设置为0,即回到缓冲区开头。在数据写入完毕后,需要调用此方法,使Read方法能够顺利读出数据。
func (b *myBuffer) Reset() {
b.curPos = 0
}
步骤5: 定义一个名为CreateNewBuffer的函数,作用是创建myBuffer实例并初始化。
func CreateNewBuffer() *myBuffer {
return &myBuffer{
curPos: 0,
innerBuf: make([]byte, 16),
}
}
myBuffer结构体的实例方法皆使用指针类型,所以 CreateNewBuffer 方法返回新实例的内存地址。使用指针类型能够保证在读写操作过程中,myBuffer实例的唯一性,即CreateNewBuffer函数所返回的myBuffer实例,在调用 Write、Read 方法时不会进行自我复制。例如,实例A在调用Write实例方法时,如果进行了自我复制,就会产生新的实例B,此时数据被写入实例B中,而不是原来的实例A,这会导致实例A中没有存入有效的数据,无法用Read方法读取。
步骤6: 调用前面定义的CreateNewBuffer函数初始化一个myBuffer实例,并赋值给bf变量。
var bf = CreateNewBuffer()
步骤7: 一次性写入36字节。
var data = make([]byte, 36)
n := len(data)
for i := 0; i < n; i++ {
data[i] = byte(i + 1)
}
bf.Write(data)
被写入的36字节是通过for循环产生的。
步骤8: 调用Reset方法,把当前位置重新移到缓冲区的开始处。
bf.Reset()
步骤9: 从myBuffer实例中读出刚刚写入的36字节。
var temp = make([]byte, 5)
for {
c, err := bf.Read(temp)
if err == io.EOF {
// 已到末尾
break
}
fmt.Println(temp[:c])
}
上面代码是通过循环来多次读取内容,每次读入5字节,直到读完为止(出现EOF错误)。
步骤10: 运行示例,结果如下:
读出来的数据:
[1 2 3 4 5] // 第1轮
[6 7 8 9 10] // 第2轮
[11 12 13 14 15] // 第3轮
[16 17 18 19 20] // 第4轮
[21 22 23 24 25] // 第5轮
[26 27 28 29 30] // 第6轮
[31 32 33 34 35] // 第7轮
[36] // 第8轮
16.2.2 嵌套封装
在实现io包中的接口类型时,可以在自定义类型中包装其他现有的类型,即自定义的类型代码中可以调用其他现有类型的成员。
下面通过一个示例说明这一点。此示例定义了两个结构体——TextFileWriter和TextFileReader,前者用来写入文本(字符串类型的数据),后者用于读取文本。这两个结构体中都包含了一个名为file的字段,它的类型是os包中的File结构体。随后在自定义的WriteString方法中会调用File类型的WriteString方法写入数据,在ReadString方法中会调用File类型的Read方法读取数据,并使用strings包中的Builder类型来组建字符串。
步骤1: 定义TextFileWriter结构体。
type TextFileWriter struct {
// 引用打开的文件
file *os.File
}
步骤2: 定义WriteString方法,向文件写入字符串数据。此方法实现了StringWriter接口。
func (w *TextFileWriter) WriteString(s string) (int, error) {
if w.file == nil {
return 0, errors.New("还没有打开文件")
}
return w.file.WriteString(s)
}
步骤3: 定义Close方法,用于关闭文件,释放资源。
func (w *TextFileWriter) Close() error {
return w.file.Close()
}
步骤4: 定义CreateWriter函数,根据提供的文件名创建TextFileWriter实例。
func CreateWriter(filename string) *TextFileWriter {
file, err := os.Create(filename)
if err != nil {
return nil
}
return &TextFileWriter{file: file}
}
步骤5: 定义TextFileReader结构体。
type TextFileReader struct {
file *os.File
}
步骤6: 定义ReadString方法,读取文件中所有内容,并以字符串形式返回。
func (r *TextFileReader) ReadString() (string, error) {
if r.file == nil {
return "", errors.New("还没有打开文件")
}
// 组建字符串
var bd = new(strings.Builder)
var bf = make([]byte, 32)
for {
n, err := r.file.Read(bf)
if err == io.EOF {
break
}
bd.Write(bf[:n])
}
// 返回字符串
return bd.String(), nil
}
在读取的时候,可以先以字节序列的形式分多次读入,然后写入Builder对象中,由Builder对象负责组建字符串。
步骤7: 定义Close方法,关闭文件,释放资源。
func (r *TextFileReader) Close() error {
return r.file.Close()
}
步骤8: 定义CreateReader函数,可根据文件名创建TextFileReader实例。
func CreateReader(filename string) *TextFileReader {
file, err := os.Open(filename)
if err != nil {
return nil
}
return &TextFileReader{file: file}
}
步骤9: 声明一个字符串类型的常量,表示稍后要使用的文件名。
const fileName = "notes.txt"
步骤10: 将文本内容写入文件。
var wt = CreateWriter(fileName)
wt.WriteString("桃花帘外东风软\n")
wt.WriteString("桃花帘内晨妆懒\n")
wt.WriteString("帘外桃花帘内人\n")
wt.WriteString("人与桃花隔不远")
// 关闭文件
wt.Close()
步骤11: 从文件中读出刚写入的文本。
// 读取文本
var rd = CreateReader(fileName)
var content, err = rd.ReadString()
if err != nil {
fmt.Println("错误:", err)
rd.Close()
return
}
// 关闭文件
rd.Close()
fmt.Printf("从文件中读到的文本:\n%s", content)
步骤12: 运行示例程序,结果如下:
从文件中读到的文本:
桃花帘外东风软
桃花帘内晨妆懒
帘外桃花帘内人
人与桃花隔不远
16.3 Buffer类型
Buffer类型是一个结构体,位于bytes包中,它实现内存缓冲区的基本功能——支持读写操作。Buffer类型是标准库实现的类型,开发者可以直接使用。
Buffer实例有以下几种初始化方式:
(1)调用bytes包中提供的NewBuffer函数创建Buffer实例,并使用已有的字节序列去初始化。
(2)调用NewBufferString函数,创建Buffer实例,并用一个字符串来初始化。
(3)直接声明变量(用var关键字),Buffer实例将被初始化为空缓冲区。
(4)使用new函数创建Buffer实例并返回其地址,即指针类型(*Buffer),Buffer实例被初始化为空的缓冲区。
下面的示例演示了Buffer的使用,分三次将数据写入Buffer实例。
// 初始化Buffer对象
var bf bytes.Buffer
// 写入第一批数据
var data = []byte{1, 2, 3, 4, 5, 6}
bf.Write(data)
// 写入第二批数据
data = []byte{7, 8, 9, 10}
bf.Write(data)
// 写入第三批数据
data = []byte{11, 12, 13, 14, 15}
bf.Write(data)
// 获取全部数据
var all = bf.Bytes()
fmt.Printf("缓冲区中的数据:\n%v", all)
Bytes方法返回缓冲区中未被读取的所有字节。在本示例中,由于未曾调用过Read方法,所以Bytes方法返回的是缓冲区中的全部数据。
示例运行结果如下:
缓冲区中的数据:
[1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]
16.4 Copy函数
在程序开发过程中,经常要从一个地方读出数据,然后写到另一个地方。这实际上是一种复制操作,如果数据比较多,要多次调用Read方法从数据源读取内容,再调用Write方法写入数据目标。就像下面这样:
// 用于临时存放数据的缓冲区
var buffer = make([]byte, 16)
// 多次读写
for {
n, err := reader.Read(buffer)
if err == io.EOF {
// 一直读到末尾
break
}
writer.Write(buffer[:n])
}
如果每次复制数据的时候都要将上述代码重复写一遍,就过于烦琐了。io包公开了Copy函数,它封装了数据复制功能,可以直接调用。该函数的签名如下:
func Copy(dst Writer, src Reader) (written int64, err error)
只要赋值给src的类型实现了Reader或ReaderFrom接口,dst实现Writer或WriterTo接口即可。
在下面的示例中,程序代码会将现有文件中的所有内容复制到标准输出流(屏幕输出)中。
步骤1: 新建一个文件,命名为demo.txt,然后输入以下内容。
青青子衿,悠悠我心
但为君故,沉吟至今
输入完成后将文件保存到与程序代码相同的目录下。
步骤2: 声明fileName常量,用于存放文件名。
const fileName = "demo.txt"
步骤3: 打开文件,获得os.File对象。
var reader, err = os.Open(fileName)
if err != nil {
fmt.Println(err)
return
}
步骤4: 复制操作的目标是标准输出流,可以从os.Stdout成员获得其引用。
var writer = os.Stdout
步骤5: 调用Copy函数,执行复制。
_, err = io.Copy(writer, reader)
if err != nil {
fmt.Println(err)
}
步骤6: 运行示例程序,结果如下:
------ 从文件中复制的内容 ------
青青子衿,悠悠我心
但为君故,沉吟至今
需要注意的是,io.Copy函数与内置的copy函数并不相同,copy函数用于复制切片中的元素,而io.Copy函数则用于复制字节流。
16.5 MultiReader函数和MultiWriter函数
MultiReader函数实现从多个数据源(实现Reader接口的对象)读取数据,其读入顺序与Reader对象提供的顺序一致。
下面看一个例子。
// 四个Reader对象
var (
rd1 = strings.NewReader("第一个数据源\n")
rd2 = strings.NewReader("第二个数据源\n")
rd3 = strings.NewReader("第三个数据源\n")
rd4 = strings.NewReader("第四个数据源\n")
)
var mtRd = io.MultiReader(rd1, rd2, rd3, rd4)
// 将数据复制到标准输出流
io.Copy(os.Stdout, mtRd)
上述示例将从四个数据源头读取内容,然后写入标准输出流中,此处可直接使用Copy函数。
strings.NewReader函数创建新的strings.Reader实例,strings.Reader类型专用于读取字符串内容并通过给定的字符串来初始化,然后可以通过Read方法来读取。
代码运行结果如下:
------ 从多个源中读到的内容 ------
第一个数据源
第二个数据源
第三个数据源
第四个数据源
MultiWriter函数的功能是将数据写入到多个Writer对象中。其特点是相同的内容依次写入各个Writer对象中。
下面的例子中,将一组随机生成的字节分别写入到三个文件中。
var (
// 三个Writer对象
wt1, _ = os.Create("file-1.txt")
wt2, _ = os.Create("file-2.txt")
wt3, _ = os.Create("file-3.txt")
)
// 退出时自动关闭文件
defer wt1.Close()
defer wt2.Close()
defer wt3.Close()
// 生成随机字节
data := make([]byte, 24)
rand.Read(data)
// 写入数据
writer := io.MultiWriter(wt1, wt2, wt3)
var n, err = writer.Write(data)
// 若发生错误,则输出错误信息
if err != nil {
fmt.Println(err)
}
fmt.Printf("已成功向三个文件写入%d个字节\n", n)
16.6 SectionReader
SectionReader 类型可以从数据来源中“截取”一部分用于读取操作,大型数据(例如大文件)很适合使用SectionReader来读取。
指向SectionReader实例的指针需要调用以下函数来获取。
func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader
r参数引用其他数据来源(例如大型文件的数据流),要求实现ReaderAt接口,即公开ReadAt方法,使数据流支持从指定位置开始读取内容。off参数表示偏移量——开始读取内容的位置(从0开始计算),n参数指定要读取的数据量,即要读多少个字节。
下面示例演示了SectionReader的使用。
// 原始数据
var dataSrc = strings.NewReader("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
fmt.Println("原数据:")
io.Copy(os.Stdout, dataSrc)
// 截取 off = 12,n = 7
rd := io.NewSectionReader(dataSrc, 12, 7)
fmt.Println("\n\n第一次截取:")
io.Copy(os.Stdout, rd)
// 截取 off = 30,n = 13
rd = io.NewSectionReader(dataSrc, 30, 13)
fmt.Println("\n\n第二次截取:")
io.Copy(os.Stdout, rd)
strings.NewReader函数会使用提供的字符串对象来创建Reader实例。第一次调用NewSectionReader函数会从偏移量12处开始截取7字节;第二次则从偏移量30处开始截取13字节。
示例运行结果如下:
原数据:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
第一次截取:
MNOPQRS
第二次截取:
efghijklmnopq
【思考】
- 如何将数据写入Buffer对象?
- 如何将数据同时写入多个文件?
1. 如何将数据写入Buffer对象?
在Go语言中,可以使用bytes.Buffer来实现将数据写入缓冲区对象。以下是几种常见的写入方式:
方式一:使用Write方法写入字节切片
package main
import (
"bytes"
"fmt"
)
func main() {
// 初始化一个空的Buffer对象
var buffer bytes.Buffer
data := []byte{1, 2, 3, 4, 5}
// 使用Write方法写入字节切片
n, err := buffer.Write(data)
if err != nil {
fmt.Println("写入错误:", err)
return
}
fmt.Printf("成功写入 %d 个字节\n", n)
// 输出缓冲区中的内容
fmt.Println("缓冲区内容:", buffer.Bytes())
}
方式二:使用WriteString方法写入字符串
package main
import (
"bytes"
"fmt"
)
func main() {
var buffer bytes.Buffer
str := "Hello, World!"
// 使用WriteString方法写入字符串
n, err := buffer.WriteString(str)
if err != nil {
fmt.Println("写入错误:", err)
return
}
fmt.Printf("成功写入 %d 个字节\n", n)
// 输出缓冲区中的内容
fmt.Println("缓冲区内容:", buffer.String())
}
方式三:多次写入操作
package main
import (
"bytes"
"fmt"
)
func main() {
var buffer bytes.Buffer
data1 := []byte{1, 2, 3}
data2 := []byte{4, 5, 6}
// 第一次写入
n1, err := buffer.Write(data1)
if err != nil {
fmt.Println("第一次写入错误:", err)
return
}
// 第二次写入
n2, err := buffer.Write(data2)
if err != nil {
fmt.Println("第二次写入错误:", err)
return
}
fmt.Printf("总共成功写入 %d 个字节\n", n1 + n2)
fmt.Println("缓冲区内容:", buffer.Bytes())
}
2. 如何将数据同时写入多个文件?
可以使用io.MultiWriter来实现将数据同时写入多个文件,示例代码如下:
package main
import (
"io"
"os"
)
func main() {
// 创建三个文件
file1, err := os.Create("file1.txt")
if err != nil {
panic(err)
}
defer file1.Close()
file2, err := os.Create("file2.txt")
if err != nil {
panic(err)
}
defer file2.Close()
file3, err := os.Create("file3.txt")
if err != nil {
panic(err)
}
defer file3.Close()
// 创建MultiWriter对象,将数据同时写入三个文件
writer := io.MultiWriter(file1, file2, file3)
data := []byte("这是要同时写入多个文件的数据\n")
// 写入数据
n, err := writer.Write(data)
if err != nil {
fmt.Println("写入错误:", err)
return
}
fmt.Printf("成功写入 %d 个字节到多个文件\n", n)
}
在上述代码中,首先创建了三个文件,然后通过io.MultiWriter创建了一个可以将数据同时写入这三个文件的写入器,最后将数据写入该写入器,从而实现了将数据同时写入多个文件的功能。

103

被折叠的 条评论
为什么被折叠?



