36

Go Scanner的使用和源码分析

 5 years ago
source link: https://studygolang.com/articles/19954?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

简介

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个返回值。第一个是推进输入的字节(一般为标志位字节数)

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 意味着每次操作能够读取更多的数据,既减少了系统调用的次数,又通过以块为单位读取硬盘数据来更高效地使用底层硬件。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK