乐趣区

Go-Scanner的使用和源码分析

简介

go 标准库 bufio.Scanner,从字面意思来看是一个扫描器、扫描仪。所用是不停的从一个 reader 中读取数据兵缓存在内存中,还提供了一个注入函数用来自定义分割符。库中还提供了 4 个预定义分割方法。

  • ScanLines:以换行符分割(’n’)
  • ScanWords:返回通过“空格”分词的单词
  • ScanRunes:返回单个 UTF-8 编码的 rune 作为一个 token
  • ScanBytes:返回单个字节作为一个 token

使用方法

在看使用方法之前,我们需要先看一个函数。

type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)

这个函数接受一个 byte 数组,和一个 atEOF 标志位(标志位用来表示是否还有更多的数据)返回的是 3 个返回值。第一个是推进输入的字节(一般为标志位字节数)
在 splist 函数判断是否找到标志位,如果没有找到则可以返回(0,nil,nil) Scan 获取到这个返回值则会继续读取之后未读取完成的字符。如果找到则按照正确的返回值返回。下面是一个简单的使用例子

func main() {
    input := "abcend234234234"
    fmt.Println(strings.Index(input,"end"))
    scanner := bufio.NewScanner(strings.NewReader(input))
    scanner.Split(ScanEnd)
    // 设置读取缓冲读取大小 每次读取 2 个字节 如果缓冲区不够则翻倍增加缓冲区大小
    buf := make([]byte, 2)
    scanner.Buffer(buf, bufio.MaxScanTokenSize)
    for scanner.Scan() {fmt.Println("output:",scanner.Text())
    }
    if scanner.Err() != nil {fmt.Printf("error: %s\n", scanner.Err())
    }
}

func ScanEnd(data []byte, atEOF bool) (advance int, token []byte, err error) {
    // 如果数据为空,数据已经读完直接返回
    if atEOF && len(data) == 0 {return 0, nil, nil}
    // 获取自定义的结束标志位的位置
    index:= strings.Index(string(data),"end")
    if index > 0{
        // 如果找到 返回的第一个参数为后推的字符长度  
        // 第二个参数则指标志位之前的字符 
        // 第三个参数为是否有错误
        return index+3, data[0:index],nil
    }
    if atEOF {return len(data), data, nil
    }
    // 如果没有找到则返回 0,nil,nil
    return 0, nil, nil
}

上面的例子可以看到 字符串是”abcend234234234“
因为设置的是每次读取 2 个字符串
第一次读取:buf = ab 没有找到 end ScanEnd 返回 0,nil,nil
第二次读取:buf = abce 没有找到 end ScanEnd 返回 0,nil,nil
第三次读取:buf = abcend23(buf 翻倍扩容) 找到自定义标志位 end 返回:6,abc, nil 打出 out abc
第四次读取:buf = 23423423 之前的已经读取的被去掉,犹豫 buf 大小为 8 直接读取 8 个字符
第五次读取:由于 buf 容量不足翻倍之后 直接获取全部数据输出 out 234234234
结果则是:
output: abc
output: 234234234
可以看到 扫描器 按照自定义的读取大小和结束符 token 输出结果

源码查看

type Scanner struct {
    r            io.Reader // reader
    split        SplitFunc // 分割函数 又外部注入
    maxTokenSize int       // token 最大长度
    token        []byte    // split 返回的最后一个令牌
    buf          []byte    // 缓冲区字符
    start        int       // buf 中的第一个未处理字节
    end          int       // buf 中的数据结束 标志位
    err          error     // Sticky error.
    empties      int       // 连续空令牌的计数
    scanCalled   bool      //
    done         bool      // 扫描是否完成
}

func (s *Scanner) Scan() bool {
    if s.done {return false}
    s.scanCalled = true
    // for 循环知道找到 token 为止
    for {
        if s.end > s.start || s.err != nil {
            // 调用 split 函数 得到返回值,函数中判断是否有 token token 往后推的标志位数 是否有错误
            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 > 100 {panic("bufio.Scan: 100 empty tokens without progressing")
                    }
                }
                return true
            }
        }
        // 如果有错误 则返回 false
        if s.err != nil {
            // Shut it down.
            s.start = 0
            s.end = 0
            return false
        }
        // 重新设置开始位置 和结束位置 读取更多数据
        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
        }
        // 如果 buf 满了 如果满了重新创建一个长度为原来两倍大小的 buf
        if s.end == len(s.buf) {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
        }
        // 如果没有找到则往后继续读取数据
        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
            }
        }
    }
}

总结

根据上面的源码和例子可以看到这个扫描器的作用,当然正式使用时候不会只是读取一个写死的字符串。可以使用在读取 scoket 读取数据,IO 缓冲区 提供了一个临时存储区来存放数据,缓冲区存储的数据达到一定容量后才会被 ” 释放 ” 出来进行下一步存储,这种方式大大减少了写操作或是最终的系统调用被触发的次数,这无疑会在频繁使用系统资源的时候节省下巨大的系统开销。而对于读操作来说,缓冲 IO 意味着每次操作能够读取更多的数据,既减少了系统调用的次数,又通过以块为单位读取硬盘数据来更高效地使用底层硬件。

退出移动版