Go 语言中,为了方便开发者使用,将 IO 操作封装在了如下几个包中:
- io 为 IO 原语(I/O primitives)提供基本的接口
- io/ioutil 封装一些实用的 I/O 函数
- fmt 实现格式化 I/O,类似 C 语言中的 printf 和 scanf
- bufio 实现带缓冲I/O
1.1 io — 基本的 IO 接口
在 io 包中最重要的是两个接口:Reader 和 Writer 接口。本章所提到的各种 IO 包,都跟这两个接口有关,也就是说,只要实现了这两个接口,它就有了 IO 的功能
Reader 接口
type Reader interface {
Read(p []byte) (n int, err error)
}
官方文档中关于该接口方法的说明:
Read 将 len(p) 个字节读取到 p 中。它返回读取的字节数 n(0 <= n <= len(p)) 以及任何遇到的错误。即使 Read 返回的 n < len(p),它也会在调用过程中使用 p 的全部作为暂存空间。若一些数据可用但不到 len(p) 个字节,Read 会照例返回可用的数据,而不是等待更多数据。当 Read 在成功读取 n > 0 个字节后遇到一个错误或 EOF (end-of-file),它就会返回读取的字节数。它会从相同的调用中返回(非nil的)错误或从随后的调用中返回错误(同时 n == 0)。 一般情况的一个例子就是 Reader 在输入流结束时会返回一个非零的字节数,同时返回的 err 不是 EOF 就是 nil。无论如何,下一个 Read 都应当返回 0, EOF。调用者应当总在考虑到错误 err 前处理 n > 0 的字节。这样做可以在读取一些字节,以及允许的 EOF 行为后正确地处理 I/O 错误。
也就是说,当 Read 方法返回错误时,不代表没有读取到任何数据。调用者应该处理返回的任何数据,之后才处理可能的错误。
下面,我们通过具体例子来谈谈该接口的用法。
func ReadFrom(reader io.Reader, num int) ([]byte, error) {
p := make([]byte, num)
n, err := reader.Read(p)
if n > 0 {
return p[:n], nil
}
return p, err
}
ReadFrom 函数将 io.Reader 作为参数,也就是说,ReadFrom 可以从任意的地方读取数据,只要来源实现了 io.Reader 接口。比如,我们可以从标准输入、文件、字符串等读取数据,示例代码如下:
// 从标准输入读取
data, err = ReadFrom(os.Stdin, 11)
// 从普通文件读取,其中 file 是 os.File 的实例
data, err = ReadFrom(file, 9)
// 从字符串读取
data, err = ReadFrom(strings.NewReader("from string"), 12)
io.EOF 变量的定义:var EOF = errors.New(“EOF”),是 error 类型。根据 reader 接口的说明,在 n > 0 且数据被读完了的情况下,返回的 error 有可能是 EOF 也有可能是 nil。
Writer 接口
type Writer interface {
Write(p []byte) (n int, err error)
}
官方文档中关于该接口方法的说明:
Write 将 len(p) 个字节从 p 中写入到基本数据流中。它返回从 p 中被写入的字节数 n(0 <= n <= len(p))以及任何遇到的引起写入提前停止的错误。若 Write 返回的 n < len(p),它就必须返回一个 非nil 的错误。
在上个例子中,我们是自己实现一个函数接收一个 io.Reader 类型的参数。这里,我们通过标准库的例子来学习。
在fmt标准库中,有一组函数:Fprint/Fprintf/Fprintln,它们接收一个 io.Wrtier 类型参数(第一个参数),也就是说它们将数据格式化输出到 io.Writer 中。那么,调用这组函数时,该如何传递这个参数呢?
我们以 fmt.Fprintln 为例,同时看一下 fmt.Println 函数的源码。
func Println(a ...interface{}) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
实现了 io.Reader 接口或 io.Writer 接口的类型
通过本节上面的例子,我们可以知道,os.File 同时实现了这两个接口。我们还看到 os.Stdin/Stdout 这样的代码,它们似乎分别实现了 io.Reader/io.Writer 接口。没错,实际上在 os 包中有这样的代码:
var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)
也就是说,Stdin/Stdout/Stderr 只是三个特殊的文件(即都是 os.File 的实例),自然也实现了 io.Reader 和 io.Writer。
ReaderAt 和 WriterAt 接口
ReaderAt 接口的定义如下:
type ReaderAt interface {
ReadAt(p []byte, off int64) (n int, err error)
}
官方文档中关于该接口方法的说明:
ReadAt 从基本输入源的偏移量 off 处开始,将 len(p) 个字节读取到 p 中。它返回读取的字节数 n(0 <= n <= len(p))以及任何遇到的错误。当 ReadAt 返回的 n < len(p) 时,它就会返回一个 非nil 的错误来解释 为什么没有返回更多的字节。在这一点上,ReadAt 比 Read 更严格。即使 ReadAt 返回的 n < len(p),它也会在调用过程中使用 p 的全部作为暂存空间。若一些数据可用但不到 len(p) 字节,ReadAt 就会阻塞直到所有数据都可用或产生一个错误。 在这一点上 ReadAt 不同于 Read。若 n = len(p) 个字节在输入源的的结尾处由 ReadAt 返回,那么这时 err == EOF 或者 err == nil。若 ReadAt 按查找偏移量从输入源读取,ReadAt 应当既不影响基本查找偏移量也不被它所影响。ReadAt 的客户端可对相同的输入源并行执行 ReadAt 调用。
可见,ReaderAt 接口使得可以从指定偏移量处开始读取数据。
简单示例代码如下:
reader := strings.NewReader("Go语言学习园地")
p := make([]byte, 6)
n, err := reader.ReadAt(p, 2)
if err != nil {
panic(err)
}
fmt.Printf("%s, %d\n", p, n)
输出:
//tips: a chinese character is 3 bytes
语言, 6
WriterAt 接口的定义如下:
type WriterAt interface {
WriteAt(p []byte, off int64) (n int, err error)
}
官方文档中关于该接口方法的说明:
WriteAt 从 p 中将 len(p) 个字节写入到偏移量 off 处的基本数据流中。它返回从 p 中被写入的字节数 n(0 <= n <= len(p))以及任何遇到的引起写入提前停止的错误。若 WriteAt 返回的 n < len(p),它就必须返回一个 非nil 的错误。若 WriteAt 按查找偏移量写入到目标中,WriteAt 应当既不影响基本查找偏移量也不被它所影响。若区域没有重叠,WriteAt 的客户端可对相同的目标并行执行 WriteAt 调用。
我们可以通过该接口将数据写入数据流的特定偏移量之后。
通过简单示例来演示 WriteAt 方法的使用(os.File 实现了 WriterAt 接口):
file, err := os.Create("writeAt.txt")
if err != nil {
panic(err)
}
defer file.Close()
file.WriteString("Golang中文社区——这里是多余的")
n, err := file.WriteAt([]byte("Go语言学习园地"), 24)
if err != nil {
panic(err)
}
fmt.Println(n)
打开文件 WriteAt.txt,内容是:Golang中文社区——Go语言学习园地。
分析:
file.WriteString("Golang中文社区——这里是多余的") 往文件中写入 Golang中文社区——这里是多余的,之后 file.WriteAt([]byte("Go语言学习园地"), 24) 在文件流的 offset=24 处写入 Go语言学习园地(会覆盖该位置的内容)。
ReaderFrom 和 WriterTo 接口
ReaderFrom 的定义如下:
type ReaderFrom interface {
ReadFrom(r Reader) (n int64, err error)
}
官方文档中关于该接口方法的说明:
ReadFrom 从 r 中读取数据,直到 EOF 或发生错误。其返回值 n 为读取的字节数。除 io.EOF 之外,在读取过程中遇到的任何错误也将被返回。如果 ReaderFrom 可用,Copy 函数就会使用它。
注意:ReadFrom 方法不会返回 err == EOF。
下面的例子简单的实现将文件中的数据全部读取(显示在标准输出):
file, err := os.Open("writeAt.txt")
if err != nil {
panic(err)
}
defer file.Close()
writer := bufio.NewWriter(os.Stdout)
writer.ReadFrom(file)
writer.Flush()
当然,我们可以通过 ioutil 包的 ReadFile 函数获取文件全部内容。其实,跟踪一下 ioutil.ReadFile 的源码,会发现其实也是通过 ReadFrom 方法实现(用的是 bytes.Buffer,它实现了 ReaderFrom 接口)。
如果不通过 ReadFrom 接口来做这件事,而是使用 io.Reader 接口,我们有两种思路:
stat, err := os.Stat("a.txt")
if err != nil {
panic(err)
}
p := make([]byte,stat.Size())
file, _ := os.Open("a.txt")
file.Read(p)
log.Println(string(p))
通过查看 bufio.Writer 或 strings.Buffer 类型的 ReadFrom 方法实现,会发现,其实它们的实现和上面说的第 2 种思路类似。
以后补上
WriterTo的定义如下:
type WriterTo interface {
WriteTo(w Writer) (n int64, err error)
}
官方文档中关于该接口方法的说明:
WriteTo 将数据写入 w 中,直到没有数据可写或发生错误。其返回值 n 为写入的字节数。 在写入过程中遇到的任何错误也将被返回。如果 WriterTo 可用,Copy 函数就会使用它。
读者是否发现,其实 ReaderFrom 和 WriterTo 接口的方法接收的参数是 io.Reader 和 io.Writer 类型。根据 io.Reader 和 io.Writer 接口的讲解,对该接口的使用应该可以很好的掌握。
这里只提供简单的一个示例代码:将一段文本输出到标准输出:
reader := bytes.NewReader([]byte("Go语言学习园地"))
reader.WriteTo(os.Stdout)
通过 io.ReaderFrom 和 io.WriterTo 的学习,我们知道,如果这样的需求,可以考虑使用这两个接口:“一次性从某个地方读或写到某个地方去。”
Seeker 接口
接口定义如下:
type Seeker interface {
Seek(offset int64, whence int) (ret int64, err error)
}
官方文档中关于该接口方法的说明:
Seek 设置下一次 Read 或 Write 的偏移量为 offset,它的解释取决于 whence: 0 表示相对于文件的起始处,1 表示相对于当前的偏移,而 2 表示相对于其结尾处。 Seek 返回新的偏移量和一个错误,如果有的话。
也就是说,Seek 方法用于设置偏移量的,这样可以从某个特定位置开始操作数据流。听起来和 ReaderAt/WriteAt 接口有些类似,不过 Seeker 接口更灵活,可以更好的控制读写数据流的位置。
简单的示例代码:获取倒数第二个字符(需要考虑 UTF-8 编码,这里的代码只是一个示例)
reader := strings.NewReader("Go语言学习园地")
reader.Seek(-6, os.SEEK_END)
r, _, _ := reader.ReadRune()
fmt.Printf("%c\n", r)
whence 的值,在 os 包中定义了相应的常量,应该使用这些常量:
const (
SEEK_SET int = 0 // seek relative to the origin of the file
SEEK_CUR int = 1 // seek relative to the current offset
SEEK_END int = 2 // seek relative to the end
)
Closer接口
接口定义如下:
type Closer interface {
Close() error
}
该接口比较简单,只有一个 Close() 方法,用于关闭数据流。
文件 (os.File)、归档(压缩包)、数据库连接、Socket 等需要手动关闭的资源都实现了 Closer 接口。
实际编程中,经常将 Close 方法的调用放在 defer 语句中。
SectionReader 类型
SectionReader 是一个 struct(没有任何导出的字段),实现了 Read, Seek 和 ReadAt,同时,内嵌了 ReaderAt 接口。结构定义如下:
type SectionReader struct {
r ReaderAt // 该类型最终的 Read/ReadAt 最终都是通过 r 的 ReadAt 实现
base int64 // NewSectionReader 会将 base 设置为 off
off int64 // 从 r 中的 off 偏移处开始读取数据
limit int64 // limit - off = SectionReader 流的长度
}
从名称我们可以猜到,该类型读取数据流中部分数据。看一下
func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader
NewSectionReader 返回一个 SectionReader,它从 r 中的偏移量 off 处读取 n 个字节后以 EOF 停止。
也就是说,SectionReader 只是内部(内嵌)ReaderAt 表示的数据流的一部分:从 off 开始后的 n 个字节。
这个类型的作用是:方便重复操作某一段 (section) 数据流;或者同时需要 ReadAt 和 Seek 的功能。