胡豆秆

个人博客

欢迎来到我的个人博客~


GO 读写文件


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() 方法在 windowslinux 系统上,其实现方式不同。

// 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() 方法将数据写入文件。

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦