Go语言入门:输入输出

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

【思考】

  1. 如何将数据写入Buffer对象?
  2. 如何将数据同时写入多个文件?

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创建了一个可以将数据同时写入这三个文件的写入器,最后将数据写入该写入器,从而实现了将数据同时写入多个文件的功能。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值