GO 读写文件
之前在学习GO语言基础时,为了能看懂代码,主要是学了下GO语言的语法。在后面接手一个小项目的需求时,发现需要操作文件,才重新开始学,现在来做一个学习总结。
OS包操作文件
文件打开方式
打开文件的标志主要分为以下几种
- t1:文件访问模式标志,不能同时使用只能指定其中一种
- t2:文件创建标志
- t3:已打开文件的状态标志
标志 | 用途 | 统一UNIX规范版本 | 类型 |
---|---|---|---|
O_RDONLY | 以只读方式打开 | v3 | t1 |
O_WRONLY | 以只写方式打开 | v3 | t1 |
O_RDWR | 以读写方式打开 | v3 | t1 |
O_CLOEXEC | 设置close-on-exec标志 | v4 | t2 |
O_CREAT | 若文件不存在则创建之 | v3 | t2 |
O_DIRECT | 无缓冲的输入/输出 | t2 | |
O_DIRECTORY | 如果pathname不是文件夹,则失败 | v4 | t2 |
O_EXCL | 结合O_CREAT参数使用,专门用于创建文件 | v3 | t2 |
O_LARGEFILE | 在32位系统使用标志打开大文件 | t2 | |
O_NOATIME | 调用read,不修改文件的最近访问时间 | t2 | |
O_NOCTTY | 不让pathname(指向的终端设备)成为控制终端 | v3 | t2 |
O_NOFOLLOW | 对符号链接不予解引用 | v4 | t2 |
O_TRUNC | 截断已有文件,使其长度为零 | v3 | t2 |
O_APPEND | 总在文件尾部追加 | v3 | t3 |
O_ASYNC | 当IO操作可用,产生信号通知进程 | t3 | |
O_DSYNC | 提供同步的IO数据完整性 | v3 | t3 |
O_NONBLOCK | 以非阻塞方式打开 | v3 | t3 |
O_SYNC | 以同步方式写入文件 | v3 | t3 |
打开文件
os.Open
传入文件路径,并以只读的方式打开文件。如果文件不存在,则返回错误:open test: The system cannot find the file specified.
// Open opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode O_RDONLY.
// If there is an error, it will be of type *PathError.
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
os.Create
传入文件路径,如果文件不存在,则使用文件操作权限 0666 创建新文件并以读写方式打开文件;如果文件已存在,则使用读写方式打开文件并截断文件(使原文件长度为零)。
// Create creates or truncates the named file. If the file already exists,
// it is truncated. If the file does not exist, it is created with mode 0666
// (before umask). If successful, methods on the returned File can
// be used for I/O; the associated file descriptor has mode O_RDWR.
// If there is an error, it will be of type *PathError.
func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}
os.OpenFile
传入文件路径、打开方式及文件操作权限,返回文件对象。 os.Open()
及 os.Create()
方法内部都是调用了该方法。其中 openFileNolog()
方法在 windows
和 linux
系统上,其实现方式不同。
// OpenFile is the generalized open call; most users will use Open
// or Create instead. It opens the named file with specified flag
// (O_RDONLY etc.). If the file does not exist, and the O_CREATE flag
// is passed, it is created with mode perm (before umask). If successful,
// methods on the returned File can be used for I/O.
// If there is an error, it will be of type *PathError.
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
testlog.Open(name)
f, err := openFileNolog(name, flag, perm)
if err != nil {
return nil, err
}
f.appendMode = flag&O_APPEND != 0
return f, nil
}
写入数据
通过打开文件返回的文件对象,即可对该文件进行操作。
file.WriteString
传入一个 string 字符串,将该字符串写入到文件中。其内部是将传入的 string 字符串转换为 byte 数组,再调用 Write()
方法进行数据写入。
// WriteString is like Write, but writes the contents of string s rather than
// a slice of bytes.
func (f *File) WriteString(s string) (n int, err error) {
return f.Write([]byte(s))
}
file.Write
将传入的 byte 数组数据写入文件,并返回写入数据的长度。如果写入的长度不等于 byte 数组长度,则返回错误。
// Write writes len(b) bytes to the File.
// It returns the number of bytes written and an error, if any.
// Write returns a non-nil error when n != len(b).
func (f *File) Write(b []byte) (n int, err error) {
if err := f.checkValid("write"); err != nil {
return 0, err
}
n, e := f.write(b)
if n < 0 {
n = 0
}
if n != len(b) {
err = io.ErrShortWrite
}
epipecheck(f, e)
if e != nil {
err = f.wrapErr("write", e)
}
return n, err
}
file.WriteAt
传入待写入的 byte 数组及偏移量,将数据从文件中原数据的第偏移量个位置的地方开始写入,并返回写入数据的长度。与 Write
方法类似,如果写入数据的长度不等于待写入数据的长度,则返回错误;且当打开文件的方式指定了 os.O_APPEND
标志时,写入也会发生错误(因为 APPEND 表示从文件末尾追加,而 WriteAt 表示从文件中覆盖写入)。
// WriteAt writes len(b) bytes to the File starting at byte offset off.
// It returns the number of bytes written and an error, if any.
// WriteAt returns a non-nil error when n != len(b).
//
// If file was opened with the O_APPEND flag, WriteAt returns an error.
func (f *File) WriteAt(b []byte, off int64) (n int, err error) {
if err := f.checkValid("write"); err != nil {
return 0, err
}
if f.appendMode {
return 0, errWriteAtInAppendMode
}
if off < 0 {
return 0, &PathError{"writeat", f.name, errors.New("negative offset")}
}
for len(b) > 0 {
m, e := f.pwrite(b, off)
if e != nil {
err = f.wrapErr("write", e)
break
}
n += m
b = b[m:]
off += int64(m)
}
return
}
- 在打开文件时若未指定特殊打开方式标志(如 os.O_APPEND)时,Write 方法的写入默认是覆盖写,即新数据会覆盖文件中的原有数据。当写入的数据大于文件中原数据时,文件原数据会被全部覆盖,效果类似于使用了标志 os.O_TRUNC ;如果写入的数据小于文件中的原数据,则只会覆盖写入数据长度的原数据,超出写入数据长度之后的原数据将会被保留。例:如果文件中内容为 12345 ,写入数据 123456789 ,则文件内容变为123456789;如果文件内容为 123456789 写入数据 11111 ,此时的写入操作只会覆盖文件中的前 5 个字符,剩下的原数据 6789 将会被保留,文件内容变为 111116789。如果指定打开标志 os.O_APPEND ,则每次数据写入都会是在文件末尾进行追加;如果指定打开标志 os.O_TRUNC ,则每次在打开文件之后,都会将文件中的原数据清空,可保证每次写入文件后,文件原数据不被保留。
- WriteString 方法其内部就是将传入的 string 转换成了 byte 数组,并调用 Write 方法。
- WriteAt 方法与 Write 方法类似,除了传入待写入的 byte 数组数据外,还需指定一个偏移量 offset 。每次在写入数据时,会从文件中原数据的第 offset 个位置开始写(与 Write 方法写一样的覆盖写);也可理解为每次写入一定会保留文件中原数据的前 offset 个数据。
读取数据
file.Read
传入一个 byte 数组,将文件中的内容覆盖写入到该 byte 数组中,并返回读取到的数据长度。如果文件已读取完毕,则会返回错误 EOF 。如果该 byte 数组的长度 len 小于等于文件中的内容长度,则会使用文件内容的前 len 个数据填满该byte数组(超出 byte 数组长度的数据无法读取出来);如果该 byte 数组的长度大于文件中内容的长度 length,则会使用读取到的文件内容覆盖该 byte 数组的前 length 个数据(未被覆盖的数组原数据将会被保留)。
// Read reads up to len(b) bytes from the File.
// It returns the number of bytes read and any error encountered.
// At end of file, Read returns 0, io.EOF.
func (f *File) Read(b []byte) (n int, err error) {
if err := f.checkValid("read"); err != nil {
return 0, err
}
n, e := f.read(b)
return n, f.wrapErr("read", e)
}
file.ReadAt
传入一个 byte 数组以及一个偏移量 off,从文件内容的第 off 个位置开始读取 byte 数组长度 len 个数据,并覆盖写入 byte 数组中,返回读取到的数据长度(当读取到文件末尾时,返回错误 EOF 并将读取到的数据覆盖写入 byte 数组中)。如果传入的偏移量小于文件中的内容长度,则会从文件内容的第偏移量 off 个位置开始读取 len 个长度的数据,并将读取到的数据从数组头部开始覆盖写入(与 Read 方法类似,如果数组中有原数据并未被覆盖到,则会被保留);如果传入的偏移量大于文件中内容的长度(已读取到文件末尾),则会返回错误 EOF (即未读取任何数据)。
// ReadAt reads len(b) bytes from the File starting at byte offset off.
// It returns the number of bytes read and the error, if any.
// ReadAt always returns a non-nil error when n < len(b).
// At end of file, that error is io.EOF.
func (f *File) ReadAt(b []byte, off int64) (n int, err error) {
if err := f.checkValid("read"); err != nil {
return 0, err
}
if off < 0 {
return 0, &PathError{"readat", f.name, errors.New("negative offset")}
}
for len(b) > 0 {
m, e := f.pread(b, off)
if e != nil {
err = f.wrapErr("read", e)
break
}
n += m
b = b[m:]
off += int64(m)
}
return
}
ioutil包读写文件
使用 ioutil 包读写文件,并不需要打开文件。
写文件
ioutil.WriteFile
传入文件路径、待写入数据及文件操作权限,将待写入数据写入文件;如果文件不存在则使用文件操作权限创建新文件并写入数据。其内部是使用 os.OpenFile()
打开文件,并使用文件的 Write()
方法将数据写入文件。
// WriteFile writes data to a file named by filename.
// If the file does not exist, WriteFile creates it with permissions perm
// (before umask); otherwise WriteFile truncates it before writing.
func WriteFile(filename string, data []byte, perm os.FileMode) error {
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
if err != nil {
return err
}
_, err = f.Write(data)
if err1 := f.Close(); err == nil {
err = err1
}
return err
}
读文件
ioutil.ReadFile
传入文件路径,返回对应文件的内容。如果文件不存在,则会返回文件不存在错误。其内部执行流程是通过 os.Open()
打开文件;如果文件大小小于 512 则通过 readAll()
方法读取 512 个字节的内容;如果文件大小大于 512 则通过 readAll()
读取文件大小加上 512 个字节的内容。分配的额外空间是为了防止缓存空间被暂满。
通过该方法读取文件内容,返回的是 byte 数组,直接转换成 string 类型即可得到文件的文字内容。
// ReadFile reads the file named by filename and returns the contents.
// A successful call returns err == nil, not err == EOF. Because ReadFile
// reads the whole file, it does not treat an EOF from Read as an error
// to be reported.
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
// It's a good but not certain bet that FileInfo will tell us exactly how much to
// read, so let's try it but be prepared for the answer to be wrong.
var n int64 = bytes.MinRead
if fi, err := f.Stat(); err == nil {
// As initial capacity for readAll, use Size + a little extra in case Size
// is zero, and to avoid another allocation after Read has filled the
// buffer. The readAll call will read into its allocated internal buffer
// cheaply. If the size was wrong, we'll either waste some space off the end
// or reallocate as needed, but in the overwhelmingly common case we'll get
// it just right.
if size := fi.Size() + bytes.MinRead; size > n {
n = size
}
}
return readAll(f, n)
}
ioutil.ReadAll
传入文件路径,返回对应文件的内容。读取到的内容与 ReadFile 方法一致。
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(r io.Reader) ([]byte, error) {
return readAll(r, bytes.MinRead)
}
从读取结果上看, ReadAll 和 ReadFile 方法效果一样。通过源码可以看出: ReadAll 方法直接调用其内部的 readAll 方法,并传入初始长度 512 ;而 ReadFile 方法则是事先算出文件内容的长度,并在该长度的基础上增加 512 个长度来作为调用 readAll 方法的参数。
// readAll reads from r until an error or EOF and returns the data it read // from the internal buffer allocated with a specified capacity. func readAll(r io.Reader, capacity int64) (b []byte, err error) { var buf bytes.Buffer // If the buffer overflows, we will get bytes.ErrTooLarge. // Return that as an error. Any other panic remains. defer func() { e := recover() if e == nil { return } if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge { err = panicErr } else { panic(e) } }() if int64(int(capacity)) == capacity { buf.Grow(int(capacity)) } _, err = buf.ReadFrom(r) return buf.Bytes(), err }
readAll 方法内部是在调用 bytes.Buffer 的 ReadFrom 方法来进行内容读取。在 ReadFrom 方法中,会对当前 Buffer 根据情况进行扩容。如果当前 Buffer 对象中的 buf 数组不够存放文件内容,则会循环扩容 512 个长度并再次读取,直至读取全部内容(读取到 EOF )。而 ReadFile 方法在事先就进行了 Buffer 的扩容,所以会省去循环扩容的麻烦。当文件内容大于 512 个长度时,ReadFile 的效率相对于 ReadAll 会更高。
// ReadFrom reads data from r until EOF and appends it to the buffer, growing // the buffer as needed. The return value n is the number of bytes read. Any // error except io.EOF encountered during the read is also returned. If the // buffer becomes too large, ReadFrom will panic with ErrTooLarge. func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) { b.lastRead = opInvalid for { i := b.grow(MinRead) b.buf = b.buf[:i] m, e := r.Read(b.buf[i:cap(b.buf)]) if m < 0 { panic(errNegativeRead) } b.buf = b.buf[:i+m] n += int64(m) if e == io.EOF { return n, nil // e is EOF, so return nil explicitly } if e != nil { return n, e } } }
总结
- os包中的 Read 和 ReadAt 方法在读取到文件末尾时,都会返回 io.EOF 错误。需要在读取途中通过判断该错误来辨别是否已读取全部内容。
- os包中的 Read 方法是直接从文件内容的开头位置开始读取; ReadAt 方法通过传入一个偏移量 off ,从文件内容的第 off 个位置开始读取。
- os包中的 Read 和 ReadAt 方法都需要传入一个 byte 数组用来存放读取到的文件内容,如果数组长度 len 小于等于读取到的文件内容,则只会读取文件内容的前 len 个长度;如果数组长度 len 大于读取到的文件内容,则会使用读取到的文件内容覆盖数组的前 len 个长度,未被覆盖的内容将会被保留在数组中。
- ioutil包中的 ReadFile 和 ReadAll 方法在内部进行文件内容读取时,在读取到 io.EOF 时,便停止读取并返回存放在文件内容的 byte 数组。所以 ReadFile 和 ReadAll 方法并不会返回 io.EOF 错误。
- ioutil包中的 ReadFile 和 ReadAll 方法的内部实现都是调用了 readAll 方法,区别在于 ReadFile 会事先算出待读取文件的内容长度,并进行扩容(文件长度 + 512 )。
- os包中的文件写入操作的写入默认是覆盖写,即新数据会覆盖文件中的原有数据。当写入的数据大于文件中原数据时,文件原数据会被全部覆盖;如果写入的数据小于文件中的原数据,则只会覆盖写入数据长度的原数据,超出写入数据长度之后的原数据将会被保留。
- os包中的 WriteString 方法是将传入的 string 数据转换成 byte 数组,然后再调用 Write 方法进行写入。
- ioutil包中的 WriteFile 方法,其内部是使用 os.OpenFile 打开文件,并使用文件的 Write() 方法将数据写入文件。