-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Go按行读取数据的坑 #10
Comments
go 的模块引入是按照引号来区分的麽,感觉好不直观,哈哈,我是来打酱油的。 |
是啊,引号括起来,一行一个,其实Go的模块管理很灵活的: http://yilvqingfeng.diandian.com/post/2013-02-05/40049732905 |
可以在调用Scan之前设置buffer的大小,比如 |
|
这样不会遗漏最后一行,关键点:最后一次返回的错误是 io.EOF,line 里面是可能有数据的。
|
这里貌似还有个坑: |
做大数据全家桶的约定换行符为 \n, 这样就简单了。 另外,这种流的方式读取确实方便,但是效率比不上直接对整块大数据 bytes.Split()。 |
地确,很坑 |
首先从一个日志分析的Go程序说起,基本功能就是一行一行读取数据并处理。代码大体是这样的:
因为数据是用hadoop直接cat出来的,通过管道传输到日志处理程序,所以这里输入源是
stdin
。实际处理的时候发现一个比较奇怪的现象,每个日志文件的总行数差别不大,但是有的文件处理时间明显比其他短,并且还会能看到这样的日志信息:
cat: Unable to write to output stream.
。这个日志不是Go里面的,确认是hadoop报了这个错误,起初以为是hadoop在某些情况下可能会出现这个错误,因为出现这个错误就意味着某些文件没有处理完程序就退出了。这个是不能容忍的。于是便向运维那边报bug。运维那边承认这个错误是hadoop报的,但是出现这个错误一般是因为
stream
断了,比如如果有head操作就会出现这个错误:为了证明是后面处理程序的问题而不是hadoop的问题,运维那边做了这样一个操作:
cat非0,后面的程序返回是0,UNIX基本程序一般是不会有问题的,看来是后面的Go程序有问题。开始检查验证。
首先要确认的问题有两个:
查询这个问题也是比较容易的,将处理过的每一行日志输出并打印行号,这样就知道处理到哪一行了,也就能知道在哪一行出错。
修改程序后重新执行,确实是Go程序提前退出了,并且没处理的下一行唯一的特殊之处是这一行超级长。难道Go不能处理很长的一行?这看起来不可能发生,但是还要验证一下。
首先是重新review代码,按理说程序出错之后会有错误日志的,为啥Go程序不但没报错还正常退出了?首先是review数据处理代码,没发现问题,然后就开始怀疑是bufio.Scanner的问题,重新看了一下Go文档,官网有一个example是这么写的:
原来scanner还有一个
Err()
方法,官方的说法是Err returns the first non-EOF error that was encountered by the Scanner.
,也就是说如果scanner.Scan()
如果出错的话错误信息是要通过Err()方法才能得到的,我的go程序将这个Err忽略了,出错后退出for循环就直接退出程序了,到底是有什么错误?代码补充完整之后看到这样的错误:bufio.Scanner: token too long
。到目前能确认的确实是Go程序问题,在读数据的时候出现错误,上面的错误是什么意思?翻Go代码。
确实有这个错误,继续看scan.go,看看在哪报
ErrTooLong
这个错误:scan.go 147行出现的错误,看具体代码:
看了一下Scanner模块的代码,原来是这样的:Scanner在初始化的时候有设置一个
maxTokenSize
,这个值默认是MaxScanTokenSize = 64 * 1024
,当一行的长度大于64*1024即65536之后,就会出现ErrTooLong
错误。这样就能解释之前为什么长行处理报错的问题了。毕竟日志一行可能不止65536这么长,问题还是要解决的,有两种方案:
对于方案1,仔细看了一下Scnner,并没有发现有接口可以修改这个值,除非修改Go源码并重新编译Go,这个太折腾。在官方文档里面发现这样一段话:
它告诉我们如果token太大(行太长)的时候,要使用
bufio.Reader
。这里是第一个坑,怪当初用的时候没仔细看文档,没仔细研读Go源码。从这里我们可以得到一个教训: 除非能确定行长度不超过65536,否则不要使用bufio.Scanner!那接下来老老实实用
bufio.Reader
吧。那bufio.Reader
有没有类似bufio.Scanner
的问题呢?看了一下
bufio.Reader
代码,这个也是有缓冲区大小限制的,并且默认缓冲区大小是4096
,还好有一个函数NewReaderSize
可以调整这个缓冲区大小。还是之前的需求,如何按行去读取数据呢?
bufio.Reader
提供了一个ReadLine()
函数,文档上是这么说的:意思是这个函数比较底层,建议使用ReadBytes或ReadString或者Scanner。继续看文档说明:
从这里我们能看出来设置缓冲区的作用了,ReadLine会尽量去读取并返回完整的一行,但是如果行太长缓冲区满了的话,就不会返回完整的一行而是返回缓冲区里面的内容,并且会设置isPrefix为true。这时候需要继续调用ReadLine直到将完整一行读完。然后外层调用程序需要将这些块拼起来才能组成完整的行。不仅要处理isPrefix还要处理前缀,麻烦!除非我们主动设置一个非常大的缓冲,但是前提是你必须知道最长行的长度,在大多数情况下这个是无法预先知道的。怪不得建议我们使用ReadBytes或ReadString或者Scanner。
之前讨论了,Scanner在行太长的时候是有问题的,ReadBytes和ReadString原理上是一样的,这里我们以ReadString为例看看这个函数。ReadString原理很简单,使用也很方便,基本用法是指定一个分隔符,比如我们如果读取一行的话就指定分隔符为
\n
,这样ReadString就去不断去读数据,直到发现分隔符\n
或者出错为止。官网是这么说的:(ps: 这里又在推荐Scanner...)
ReadString有没有缓冲区大小限制呢?这个其实也还是有的,不过,它在代码里面就将这个缓冲区的问题处理好了,也就是说能保证返回的一行肯定是完整的。最起码用起来比ReadLine函数方便。
那么用ReadString去按行读取有没有坑呢?答案是肯定的。
上面这段代码的输出是:
为什么
c
没有输出?这里算是一个坑。之前讨论过,按行读取的话ReadString函数需要以\n
作为分割,上面那种特殊情况当数据末尾没有\n
的时候,直到EOF还没有分隔符\n
,这时候返回EOF错误,但是line里面还是有数据的,如果不处理的话就会漏掉最后一行。简单修改一下:这样执行会输出:
这样处理的时候也要当心,因为最后一行后面没有
\n
。相比之下,python处理要简单和清晰很多,有生成器,直接for循环遍历即可:
The text was updated successfully, but these errors were encountered: