golang 源码阅读 —— bufio

陈蕃
• 阅读 4135

前言

文件读写一直是我在学习一门语言的时候比较难以记忆和弄懂的部分,每次当我使用比如 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())          // 按行读取,打印每行的内容
 }
}
  • 测试:

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

golang 源码阅读 —— bufio

- 程序运行结果:

golang 源码阅读 —— bufio

  • 源码分析

既然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.
  }

可以看到,Scanner 是对 io.Reader 更加完备的封装,而且值得注意的是,Scanner数据结构中还有一个函数类型的变量: SplitFunc, 这是Scanner 在调用 Scan 后能够实现按行读取的核心部分。
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,
 }
}

bufio中的Writer最重要的特性就是 buf 这个成员的使用。当且仅当 Writer 调用 Flush 方法时,buf中的内容才会被写进封装的 io.Writer 对象中。对 buf 的操作中,Write是较为重要的一个,它的实现如下:

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实现了对于文件的写操作。

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Easter79 Easter79
4年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
4年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Stella981 Stella981
4年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
2年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这