Go 标准库中涉及 I/O 操作的几个包的区别

Go 标准库中涉及 I/O 操作的几个包的区别

1月 15, 2020
Golang

工作中需要用 Go 读写一些 Excel 表等类型的文件,发现 Go 提供了很多关于 I/O 操作的包,比如 io、ioutil、bufio 等,于是在这里总结一下,主要目标是理清几个包之间在使用时如何选择。

io #

io 库提供了底层的接口定义,还有一些常量,比如 io.EOF。最主要的,它定义了 io.Readerio.Writerio.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 包里的函数,除了 ReadDirReadAll

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.Readdirfile.ReadDir 的区别(注意大小写),看返回值就是 DirEntryFileInfo 的区别。

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 的操作,同时也为很多其他包定义了底层接口,比如前面的 FileInfoDirEntry

有一点需要注意,fs.ReadFileos.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 的 ReaderWriter 实现 io.Readerio.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 来逐行读取和处理。

本文共 1753 字,上次修改于 Dec 6, 2024,以 CC 署名-非商业性使用-禁止演绎 4.0 国际 协议进行许可。