原创文章,转载请标明出处:無痕的碎碎念
本文链接地址:Golang 各种输入姿势
最近开始在Hacker Rank上面刷题玩,因为从开始工作起一直是用的Go语言作为主力开发语言,所以就选择用Go语言来解题了。
刚开始做第一题Hello World,突然发现自己还没用Go语言从stdin读取过数据。于是自己摸索加各种地方找了一下资料,打算总结一下Go语言从标准输入读取数据的姿势。
fmt
经常使用fmt.Printf之类的做输出,在处理输入自然是第一个想到fmt包。
fmt提供了许多输入函数:
ScanScanfScanln
由于只讲从stdin读取,所以其他的Fscanf、Sscanf之类的就省略了,下面逐一来玩一下这三个输入。
Scan
|
|
Scan从标准输入读取数据,以空格作为分隔(换行符也视作空格),依次将输入赋给参数。他会返回读取参数的个数,和读取过程中遇到的错误。
来个例子:
|
|
上述代码接受上述输入之后,i里面存储的是1,d里面存储的是2.2,s里面存储的是Three。
需要注意的一点是:因为Scan是以空格和换行作为分隔,所以Scan不能将一整行作为一个字符串。
例子:
|
|
上述代码接受上述输入之后,s里面存储的是This,而is a string会继续保留在stdin中。
Scanf
|
|
Scanf 和Scan类似,只是可以自己定义输入格式。
来个例子:
|
|
上述代码接受上述输入之后,i里面存储的是1,d里面存储的是2.2,s依然是空字符串。因为给定的输入格式是%d %f %s,而Three和2.2之间的是\n。所以Three并没有存储到s中。把输入格式改成%d %f\n%s就可以顺利读取了。
Scanf的格式控制非常强大,可以做很多事情,例如指定长度:
|
|
上述代码接受上述输入之后,i里面存储的是123,j里面存储的是45,而67依然保留在stdin中。
Scanln
|
|
Scanln和Scan几乎一样,只不过Scanln将换行符作为一次输入的结束。
来个栗子:
|
|
上述代码接受上述输入之后,i里面存储的是1,d里面存储的是2.2,s依然是空字符串。因为Scanln将换行符作为输入的结束,所以Three被保留在了stdin中。
小结
fmt中提供了非常方便而且强大的输入函数,基本可以满足各种输入需求了。但是目前还没找到方法能一次读取一整行。
bufio
bufio是在处理各种I/O的时候常用的包之一,他实现了各种带缓冲的I/O。其中有两个和输入有关的结构:
ReaderScanner
下面来看一下这两货怎么玩.
Reader
Reader是bufio中提供的一个带缓存的Reader,通过NewReader创建的Reader的默认缓存大小是4096 byte。也可以通过NewReaderSize来创建一个自定义缓存大小的Reader,不过缓存最小是16 byte。
来个例子:
|
|
上述代码先后创建了两个Reader,都是对stdin进行操作的。其中第一个Reader的缓存大小是4096 byte,第二个的缓存大小是1024 byte。
Reader提供了很多很好用的读取函数:
ReadReadBytesReadSliceReadByteReadRuneReadStringReadLine
还有一些和输入没太大关系我就忽略了,在函数列表里面看到了ReadLine,不能更激动,终于可以一次读取一行了,不过还是一个一个来讲一下。
Read
|
|
Read函数需要一个byte切片作为参数,一次读取最多将给定的切片读满。返回值是实际读取的字节数,以及读取过程中遇到的错误。
ReadBytes
|
|
ReadBytes函数需要给定一个byte作为参数,然后他会一直读取,直到读取到给定的byte,EOF或者在读取过程中发生错误。
从源码里面看到了官方的一行注释:
|
|
所以没有什么特殊需求的话,还是推荐使用之后讲的Scanner
ReadSlice
|
|
ReadSlice函数需要给定一个byte作为参数,然后他会一直读取,直到读取到给定的byte,EOF或者发生错误。
ReadSlice和ReadBytes的区别在于:ReadBytes是有I/O操作的,而ReadSlice没有I/O操作。ReadSlice的读取仅限于已经缓存的部分,而ReadBytes会一直读取输入和刷新缓存。
所以如果某一次调用ReadSlice把缓存读完了的话,后面再调用是不会返回任何结果的,需要调用ReadBytes或者ReadString触发下一次I/O操作。所以一般情况下直接调用ReadBytes和ReadString相对来说好一些。
ReadByte
|
|
ReadByte比较简单粗暴,就是直接读取一个byte的内容。
ReadRune
|
|
Rune是Go语言里面的字符类型,可以当做是C/C++里面的char,只不过Rune是UTF-8编码的,本质是int32的别名。
ReadRune会读取一个Rune的内容,并且返回这个Rune占用了多少byte。
ReadString
|
|
ReadString函数需要给定一个byte作为参数,然后他会一直读取,直到读取到给定的byte,并将读取的内容转换成字符串返回。
从源码上来看,ReadString直接调用了ReadBytes读取数据,然后转换成了字符串。
ReadLine
|
|
ReadLine会读取一整行的内容,如果这一行的数据很大,超过了缓存的大小,isPrefix会设置为true。
从源码上来看,ReadLine是调用了ReadSlice,所以ReadLine是没有I/O操作的,并且会丢掉换行符(\n或者\r\n)。
由于是调用了ReadSlice,所以不推荐使用ReadLine来读取一整行内容,而是使用ReadBytes('\n')或者ReadString('\n')来代替。
小结
Reader提供的方法读取到的值都是byte,rune或者string类型的,用Reader来读取int或者float32等类型的话还不如直接用fmt包的Scan方法。
不过总算是能用Reader一次读取一整行了。
Scanner
Scanner提供了一些接口,能很方便的一次读取一行,非常适合拿来读取文件之类的数据。Scanner只能通过NewScanner来创建,缓存大小最开始会设置为4096 byte,之后如果读取到更大的数据会自己调整,最大可以达到 65536 byte。
来个例子:
|
|
上述代码创建了一个Scanner,用来对stdin做操作。
在Scanner中有一个token的概念,一个token表示一次读取的内容,可以是一整行,也可以是自定义的一段数据,token之间的划分通过SplitFunc来进行,关于SplitFunc的内容包含在了Split函数中了,此处不再赘述。
Scanner提供的方法相对Reader来说要少很多,主要有以下几个:
SplitScanBytesText
Split
|
|
Split 是可以传入一个SplitFunc,传入的这个方法是用来帮忙界定“行”的,即你可以自己规定怎么样才算一“行”。默认的SplitFunc就是按行读入。
SplitFunc
|
|
一个标准的SplitFunc需要接受两个参数:
- 读取到的原始数据
- 是否读取到
EOF的标识
SplitFunc根据传入的两个参数进行处理,并返回三个值:
- 本次分割的长度
- 实际读取到的内容
- 分割过程中发生的错误
其中第一个返回值不一定就是len(token)用Scanner默认的SplitFunc来解释一下:
Scanner默认使用的SplitFunc以\n作为分隔符,返回分割的长度,实际读取到的内容和分割过程中的错误,其中分割的长度包含了行尾的\n或者\r\n,而实际读取到的内容中不包含,所以len(token)是不等于advance的。而advance的值,可以看作是偏移量。
其次,还有一个需要注意的地方,如果想通过每次返回advance为0的方式来一直读取同一个token的话,会导致程序出现PANIC。因为在等下要介绍的Scan函数中做了判定:如果连续100次读取到的token不为空,但是advance等于0的话,会直接PANIC:
|
|
当然,advance为负数的话就更不行了。
Scan
|
|
从函数前面上看,Scan非常简单,不接受任何参数,只返回一个布尔值来表示是否已经全部读取完毕。但是从源码中可以知道,Scan做了非常多的事情。
每次调用Scan都会读取一个token,并存放在内部。如果缓存大小不够了会自动扩大缓存。然后还有一些错误的处理等。
Bytes
|
|
Bytes是将上一次调用Scan所读取到的token以[]byte的形式返回。
所以连续多次调用Bytes会得到一模一样的结果。
Text
|
|
Text和Bytes一样,只不过是将token转化成string再返回。
同样的,连续多次调用Text也会得到一模一样的结果。
小结
Scanner如果能比较好的理解token和SplitFunc的话,用起来的确比Reader要方便很多。不过和Reader一样,获得的数据都是[]byte和string,如果要获取其他类型的数据的话需要再进行一次转换,不过这时候就不如直接用fmt的Scan函数来做了。
总结
为了用Go语言做一个“Hello World”的大水题我也是够拼的,搞了这么多事情出来。通过这次的折腾,找资料,看源码了解到了许多平常不会注意到的事情:
反正不看源码我是不会知道ReadSlice和ReadString是一次性的 =。=
最后还是贴一发我的“Hello world”的源码:
|
|