Go 标准库中涉及 I/O 操作的几个包的区别
1月 15, 2020
工作中需要用 Go 读写一些 Excel 表等类型的文件,发现 Go 提供了很多关于 I/O 操作的包,比如 io、ioutil、bufio 等,于是在这里总结一下,主要目标是理清几个包之间在使用时如何选择。
io #
io 库提供了底层的接口定义,还有一些常量,比如 io.EOF
。最主要的,它定义了 io.Reader
、 io.Writer
和 io.Closer
等接口,在 Go 中很多方法都会引用这两些接口作为形参,比如:
- os.File 同时实现了 io.Reader 和 io.Writer
- strings.Reader 实现了 io.Reader
- bufio.Reader/Writer 分别实现了 io.Reader 和 io.Writer
- bytes.Buffer 同时实现了 io.Reader 和 io.Writer
- bytes.Reader 实现了 io.Reader
Reader 接口 #
type Reader interface {
Read(p []byte) (n int, err error)
}
Read
方法指定了读取一个长度为 len(p) 的 []byte
数组,它需要返回一个读取到的长度 n
和 error,其中 (0 <= n <= len(p)),只要实现了这个方法的,就实现了 Reader
接口。
Writer 接口 #
Writer
接口实现了一个 Write
方法,它接受一个 byte
类型的数组,并返回成功写入的长度 n
和遇到异常情况返回的 error,实现了这个 Write 方法,也就实现了 Writer
接口。
type Writer interface {
Write(p []byte) (n int, err error)
}
Closer 接口 #
io 包还有一个 Closer
接口比较重要,很多资源都需要在操作(读写)完毕后进行 close
。
type Closer interface {
Close() error
}
io/ioutil #
ioutil 是 io 包下面的子包,以 io 库里的接口为基础,封装了一些常用的操作,比如读取文件。不过在 go 1.16 以后,这些函数几乎都变成了直接调用 os
包里的函数,除了 ReadDir
和 ReadAll
。
ReadAll
#
// As of Go 1.16, this function simply calls io.ReadAll.
func ReadAll(r io.Reader) ([]byte, error) {
return io.ReadAll(r)
}
ReadFile
#
// As of Go 1.16, this function simply calls os.ReadFile.
func ReadFile(filename string) ([]byte, error) {
return os.ReadFile(filename)
}
WriteFile
#
// As of Go 1.16, this function simply calls os.WriteFile.
func WriteFile(filename string, data []byte, perm fs.FileMode) error {
return os.WriteFile(filename, data, perm)
}
ReadDir #
ioutil.ReadDir
读取目录并返回排好序的文件和子目录信息:
// ReadDir reads the directory named by dirname and returns
// a list of fs.FileInfo for the directory's contents,
// sorted by filename. If an error occurs reading the directory,
// ReadDir returns no directory entries along with the error.
//
// As of Go 1.16, os.ReadDir is a more efficient and correct choice:
// it returns a list of fs.DirEntry instead of fs.FileInfo,
// and it returns partial results in the case of an error
// midway through reading a directory.
func ReadDir(dirname string) ([]fs.FileInfo, error) {
f, err := os.Open(dirname)
if err != nil {
return nil, err
}
list, err := f.Readdir(-1)
f.Close()
if err != nil {
return nil, err
}
sort.Slice(list, func(i, j int) bool { return list[i].Name() < list[j].Name() })
return list, nil
}
ReadDir
没有转发到 os 下面,源码的注释强调了 os.ReadDir
是一个更好的选择,对比了下,两个函数的返回值类型是不一样的,下面是 os.ReadDir
:
// ReadDir reads the named directory,
// returning all its directory entries sorted by filename.
// If an error occurs reading the directory,
// ReadDir returns the entries it was able to read before the error,
// along with the error.
func ReadDir(name string) ([]DirEntry, error) {
f, err := Open(name)
if err != nil {
return nil, err
}
defer f.Close()
dirs, err := f.ReadDir(-1)
sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })
return dirs, err
}
仔细看代码,两者的底层就是 file.Readdir
和 file.ReadDir
的区别(注意大小写),看返回值就是 DirEntry
和 FileInfo
的区别。
type FileInfo interface {
Name() string // base name of the file
Size() int64 // length in bytes for regular files; system-dependent for others
Mode() FileMode // file mode bits
ModTime() time.Time // modification time
IsDir() bool // abbreviation for Mode().IsDir()
Sys() any // underlying data source (can return nil)
}
type DirEntry interface {
Name() string
IsDir() bool
Type() FileMode
Info() (FileInfo, error)
}
io/fs #
io/fs
封装的是针对 file 的操作,同时也为很多其他包定义了底层接口,比如前面的 FileInfo
和 DirEntry
。
有一点需要注意,fs.ReadFile
和 os.ReadFile
的实现不太一样,先看 fs.ReadFile
:
func ReadFile(fsys FS, name string) ([]byte, error)
而 os.ReadFile
为:
func ReadFile(name string) ([]byte, error)
os #
os 包,本身就是和操作系统打交道,其中有相当一部分是文件的 I/O 相关,通过一个 os.File
对象来专门管理。
os 还有一些其他的内容比如和进程管理相关的 Process
,信号相关的 Signal
,以及一些常用的环境变量、系统调用等。
bufio #
bufio 顾名思义和缓存有关,相比 io 包,bufio 的 Reader
和 Writer
实现 io.Reader
和 io.Writer
接口,即同样可以进行文件读写,同时加强了缓存功能。比如使用 io 包读取文件只能一次性全部读取,但是使用 bufio 就可以指定字节的大小,这样可操作性更强,对内存更加友好。
Reader #
// Reader implements buffering for an io.Reader object.
type Reader struct {
buf []byte
rd io.Reader // reader provided by the client
r, w int // buf read and write positions
err error
lastByte int // last byte read for UnreadByte; -1 means invalid
lastRuneSize int // size of last rune read for UnreadRune; -1 means invalid
}
可以通过 bufio.NewReader
初始化自己的 Reader:
// NewReader returns a new Reader whose buffer has the default size.
func NewReader(rd io.Reader) *Reader {
return NewReaderSize(rd, defaultBufSize)
}
func NewReaderSize(rd io.Reader, size int) *Reader {
// Is it already a Reader?
b, ok := rd.(*Reader)
if ok && len(b.buf) >= size {
return b
}
if size < minReadBufferSize {
size = minReadBufferSize
}
r := new(Reader)
r.reset(make([]byte, size), rd)
return r
}
可见,所有实现了 io.Reader
的实例都可以初始化一个 bufio.Reader
。
Writer #
type Writer struct {
err error
buf []byte
n int
wr io.Writer
}
bytes.Buffer #
bytes.Buffer 和 bufio 两者都提供一层缓存功能,它们的不同主要在于 bufio 针对的是文件到内存的缓存,而 bytes.Buffer 的针对的是内存到内存的缓存,所以在使用场景上,不算有交集,但是比较容易混淆。
type Buffer struct {
buf []byte // contents are the bytes buf[off : len(buf)]
off int // read at &buf[off], write at &buf[len(buf)]
lastRead readOp // last read operation, so that Unread* can work correctly.
}
type readOp int8
func NewBuffer(buf []byte) *Buffer { return &Buffer{buf: buf} }
fmt #
还有一个 I/O 相关的包就是 fmt
,但是这个好像不用介绍了,它负责格式化 I/O,使用起来和上面几个包没有交集,不会造成困扰。
总结 #
总结下来,关于文件的操作,如果没有特殊需要(明确知道自己在做什么),一般先去 os 包里找就可以了,如果对内存空间或性能有要求,可以再考虑使用 bufio 来逐行读取和处理。