前言

文件读写一直是我在学习一门语言的时候比较难以记忆和弄懂的部分,每次当我使用比如 golang 去读取/写入一份文件的时候,总会在浏览器中不停的google: "how to read and write file via golang". 当隔一段时间再要实现上述功能的时候,我还是会去浏览器搜索相同的关键字,这样实际上很没有效率,因此借着这篇博客,我将解析bufio有关文件读写方面的源代码实现及其常用的方法。

Part 0: io库简要分析,以及bufio究竟做了什么

bufio做了什么?官方文档中这样描述:

Package bufio implements buffered I/O. It wraps an io.Reader or io.Writer object, creating another object (Reader or Writer) that also implements the interface but provides buffering and some help for textual I/O.

翻译过来的大致含义是:bufio是对IO操作的缓存的实现,它封装了 io.Reader 和 io.Writer 为新的对象,也就是 Reader 和 Writer;同时它也实现了相应的对于缓存的操作。学过《操作系统》这门课的同学应该对于 buffer 不陌生,buffer与操作系统的IO操作结合可以很好的提高文件操作的效率,因为这样避免了频繁的IO操作。同时缓冲区可以同时接受多次写入/读取,再写入文件系统,提高了我们程序的效率。
io库中,Reader和Writer实际上是两种interface, interface的定义如下:

// Reader 
// Reader 封装了基本的 Read 方法。Read 方法将 Reader 中的最多 len(p) 个byte读入p中
type Reader interface {
    Read(p [][byte](https://golang.org/pkg/builtin/#byte)) (n [int](https://golang.org/pkg/builtin/#int), err [error](https://golang.org/pkg/builtin/#error))
}

// Writer
// Write方法将 len(p) 个byte从p写入 writer中的底层数据流
type Writer interface {
    Write(p [][byte](https://golang.org/pkg/builtin/#byte)) (n [int](https://golang.org/pkg/builtin/#int), err [error](https://golang.org/pkg/builtin/#error))
}

Part 1: 如何通过bufio.Scanner读文件

为了避免写成一篇官方文档的中文翻译博客,我在这里从bufio的使用角度出发,来逐步深入bufio的实现。

1. 读文件

我们先从按行读取出发,实现如下:

func ReadByLine(filePath string) {        // filePath为文件的路径
 file, err := os.Open(filePath)       // 首先打开文件,file的类型为 *os.File
 if err != nil {
      log.Fatal(err)
   }
   defer func() {
      if err := file.Close(); err != nil {
         log.Fatal(err)
      }
   }()
   fmt.Println(file.Name())               // 打印文件名
 fileScanner := bufio.NewScanner(file)     // 申请一个fileScanner, 类型为 *bufio.Scanner
 for fileScanner.Scan() {
      fmt.Println(fileScanner.Text())          // 按行读取,打印每行的内容
 }
}
  • 测试:

    • 要读取的文件中的内容:

截屏2020-10-04 下午4.58.16.png

- 程序运行结果:

截屏2020-10-04 下午4.59.02.png

  • 源码分析

既然bufio是对于io库中Reader和Writer的封装,首先我们看一下bufio中的Reader定义,如下:

type Reader struct {
      buf          []byte
      rd           io.Reader // 由使用者传入的reader
      r, w         int       // 缓冲区读写的位置
      err          error
      lastByte     int // last byte read for UnreadByte; -1 means invalid
      lastRuneSize int // size of last rune read for UnreadRune; -1 means invalid
  }

而在按行读取文件例子中,我们并没有直接使用 Reader,而是使用了 Scanner, bufio中 Scanner 的定义如下:

type Scanner struct {
      r            io.Reader // The reader provided by the client.
      split        SplitFunc // The function to split the tokens.
      maxTokenSize int       // Maximum size of a token; modified by tests.
      token        []byte    // Last token returned by split.
      buf          []byte    // Buffer used as argument to split.
      start        int       // First non-processed byte in buf.
      end          int       // End of data in buf.
      err          error     // Sticky error.
      empties      int       // Count of successive empty tokens.
      scanCalled   bool      // Scan has been called; buffer is in use.
      done         bool      // Scan has finished.
  }
SplitFunc
// SplitFunc 
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)

SplitFunc 要求根据给定data求解是否还有token, 判断的规则由不同的SplitFunc类型函数给出;advance给出了从s.start位置读取token后前进的字节数。当按行读取的时候,Scanner中的default split函数是 ScanLines 函数,ScanLines 函数的定义如下:

func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
    // dropCR 的作用是去掉byte末尾的\r
    //如果当前在EOF,并且data的长度为0,说明已经没有token
      if atEOF && len(data) == 0 {
          return 0, nil, nil
      }
    // 找到一个终结符,也就是换行符,说明当前data存在一个完整的token, 返回
      if i := bytes.IndexByte(data, '\n'); i >= 0 {
          // We have a full newline-terminated line.
          return i + 1, dropCR(data[0:i]), nil
      }
      // 处于eof,data长度不为0,返回未被终结符终结的token
      if atEOF {
          return len(data), dropCR(data), nil
      }
     // Request more data.
     return 0, nil, nil
}

基于ScanLines以及Scanner中的其他数据成员,我们终于可以分析代码中用到的Scan函数了,Scan函数的实现如下:

func (s *Scanner) Scan() bool {
   if s.done {
      return false
 }
   s.scanCalled = true
 // Loop until we have a token.
 for {
      // See if we can get a token with what we already have.
 // If we've run out of data but have an error, give the split function // a chance to recover any remaining, possibly empty token. if s.end > s.start || s.err != nil {
         advance, token, err := s.split(s.buf[s.start:s.end], s.err != nil)
         if err != nil {
            if err == ErrFinalToken {
               s.token = token
               s.done = true
 return true }
            s.setErr(err)
            return false
 }
         if !s.advance(advance) {
            return false
 }
         s.token = token
         if token != nil {
            if s.err == nil || advance > 0 {
               s.empties = 0
 } else {
               // Returning tokens not advancing input at EOF.
 s.empties++
               if s.empties > maxConsecutiveEmptyReads {
                  panic("bufio.Scan: too many empty tokens without progressing")
               }
            }
            return true
 }
      }
      // We cannot generate a token with what we are holding.
 // If we've already hit EOF or an I/O error, we are done. if s.err != nil {
         // Shut it down.
 s.start = 0
 s.end = 0
 return false
 }
      // Must read more data.
 // First, shift data to beginning of buffer if there's lots of empty space // or space is needed. if s.start > 0 && (s.end == len(s.buf) || s.start > len(s.buf)/2) {
         copy(s.buf, s.buf[s.start:s.end])
         s.end -= s.start
         s.start = 0
 }
      // Is the buffer full? If so, resize.
 if s.end == len(s.buf) {
         // Guarantee no overflow in the multiplication below.
 const maxInt = int(^uint(0) >> 1)
         if len(s.buf) >= s.maxTokenSize || len(s.buf) > maxInt/2 {
            s.setErr(ErrTooLong)
            return false
 }
         newSize := len(s.buf) * 2
 if newSize == 0 {
            newSize = startBufSize
 }
         if newSize > s.maxTokenSize {
            newSize = s.maxTokenSize
         }
         newBuf := make([]byte, newSize)
         copy(newBuf, s.buf[s.start:s.end])
         s.buf = newBuf
         s.end -= s.start
         s.start = 0
 }
      // Finally we can read some input. Make sure we don't get stuck with
 // a misbehaving Reader. Officially we don't need to do this, but let's // be extra careful: Scanner is for safe, simple jobs. for loop := 0; ; {
         n, err := s.r.Read(s.buf[s.end:len(s.buf)])
         s.end += n
         if err != nil {
            s.setErr(err)
            break
 }
         if n > 0 {
            s.empties = 0
 break
 }
         loop++
         if loop > maxConsecutiveEmptyReads {
            s.setErr(io.ErrNoProgress)
            break
 }
      }
   }
}

Scan()函数的流程可以总结为如下的几个步骤:

  1. 如果已经完成Scan, 即done为true,则直接返回false
  2. 否则尝试读取新的token/错误,即:

    1. 如果已经到达了最后的token(err为ErrFinalToken)则取出最后的token,并且设置done为true
    2. 如果发生了其他错误,则终止,返回false
    3. 如果读取了太多空token且没有发生错误,则抛出"bufio.scan: too many empty tokens without progressing"的异常(也就是发生了死循环)
  3. 在下一次读取token前要准备缓冲区的容量,确保不会发生溢出,或者缓冲不足的情况,具体处理如下:

    1. 首先将缓冲区的数据移到缓冲区的开头
    2. 如果缓冲区满,则将缓冲区的大小翻倍
  4. 将数据读入缓冲区

可以看到,Scanner 的数据结构允许我们自定义 SplitFunc, 从而根据不同的规则生成 token, 比如我们可以使用内置的 ScanRune 来作为 SplitFunc, 这样就实现了逐字符读取的需求,实现如下:

func ReadByRune(filePath string) {        // filePath为文件的路径
 file, err := os.Open(filePath)       // 首先打开文件,file的类型为 *os.File
 if err != nil {
      log.Fatal(err)
   }
   defer func() {
      if err := file.Close(); err != nil {
         log.Fatal(err)
      }
   }()
   fmt.Println(file.Name())               // 打印文件名
 fileScanner := bufio.NewScanner(file)     // 申请一个fileScanner, 类型为 *bufio.Scanner
 fileScanner.Split(bufio.ScanRunes)       // 这里替换SplitFunc为ScanRunes
 for fileScanner.Scan() {
      fmt.Println(fileScanner.Text())          // 按行读取,打印每行的内容
 }
}

Part2: 如何通过 bufio.Writer 写文件

先看一个 Write 的例子:

func WriteTo(dst *os.File, s string) {
   w := bufio.NewWriter(dst)
   fmt.Fprint(w, s)
   w.Flush() 
}

可以看到,我们首先创建了一个Writer实体,然后调用 fmt.Fprint 写入dst, 最后调用 Flush 就能成功写入文件。由于代码逻辑比较简单,这里我们直接看 bufio 中有关Writer的源码:

  • Writer的定义:
type Writer struct {
      err error
      buf []byte
      n   int
      wr  io.Writer
  }

可以看到,Writer的定义及其简介,只是在io.Writer的基础上增加了buf缓冲区。创建一个Writer的时候我们使用了NewWriter这一函数,而这个函数在实现的时候实际上是调用了 NewWriterSize, NewWriterSize 的实现如下:

func NewWriterSize(w io.Writer, size int) *Writer {

// 首先判断w是否已经为Writer,并且其缓冲区的size满足size需求
 b, ok := w.(*Writer)
   if ok && len(b.buf) >= size {
      return b
   }
   if size <= 0 {
      size = defaultBufSize
 }
 // 直接按照要求创建Writer对象
   return &Writer{
      buf: make([]byte, size),
 wr:  w,
 }
}
buf
func (b *Writer) Write(p []byte) (nn int, err error) {
   for len(p) > b.Available() && b.err == nil {
      var n int
 if b.Buffered() == 0 {     // 如果此时缓冲区为空,则直接写入io.Writer中
         // Large write, empty buffer.
 // Write directly from p to avoid copy. 
    n, b.err = b.wr.Write(p)
      } else {              // 否则使用缓冲区作为缓冲,调用copy
         n = copy(b.buf[b.n:], p)
         b.n += n
         b.Flush()
      }
      nn += n                   // 改变已经写入的大小,同时移动p
      p = p[n:]
   }
   if b.err != nil {
      return nn, b.err
   }
   n := copy(b.buf[b.n:], p)      // 将剩余部分写入
   b.n += n
   nn += n
   return nn, nil
}

Flush也是Writer的功能核心,它的实现如下:

func (b *Writer) Flush() error {
   if b.err != nil {
      return b.err
   }
   if b.n == 0 {
      return nil
 }
   n, err := b.wr.Write(b.buf[0:b.n])       // Write 的核心就是调用io.Writer中的Write函数
   if n < b.n && err == nil {
      err = io.ErrShortWrite
   }
   if err != nil {                          // 如果实际写入的byte数量小于buf中的byte数量,则调整buf中的数据位置之后抛出异常
      if n > 0 && n < b.n {
         copy(b.buf[0:b.n-n], b.buf[n:b.n])
      }
      b.n -= n
      b.err = err
      return err
   }
   b.n = 0
 return nil
}

基于上述的几个核心函数,bufio实现了对于文件的写操作。