原创文章,转载请标明出处:無痕的碎碎念
本文链接地址:Golang 各种输入姿势
最近开始在Hacker Rank上面刷题玩,因为从开始工作起一直是用的Go语言作为主力开发语言,所以就选择用Go语言来解题了。
刚开始做第一题Hello World,突然发现自己还没用Go语言从stdin
读取过数据。于是自己摸索加各种地方找了一下资料,打算总结一下Go语言从标准输入读取数据的姿势。
fmt
经常使用fmt.Printf
之类的做输出,在处理输入自然是第一个想到fmt
包。
fmt
提供了许多输入函数:
Scan
Scanf
Scanln
由于只讲从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。其中有两个和输入有关的结构:
Reader
Scanner
下面来看一下这两货怎么玩.
Reader
Reader
是bufio
中提供的一个带缓存的Reader
,通过NewReader
创建的Reader
的默认缓存大小是4096 byte。也可以通过NewReaderSize
来创建一个自定义缓存大小的Reader
,不过缓存最小是16 byte。
来个例子:
|
|
上述代码先后创建了两个Reader
,都是对stdin
进行操作的。其中第一个Reader
的缓存大小是4096 byte,第二个的缓存大小是1024 byte。
Reader
提供了很多很好用的读取函数:
Read
ReadBytes
ReadSlice
ReadByte
ReadRune
ReadString
ReadLine
还有一些和输入没太大关系我就忽略了,在函数列表里面看到了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
来说要少很多,主要有以下几个:
Split
Scan
Bytes
Text
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”的源码:
|
|