Skip to main content

Std io

Go 语言的 io 标准库提供了一组基本的接口和函数,用于 I/O 操作。以下是 io 包的一些主要 API 及其释义和代码示例。

函数

func Copy(dst Writer, src Reader) (written int64, err error)

src 中的数据复制到 dst,直到 src 到达 EOF 或发生错误。返回复制的字节数和遇到的第一个错误(如果有)。

package main

import (
"fmt"
"io"
"strings"
)

func main() {
src := strings.NewReader("Hello, World!")
dst := &strings.Builder{}
written, err := io.Copy(dst, src)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Copied %d bytes: %s\n", written, dst.String())
}

func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error)

类似 Copy,但使用提供的缓冲区 buf。如果 buf 为 nil,则行为与 Copy 相同。

package main

import (
"fmt"
"io"
"strings"
)

func main() {
src := strings.NewReader("Hello, World!")
dst := &strings.Builder{}
buf := make([]byte, 8) // 自定义缓冲区
written, err := io.CopyBuffer(dst, src, buf)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Copied %d bytes: %s\n", written, dst.String())
}

func CopyN(dst Writer, src Reader, n int64) (written int64, err error)

src 复制 n 字节到 dst。如果 src 在复制 n 字节之前到达 EOF,则返回 EOF

package main

import (
"fmt"
"io"
"strings"
)

func main() {
src := strings.NewReader("Hello, World!")
dst := &strings.Builder{}
written, err := io.CopyN(dst, src, 5)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Copied %d bytes: %s\n", written, dst.String())
}

func Pipe() (*PipeReader, *PipeWriter)

创建一个内存管道,返回一对 PipeReaderPipeWriter

package main

import (
"fmt"
"io"
"strings"
)

func main() {
r, w := io.Pipe()

go func() {
defer w.Close()
w.Write([]byte("Hello, World!"))
}()

buf := make([]byte, 13)
n, err := r.Read(buf)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Read %d bytes: %s\n", n, string(buf))
}

func ReadAll(r Reader) ([]byte, error)

读取 r 直到 EOF 并返回读取的数据。

package main

import (
"fmt"
"io"
"strings"
)

func main() {
r := strings.NewReader("Hello, World!")
data, err := io.ReadAll(r)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Read data: %s\n", string(data))
}

func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)

r 读取数据到 buf 中,直到至少读取 min 字节。返回读取的字节数和遇到的第一个错误(如果有)。

package main

import (
"fmt"
"io"
"strings"
)

func main() {
r := strings.NewReader("Hello, World!")
buf := make([]byte, 8)
n, err := io.ReadAtLeast(r, buf, 5)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Read %d bytes: %s\n", n, string(buf[:n]))
}

func ReadFull(r Reader, buf []byte) (n int, err error)

r 读取 len(buf) 字节到 buf 中。返回读取的字节数和遇到的第一个错误(如果有)。

package main

import (
"fmt"
"io"
"strings"
)

func main() {
r := strings.NewReader("Hello, World!")
buf := make([]byte, 5)
n, err := io.ReadFull(r, buf)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Read %d bytes: %s\n", n, string(buf))
}

func WriteString(w Writer, s string) (n int, err error)

将字符串 s 写入 w

package main

import (
"fmt"
"io"
"strings"
)

func main() {
var sb strings.Builder
n, err := io.WriteString(&sb, "Hello, World!")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Wrote %d bytes: %s\n", n, sb.String())
}

类型

type ByteReader

一个接口,表示可以读取单个字节的 Reader。

type ByteReader interface {
ReadByte() (byte, error)
}

type ByteScanner

一个接口,表示可以读取和撤销单个字节的 Reader。

type ByteScanner interface {
ByteReader
UnreadByte() error
}

type ByteWriter

一个接口,表示可以写入单个字节的 Writer。

type ByteWriter interface {
WriteByte(c byte) error
}

type Closer

一个接口,表示可以关闭的对象。

type Closer interface {
Close() error
}

type LimitedReader

一个结构体,限制 Reader 读取的最大字节数。

type LimitedReader struct {
R Reader // underlying reader
N int64 // max bytes remaining
}

func (l *LimitedReader) Read(p []byte) (n int, err error)

type OffsetWriter

一个结构体,表示从特定偏移量开始写入的 Writer。

type OffsetWriter struct {
// 内部字段
}

func NewOffsetWriter(w WriterAt, off int64) *OffsetWriter
func (o *OffsetWriter) Seek(offset int64, whence int) (int64, error)
func (o *OffsetWriter) Write(p []byte) (n int, err error)
func (o *OffsetWriter) WriteAt(p []byte, off int64) (n int, err error)

type PipeReader

管道的读取端。

type PipeReader struct {
// 内部字段
}

func (r *PipeReader) Close() error
func (r *PipeReader) CloseWithError(err error) error
func (r *PipeReader) Read(data []byte) (n int, err error)

type PipeWriter

管道的写入端。

type PipeWriter struct {
// 内部字段
}

func (w *PipeWriter) Close() error
func (w *PipeWriter) CloseWithError(err error) error
func (w *PipeWriter) Write(data []byte) (n int, err error)

type ReadCloser

一个接口,组合了 Reader 和 Closer。

type ReadCloser interface {
Reader
Closer
}

func NopCloser(r Reader) ReadCloser

type ReadSeekCloser

一个接口,组合了 Reader、Seeker 和 Closer。

type ReadSeekCloser interface {
Reader
Seeker
Closer
}

type ReadSeeker

一个接口,组合了 Reader 和 Seeker。

type ReadSeeker interface {
Reader
Seeker
}

type ReadWriteCloser

一个接口,组合了 Reader、Writer 和 Closer。

type ReadWriteCloser interface {
Reader
Writer
Closer
}

type ReadWriteSeeker

一个接口,组合了 Reader、Writer 和 Seeker。

type ReadWriteSeeker interface {
Reader
Writer
Seeker
}

type ReadWriter

一个接口,组合了 Reader 和 Writer。

type ReadWriter interface {
Reader
Writer
}

type Reader

一个接口,表示可以读取数据的对象。

type Reader interface {
Read(p []byte) (n int, err error)
}

func LimitReader(r Reader, n int64) Reader
func MultiReader(readers ...Reader) Reader
func TeeReader(r Reader, w Writer) Reader

type ReaderAt

一个接口,表示可以从特定偏移量读取数据的对象。

type ReaderAt interface {
ReadAt(p []byte, off int64) (n int, err error)
}

type ReaderFrom

一个接口,表示可以从 Reader 中读取数据的对象。

type ReaderFrom interface {
ReadFrom(r Reader) (n int64, err error)
}

type RuneReader

一个接口,表示可以读取单个 Unicode 码点的 Reader。

type RuneReader interface {
ReadRune() (r rune, size int, err error)
}

type RuneScanner

一个接口,表示可以读取和撤销单个 Unicode 码点的 Reader。

type RuneScanner interface {
RuneReader
UnreadRune() error
}

type SectionReader

一个结构体,表示从特定偏移量开始读取的 Reader。

type SectionReader struct {
// 内部字段
}

func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader
func (s *SectionReader) Outer() (r ReaderAt, off int64, n int64)
func (s *SectionReader) Read(p []byte) (n int, err error)
func (s *SectionReader) ReadAt(p []byte, off int64) (n int, err error)
func (s *SectionReader) Seek(offset int64, whence int) (int64, error)
func (s *SectionReader) Size() int64

type Seeker

一个接口,表示可以设置读取位置的对象。

type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}

type StringWriter

一个接口,表示可以写入字符串的 Writer。

type StringWriter interface {
WriteString(s string) (n int, err error)
}

type WriteCloser

一个接口,组合了 Writer 和 Closer。

type WriteCloser interface {
Writer
Closer
}

type WriteSeeker

一个接口,组合了 Writer 和 Seeker。

type WriteSeeker interface {
Writer
Seeker
}

type Writer

一个接口,表示可以写入数据的对象。

type Writer interface {
Write(p []byte) (n int, err error)
}

func MultiWriter(writers ...Writer) Writer

type WriterAt

一个接口,表示可以从特定偏移量写入数据的对象。

type WriterAt interface {
WriteAt(p []byte, off int64) (n int, err error)
}

type WriterTo

一个接口,表示可以将数据写入 Writer 的对象。

type WriterTo interface {
WriteTo(w Writer) (n int64, err error)
}

这些是 io 包的主要 API 和类型。通过这些接口和函数,开发者可以方便地进行各种 I/O 操作。

我理解 buf 缓冲区,每满一次,执行一次,为什么上述代码没看到循环?

在 Go 语言的 io 包中,像 io.Copy, io.CopyBuffer, io.CopyN 这样的函数内部确实使用了缓冲区,并在缓冲区填满或读取完成之前会多次调用读取和写入操作。虽然在示例代码中没有显式的循环,但这些函数内部实现了循环逻辑。

以下是 io.Copy 函数的简化版本,展示了内部是如何使用循环的:

package main

import (
"bytes"
"fmt"
"io"
)

func main() {
src := bytes.NewReader([]byte("Hello, World!"))
dst := &bytes.Buffer{}
written, err := Copy(dst, src)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Copied %d bytes: %s\n", written, dst.String())
}

// Copy is a simplified version of io.Copy
func Copy(dst io.Writer, src io.Reader) (written int64, err error) {
buf := make([]byte, 32*1024) // 32KB 缓冲区
for {
nr, er := src.Read(buf)
if nr > 0 {
nw, ew := dst.Write(buf[0:nr])
if nw > 0 {
written += int64(nw)
}
if ew != nil {
err = ew
break
}
if nr != nw {
err = io.ErrShortWrite
break
}
}
if er != nil {
if er != io.EOF {
err = er
}
break
}
}
return written, err
}

在这个简化版本中,你可以看到 Copy 函数中有一个 for 循环:

  1. 每次循环中,调用 src.Read(buf) 尝试从 src 读取数据到缓冲区 buf
  2. 如果读取到的数据长度 nr 大于 0,则将这些数据写入 dst
  3. 如果写入过程中发生错误或读取到的字节数与写入的字节数不一致,则退出循环并返回错误。
  4. 如果读取过程中遇到 io.EOF,则表示读取完毕,退出循环。

因此,虽然在使用这些函数时没有显式的循环,但它们内部确实是通过循环来实现数据的读取和写入的。这样可以简化用户代码,使用户不必自己处理循环逻辑。

example

package main

import (
"fmt"
"io"
"strings"
)

func main() {
// 创建一个 strings.Reader
r := strings.NewReader("Hello, World!")
fmt.Println(r) // &{Hello, World! 0 -1}


// 创建一个缓冲区
buf := make([]byte, 5)

// 从 strings.Reader 中读取数据
n, err := r.Read(buf)
if err != nil && err != io.EOF {
fmt.Println("Error:", err)
return
}

fmt.Printf("Read %d bytes: %s\n", n, string(buf[:n]))

// 使用 Seek 方法将读取位置移动到开头
r.Seek(0, io.SeekStart)

// 再次读取数据
n, err = r.Read(buf)
if err != nil && err != io.EOF {
fmt.Println("Error:", err)
return
}

fmt.Printf("Read %d bytes: %s\n", n, string(buf[:n]))
}

&{Hello, World! 0 -1}

在 Go 语言中,strings.Reader 是一个结构体类型,它的定义如下:

type Reader struct {
s string
i int64 // current reading index
prevRune int // index of previous rune; or < 0
}

当你使用 strings.NewReader 创建一个新的 strings.Reader 时,它会初始化上述字段。让我们详细解释这些字段的含义:

  1. s: 这是传递给 NewReader 的字符串。在你的例子中,它的值是 "Hello, World!"

  2. i: 这是当前读取的索引,表示下一个要读取的字节位置。初始化时,它的值是 0,表示从字符串的开头开始读取。

  3. prevRune: 这是前一个读取的 Unicode 码点的索引。如果没有读取任何 Unicode 码点或上一次读取操作不是读取 Unicode 码点(如 Read 方法),它的值是 -1。初始化时,它的值是 -1

所以,当你打印 strings.Reader 的实例时,输出 &{Hello, World! 0 -1} 表示:

  • s 字段是 "Hello, World!"
  • i 字段是 0,表示当前读取位置在字符串的开头。
  • prevRune 字段是 -1,表示还没有读取任何 Unicode 码点。

示例代码

以下代码演示了如何读取数据并查看这些字段的变化:

package main

import (
"fmt"
"strings"
)

func main() {
r := strings.NewReader("Hello, World!")
fmt.Println(r) // &{Hello, World! 0 -1}

buf := make([]byte, 5)
r.Read(buf)
fmt.Println(r) // &{Hello, World! 5 -1}

r.Read(buf)
fmt.Println(r) // &{Hello, World! 10 -1}

r.ReadByte()
fmt.Println(r) // &{Hello, World! 11 -1}

r.ReadRune()
fmt.Println(r) // &{Hello, World! 12 11}
}

解释

  1. 初始状态:&{Hello, World! 0 -1}
  2. 第一次读取 5 个字节后:&{Hello, World! 5 -1}i 增加到 5。
  3. 第二次读取 5 个字节后:&{Hello, World! 10 -1}i 增加到 10。
  4. 使用 ReadByte 读取一个字节后:&{Hello, World! 11 -1}i 增加到 11。
  5. 使用 ReadRune 读取一个 Unicode 码点后:&{Hello, World! 12 11}i 增加到 12,prevRune 更新为 11。

通过这些操作,你可以看到 strings.Reader 的内部状态是如何随着读取操作而变化的。

读数据到缓冲区

  1. 每一都是从缓冲区头到缓冲区结束去覆盖,最后一次覆盖到 n 位置,n 之后的内容为倒数第二次未被覆盖的内容,这部分内容是无效的

在 Go 中使用 io.Reader 接口的 Read 方法时,缓冲区(buf)的管理是由调用者负责的。Read 方法不会自动清理缓冲区或重新分配缓冲区。每次调用 Read 方法时,数据会被写入到提供的缓冲区中,并覆盖缓冲区中的现有数据。因此,调用者需要确保在每次读取之前正确管理缓冲区的内容。

示例代码

以下代码演示了如何在缓冲区读满后继续读取,并手动管理缓冲区内容:

package main

import (
"fmt"
"io"
"strings"
)

func main() {
// 创建一个 strings.Reader
r := strings.NewReader("Hello, World!")

// 创建一个缓冲区,长度为 5
buf := make([]byte, 5)

for {
// 从 strings.Reader 中读取数据到缓冲区 buf 中
n, err := r.Read(buf)
if err != nil && err != io.EOF {
fmt.Println("Error:", err)
return
}

// 输出读取的字节数和缓冲区内容
fmt.Printf("Read %d bytes: %s\n", n, string(buf[:n]))

// 如果读取到达输入的末尾,退出循环
if err == io.EOF {
break
}
}
}

解释

  1. 创建 strings.Reader: r := strings.NewReader("Hello, World!") 创建一个新的 strings.Reader,包含字符串 "Hello, World!"
  2. 创建缓冲区: buf := make([]byte, 5) 创建一个长度为 5 的字节切片 buf
  3. 循环读取数据:
    • n, err := r.Read(buf)strings.Reader 中读取数据,并将其填充到缓冲区 buf 中。
    • n 是实际读取的字节数。
    • err 表示读取过程中是否发生了错误。
  4. 输出读取结果: fmt.Printf("Read %d bytes: %s\n", n, string(buf[:n])) 打印读取的字节数和缓冲区的内容。
  5. 检查是否到达输入末尾: 如果 err == io.EOF,表示读取到达了输入的末尾,退出循环。

运行结果

运行上述代码,输出结果如下:

Read 5 bytes: Hello
Read 5 bytes: , Wor
Read 3 bytes: ld!

可以看到,每次 Read 方法会尽可能多地读取数据填充缓冲区,直到缓冲区满或没有更多数据可读取。

注意事项

  • 缓冲区管理: 在每次调用 Read 方法时,缓冲区中的数据会被新读取的数据覆盖。因此,调用者需要在每次读取之后及时处理缓冲区中的数据。
  • 缓冲区大小: 缓冲区的大小决定了每次读取的最大数据量。如果缓冲区太小,可能需要多次读取才能获取所有数据;如果缓冲区太大,可能会浪费内存。
  • 错误处理: 在读取过程中,需要正确处理可能发生的错误,例如 io.EOF 表示读取到达了输入的末尾。

通过这种方式,你可以灵活地管理缓冲区的内容,并逐步读取大块数据。每次读取操作都会覆盖缓冲区中的数据,因此你需要在读取之后及时处理这些数据。

缓冲区大小选择

选择合适的缓冲区大小是一个权衡性能和资源使用的过程。以下是一些建议和考虑因素,帮助你确定合适的缓冲区大小:

考虑因素

  1. 数据源特性:

    • 数据块大小: 如果你知道数据源的数据块大小(例如,网络包的大小、文件块的大小),可以根据这些信息来选择缓冲区大小。
    • 数据流特性: 如果数据源是一个连续的数据流(例如,视频流、音频流),可以选择较大的缓冲区以减少读取频率。
  2. 系统资源:

    • 内存使用: 如果系统内存有限,选择过大的缓冲区可能会导致内存不足问题。
    • CPU 负载: 较小的缓冲区会导致频繁的读取操作,从而增加 CPU 负载。
  3. 性能需求:

    • 吞吐量: 较大的缓冲区可以提高吞吐量,因为减少了读取操作的频率。
    • 响应时间: 较小的缓冲区可以提高响应时间,因为读取操作更频繁,数据处理更及时。

常见缓冲区大小

  • 小缓冲区: 512 字节到 4KB,适用于资源受限的环境或需要快速响应的数据流。
  • 中等缓冲区: 4KB 到 64KB,适用于大多数常见的应用场景,平衡性能和内存使用。
  • 大缓冲区: 64KB 到 1MB 或更大,适用于高吞吐量需求的场景,例如大文件传输或高带宽网络通信。

示例代码

以下代码展示了如何选择和调整缓冲区大小:

package main

import (
"fmt"
"io"
"strings"
)

func main() {
// 创建一个 strings.Reader
r := strings.NewReader("Hello, World! This is a test string to demonstrate buffer size.")

// 创建一个缓冲区,长度为 16
bufSize := 16
buf := make([]byte, bufSize)

for {
// 从 strings.Reader 中读取数据到缓冲区 buf 中
n, err := r.Read(buf)
if err != nil && err != io.EOF {
fmt.Println("Error:", err)
return
}

// 只处理缓冲区的前 n 个字节
fmt.Printf("Read %d bytes: %s\n", n, string(buf[:n]))

// 如果读取到达输入的末尾,退出循环
if err == io.EOF {
break
}
}
}

运行结果

运行上述代码,输出结果如下(假设缓冲区大小为 16 字节):

Read 16 bytes: Hello, World! Th
Read 16 bytes: is is a test st
Read 16 bytes: ring to demons
Read 16 bytes: trate buffer s
Read 16 bytes: ize.
Read 0 bytes:

调整缓冲区大小

你可以根据实际需求调整 bufSize 的值,观察对性能和内存使用的影响。例如,将 bufSize 调整为 32 或 64 字节,看看是否有更好的性能或内存使用情况。

总结

  • 测试和优化: 最好在实际应用中测试不同的缓冲区大小,观察其对性能和内存使用的影响,然后选择最合适的缓冲区大小。
  • 动态调整: 根据实际运行情况,动态调整缓冲区大小也是一种策略。例如,开始时使用较小的缓冲区,观察系统资源使用情况,然后逐步增加缓冲区大小,直到找到最佳平衡点。

通过合理选择缓冲区大小,你可以在性能和资源使用之间找到最佳平衡点,确保应用程序高效运行。