golang中bufio.SplitFunc的深入理解

前言

bufio模块是golang标准库中的模块之一,主要是实现了一个读写的缓存,用于对数据的读取或者写入操作。该模块在多个涉及io的标准库中被使用,比如http模块中使用buffio来完成网络数据的读写,压缩文件的zip模块利用bufio来操作文件数据的读写等。

golang的bufio包里面定以的SplitFunc是一个比较重要也比较难以理解的东西,本文希望通过结合简单的实例介绍SplitFunc的工作原理以及如何实现一个自己的SplitFunc。

一个例子

在bufio包里面定义了一些常用的工具比如Scanner,你可能需要读取用户在标准输入里面输入的一些东西,比如我们做一个复读机,读取用户的每一行输入,然后打印出来:

package main
import (
 "bufio"
 "fmt"
 "os"
)
func main() {
 scanner := bufio.NewScanner(os.Stdin)
 scanner.Split(bufio.ScanLines)
 for scanner.Scan() {
 fmt.Println(scanner.Text())
 }
}

这个程序很简单,os.Stdin实现了io.Reader接口,我们从这个reader创建了一个scanner,设置分割函数为bufio.ScanLines,然后for循环,每次读到一行数据就将文本内容打印出来。麻雀虽小五脏俱全,这个小程序虽然简单,却引出了我们今天要介绍的对象: bufio.SplitFunc,它的定义是这个样子的:

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

golang官方文档的描述是这个样子的:

SplitFunc is the signature of the split function used to tokenize the input. The arguments are an initial substring of the remaining unprocessed data and a flag, atEOF, that reports whether the Reader has no more data to give. The return values are the number of bytes to advance the input and the next token to return to the user, if any, plus an error, if any.

Scanning stops if the function returns an error, in which case some of the input may be discarded.

Otherwise, the Scanner advances the input. If the token is not nil, the Scanner returns it to the user. If the token is nil, the Scanner reads more data and continues scanning; if there is no more data--if atEOF was true--the Scanner returns. If the data does not yet hold a complete token, for instance if it has no newline while scanning lines, a SplitFunc can return (0, nil, nil) to signal the Scanner to read more data into the slice and try again with a longer slice starting at the same point in the input.

The function is never called with an empty data slice unless atEOF is true. If atEOF is true, however, data may be non-empty and, as always, holds unprocessed text.

英文!参数这么多!返回值这么多!好烦!不知道各位读者遇到这种文档会不会有这种感觉...正式由于这种情况,我才决定写一篇文章介绍一下SplitFunc的具体工作原理,用一种通俗的方式结合具体实例加以说明,希望对读者有所帮助。
好了,废话少说,开始正题吧!

Scanner和SplitFunc的工作机制

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

Scanner是有缓存的,意思是Scanner底层维护了一个Slice用来保存已经从Reader中读取的数据,Scanner会调用我们设置SplitFunc,将缓冲区内容(data)和是否已经输入完了(atEOF)以参数的形式传递给SplitFunc,而SplitFunc的职责就是根据上述的两个参数返回下一次Scan需要前进几个字节(advance),分割出来的数据(token),以及错误(err)。

这是一个通信双向的过程,Scanner告诉我们的SplitFunc已经扫描到的数据和是否到结尾了,我们的SplitFunc则根据这些信息将分割的结果返回和下次扫描需要前进的位置返回给Scanner。用一个例子来说明:

package main
import (
 "bufio"
 "fmt"
 "strings"
)
func main() {
 input := "abcdefghijkl"
 scanner := bufio.NewScanner(strings.NewReader(input))
 split := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
  fmt.Printf("%t\t%d\t%s\n", atEOF, len(data), data)
  return 0, nil, nil
 }
 scanner.Split(split)
 buf := make([]byte, 2)
 scanner.Buffer(buf, bufio.MaxScanTokenSize)
 for scanner.Scan() {
  fmt.Printf("%s\n", scanner.Text())
 }
}

输出

false 2 ab
false 4 abcd
false 8 abcdefgh
false 12 abcdefghijkl
true 12 abcdefghijkl

这里我们把缓冲区的初始大小设置为了2,不够的时候会扩展为原来的2倍,最大为bufio.MaxScanTokenSize,这样一开始扫描2个字节,我们的缓冲区就满了,reader的内容还没有读取到EOF,然后split函数执行,输出:

false 2 ab

紧接着函数返回 0, nil, nil这个返回值告诉Scanner数据不够,下次读取的位置前进0位,需要继续从reader里面读取,此时因为缓冲区满了,所以容量扩展为2 * 2 = 4,reader的内容还没有读取到EOF,输出

false 4 abcd

重复上述步骤,一直到最后全部内容读取完了,EOF此时变成了true

true 12 abcdefghijkl

看了上面的过程是不是对SplitFunc的工作原来有了一点理解了呢?再回头看一下golang的官方文档有没有觉得稍微理解了一点?下面是bufio.ScanLines的实现,读者可以自己研究一下该函数是如何工作的

标准库里的ScanLines

func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
 // 表示我们已经扫描到结尾了
 if atEOF && len(data) == 0 {
  return 0, nil, nil
 }
 // 找到\n的位置
 if i := bytes.IndexByte(data, '\n'); i >= 0 {
  // 把下次开始读取的位置向前移动i + 1位
  return i + 1, dropCR(data[0:i]), nil
 }
 // 这里处理的reader内容全部读取完了,但是内容不为空,所以需要把剩余的数据返回
 if atEOF {
  return len(data), dropCR(data), nil
 }
 // 表示现在不能分割,向Reader请求更多的数据
 return 0, nil, nil
}

参考

In-depth introduction to bufio.Scanner in Golang

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 详解golang中bufio包的实现原理

    最近用golang写了一个处理文件的脚本,由于其中涉及到了文件读写,开始使用golang中的 io 包,后来发现golang 中提供了一个bufio的包,使用这个包可以大幅提高文件读写的效率,于是在网上搜索同样的文件读写为什么bufio 要比io的读写更快速呢?根据网上的资料和阅读源码,以下来详细解释下bufio的高效如何实现的. bufio 包介绍  bufio包实现了有缓冲的I/O.它包装一个io.Reader或io.Writer接口对象,创建另一个也实现了该接口,且同时还提供了缓冲和一些文

  • golang bufio包中Write方法的深入讲解

    前言 bufio包实现了带缓冲的I/O,它封装了io.Reader和io.Writer对象,然后创建了另外一种对象(Reader或Writer)实现了相同的接口,但是增加了缓冲功能. 首先来看没有缓冲功能的Write(os包中)方法,它会将数据直接写到文件中. package main import ( "os" "fmt" ) func main() { file, err := os.OpenFile("a.txt", os.O_CREATE

  • golang中bufio.SplitFunc的深入理解

    前言 bufio模块是golang标准库中的模块之一,主要是实现了一个读写的缓存,用于对数据的读取或者写入操作.该模块在多个涉及io的标准库中被使用,比如http模块中使用buffio来完成网络数据的读写,压缩文件的zip模块利用bufio来操作文件数据的读写等. golang的bufio包里面定以的SplitFunc是一个比较重要也比较难以理解的东西,本文希望通过结合简单的实例介绍SplitFunc的工作原理以及如何实现一个自己的SplitFunc. 一个例子 在bufio包里面定义了一些常用

  • Golang中time.After的使用理解与释放问题

    Golang中的time.After的使用理解 关于在goroutine中使用time.After的理解, 新手在学习过程中的"此时此刻"的理解,错误还请指正. 先线上代码: package main import ( "fmt" "time" ) func main() { //closeChannel() c := make(chan int) timeout := time.After(time.Second * 2) // t1 := t

  • 对Golang中的FORM相关字段理解

    Form 字段 通过调用Request结构体提供的方法,我们可以将URL.Body.或者以上两者的数据提取到该结构体的Form.PostForm和MultipartForm等字段中. (1)调用ParseForm方法或者ParseMultipartForm方法,对请求进行分析 (2)访问相应的字段 事例: package main import ( "net/http" "fmt" ) func process(w http.ResponseWriter, r *h

  • golang中的defer函数理解

    目录 golang的defer 什么是defer 理解defer defer什么时间执行(defer. return.返回值 三者的执行顺序) defer输出的值,就是定义时的值.而不是defer真正执行时的变量值(注意引用情况) 多个defer,执行顺序 defer的函数一定会执行么? panic情况 os.Exit情况 kill情况(Ctrl+C) 参考文献 golang的defer 什么是defer defer的的官方文档:https://golang.org/ref/spec#Defer

  • golang中为什么Response.Body需要被关闭详解

    前言 本文主要介绍了关于golang中Response.Body需要被关闭的相关内容,文中通过示例代码介绍的非常详细,对各位学习或者使用golang具有一定参考学习价值,下面话不多说了,来一起看看详细的介绍吧 Body io.ReadCloser The http Client and Transport guarantee that Body is always non-nil, even on responses without a body or responses with a zero

  • Golang中匿名组合实现伪继承的方法

    "Go语言的面向对象机制与一般语言不同. 它没有类层次结构, 甚至可以说没有类: 仅仅通过组合( 而不是继承) 简单的对象来构建复杂的对象." -- <Go语言圣经> 1.匿名组合 1.1 匿名组合定义 golang中组合语法,就是在一个类中,引入了另一个类,如 type Logger struct{ } type Work struct{ log Logger } type Work2 struct{ log *Logger } func (Logger)Info(v .

  • Golang中的参数传递示例详解

    前言 本文主要给大家介绍了关于Golang参数传递的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 关于参数传递,Golang文档中有这么一句: after they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution. 函数调用参数均为值传递,不是指针传递或引用传递.经测试引申出来,

  • golang中struct和interface的基础使用教程

    前言 本文主要给大家介绍了关于golang中struct和interface的相关内容,是属于golang的基本知识,下面话不多说了,来一起看看详细的介绍吧. struct struct 用来自定义复杂数据结构,可以包含多个字段(属性),可以嵌套:go中的struct类型理解为类,可以定义方法,和函数定义有些许区别:struct类型是值类型. struct定义 type User struct { Name string Age int32 mess string } var user User

  • golang中值类型/指针类型的变量区别总结

    前言 值类型:所有像int.float.bool和string这些类型都属于值类型,使用这些类型的变量直接指向存在内存中的值,值类型的变量的值存储在栈中.当使用等号=将一个变量的值赋给另一个变量时,如 j = i ,实际上是在内存中将 i 的值进行了拷贝.可以通过 &i 获取变量 i 的内存地址 指针类型:简单地说go语言的指针类型和C/C++的指针类型用法是一样的,除了出去安全性的考虑,go语言增加了一些限制,包括如下几条: 不同类型的指针不能互相转化,例如*int, int32, 以及int

随机推荐