無痕的碎碎念

Golang 各种输入姿势

原创文章,转载请标明出处:無痕的碎碎念
本文链接地址:Golang 各种输入姿势

最近开始在Hacker Rank上面刷题玩,因为从开始工作起一直是用的Go语言作为主力开发语言,所以就选择用Go语言来解题了。

刚开始做第一题Hello World,突然发现自己还没用Go语言从stdin读取过数据。于是自己摸索加各种地方找了一下资料,打算总结一下Go语言从标准输入读取数据的姿势。

fmt

经常使用fmt.Printf之类的做输出 (tiáo shì) ,在处理输入自然是第一个想到fmt包。

fmt提供了许多输入函数:

  • Scan
  • Scanf
  • Scanln

由于只讲从stdin读取,所以其他的FscanfSscanf之类的就省略了,下面逐一来玩一下这三个输入。

Scan

1
func Scan(a ...interface{}) (n int, err error)

Scan从标准输入读取数据,以空格作为分隔(换行符也视作空格),依次将输入赋给参数。他会返回读取参数的个数,和读取过程中遇到的错误。

来个例子:

1
2
3
4
5
6
7
8
9
10
11
// Code:
func main() {
var i int
var d float32
var s string
fmt.Scan(&i, &d, &s)
}
// Input:
1 2.2
Three

上述代码接受上述输入之后,i里面存储的是1d里面存储的是2.2s里面存储的是Three

需要注意的一点是:因为Scan是以空格和换行作为分隔,所以Scan不能将一整行作为一个字符串。
例子:

1
2
3
4
5
6
7
8
// Code:
func main() {
var s string
fmt.Scan(&s)
}
// Input:
This is a string.

上述代码接受上述输入之后,s里面存储的是This,而is a string会继续保留在stdin中。

Scanf

1
func Scanf(format string, a ...interface{}) (n int, err error)

ScanfScan类似,只是可以自己定义输入格式。

来个例子:

1
2
3
4
5
6
7
8
9
10
11
// Code:
func main() {
var i int
var d float32
var s string
fmt.Scanf("%d %f %s", &i, &d, &s)
}
// Input:
1 2.2
Three

上述代码接受上述输入之后,i里面存储的是1d里面存储的是2.2s依然是空字符串。因为给定的输入格式是%d %f %s,而Three2.2之间的是\n。所以Three并没有存储到s中。把输入格式改成%d %f\n%s就可以顺利读取了。

Scanf的格式控制非常强大,可以做很多事情,例如指定长度:

1
2
3
4
5
6
7
8
// Code:
func main() {
var i, j int
fmt.Scanf("%3d%2d", &i, &j)
}
// Input:
1234567

上述代码接受上述输入之后,i里面存储的是123j里面存储的是45,而67依然保留在stdin中。

Scanln

1
func Scanln(a ...interface{}) (n int, err error)

ScanlnScan几乎一样,只不过Scanln将换行符作为一次输入的结束。

来个栗子:

1
2
3
4
5
6
7
8
9
10
11
// Code:
func main() {
var i int
var d float32
var s string
fmt.Scanln(&i, &d, &s)
}
// Input:
1 2.2
Three

上述代码接受上述输入之后,i里面存储的是1d里面存储的是2.2s依然是空字符串。因为Scanln将换行符作为输入的结束,所以Three被保留在了stdin中。

小结

fmt中提供了非常方便而且强大的输入函数,基本可以满足各种 (qí guài) 输入需求了。但是目前还没找到方法能一次读取一整行。

bufio

bufio是在处理各种I/O的时候常用的包之一,他实现了各种带缓冲的I/O。其中有两个和输入有关的结构:

  • Reader
  • Scanner

下面来看一下这两货怎么玩.

Reader

Readerbufio中提供的一个带缓存的Reader,通过NewReader创建的Reader的默认缓存大小是4096 byte。也可以通过NewReaderSize来创建一个自定义缓存大小的Reader,不过缓存最小是16 byte。

来个例子:

1
2
3
4
5
6
func main() {
// Default size.
reader := bufio.NewReader(os.Stdin)
// Custom size.
reader = bufio.NewReaderSize(os.Stdin, 1024)
}

上述代码先后创建了两个Reader,都是对stdin进行操作的。其中第一个Reader的缓存大小是4096 byte,第二个的缓存大小是1024 byte。

Reader提供了很多很好用的读取函数:

  • Read
  • ReadBytes
  • ReadSlice
  • ReadByte
  • ReadRune
  • ReadString
  • ReadLine

还有一些和输入没太大关系我就忽略了,在函数列表里面看到了ReadLine,不能更激动,终于可以一次读取一行了,不过还是一个一个来讲一下。

Read

1
func (b *Reader) Read(p []byte) (n int, err error)

Read函数需要一个byte切片作为参数,一次读取最多将给定的切片读满。返回值是实际读取的字节数,以及读取过程中遇到的错误。

ReadBytes

1
func (b *Reader) ReadBytes(delim byte) ([]byte, error)

ReadBytes函数需要给定一个byte作为参数,然后他会一直读取,直到读取到给定的byte,EOF或者在读取过程中发生错误。

从源码里面看到了官方的一行注释 (tǔ cáo)

1
// For simple uses, a Scanner may be more convenient.

所以没有什么特殊需求的话,还是推荐使用之后讲的Scanner

ReadSlice

1
func (b *Reader) ReadSlice(delim byte) (line []byte, err error)

ReadSlice函数需要给定一个byte作为参数,然后他会一直读取,直到读取到给定的byte,EOF或者发生错误。

ReadSliceReadBytes的区别在于:ReadBytes有I/O操作的,而ReadSlice没有I/O操作ReadSlice的读取仅限于已经缓存的部分,而ReadBytes会一直读取输入和刷新缓存。

所以如果某一次调用ReadSlice把缓存读完了的话,后面再调用是不会返回任何结果的,需要调用ReadBytes或者ReadString触发下一次I/O操作。所以一般情况下直接调用ReadBytesReadString相对来说好一些。

ReadByte

1
func (b *Reader) ReadByte() (byte, error)

ReadByte比较简单粗暴,就是直接读取一个byte的内容。

ReadRune

1
func (b *Reader) ReadRune() (r rune, size int, err error)

Rune是Go语言里面的字符类型,可以当做是C/C++里面的char,只不过Rune是UTF-8编码的,本质是int32的别名。

ReadRune会读取一个Rune的内容,并且返回这个Rune占用了多少byte。

ReadString

1
func (b *Reader) ReadString(delim byte) (string, error)

ReadString函数需要给定一个byte作为参数,然后他会一直读取,直到读取到给定的byte,并将读取的内容转换成字符串返回。

从源码上来看,ReadString直接调用了ReadBytes读取数据,然后转换成了字符串。

ReadLine

1
func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)

ReadLine会读取一整行的内容,如果这一行的数据很大,超过了缓存的大小,isPrefix会设置为true

从源码上来看,ReadLine是调用了ReadSlice,所以ReadLine是没有I/O操作的,并且会丢掉换行符(\n或者\r\n)。

由于是调用了ReadSlice,所以不推荐使用ReadLine来读取一整行内容,而是使用ReadBytes('\n')或者ReadString('\n')来代替。

小结

Reader提供的方法读取到的值都是byterune或者string类型的,用Reader来读取int或者float32等类型的话还不如直接用fmt包的Scan方法。

不过总算是能用Reader一次读取一整行了。

Scanner

Scanner提供了一些接口,能很方便的一次读取一行,非常适合拿来读取文件之类的数据。Scanner只能通过NewScanner来创建,缓存大小最开始会设置为4096 byte,之后如果读取到更大的数据会自己调整,最大可以达到 65536 byte。

来个例子:

1
2
3
func main() {
var scanner = bufio.NewScanner(os.Stdin)
}

上述代码创建了一个Scanner,用来对stdin做操作。

Scanner中有一个token的概念,一个token表示一次读取的内容,可以是一整行,也可以是自定义的一段数据,token之间的划分通过SplitFunc来进行,关于SplitFunc的内容包含在了Split函数中了,此处不再赘述。

Scanner提供的方法相对Reader来说要少很多,主要有以下几个:

  • Split
  • Scan
  • Bytes
  • Text

Split

1
func (s *Scanner) Split(split SplitFunc)

Split 是可以传入一个SplitFunc,传入的这个方法是用来帮忙界定“行”的,即你可以自己规定怎么样才算一“行”。默认的SplitFunc就是按行读入。

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

一个标准的SplitFunc需要接受两个参数:

  • 读取到的原始数据
  • 是否读取到EOF的标识

SplitFunc根据传入的两个参数进行处理,并返回三个值:

  • 本次分割的长度
  • 实际读取到的内容
  • 分割过程中发生的错误

其中第一个返回值不一定就是len(token)Scanner默认的SplitFunc来解释一下:

Scanner默认使用的SplitFunc\n作为分隔符,返回分割的长度,实际读取到的内容和分割过程中的错误,其中分割的长度包含了行尾的\n或者\r\n,而实际读取到的内容中不包含,所以len(token)是不等于advance的。而advance的值,可以看作是偏移量。

其次,还有一个需要注意的地方,如果想通过每次返回advance0的方式来一直读取同一个token的话,会导致程序出现PANIC。因为在等下要介绍的Scan函数中做了判定:如果连续100次读取到的token不为空,但是advance等于0的话,会直接PANIC

1
panic("bufio.Scan: 100 empty tokens without progressing")

当然,advance为负数的话就更不行了。

Scan

1
func (s *Scanner) Scan() bool

从函数前面上看,Scan非常简单,不接受任何参数,只返回一个布尔值来表示是否已经全部读取完毕。但是从源码中可以知道,Scan做了非常多的事情。

每次调用Scan都会读取一个token,并存放在内部。如果缓存大小不够了会自动扩大缓存。然后还有一些错误的处理等。

Bytes

1
func (s *Scanner) Bytes() []byte

Bytes是将上一次调用Scan所读取到的token[]byte的形式返回。

所以连续多次调用Bytes会得到一模一样的结果。

Text

1
func (s *Scanner) Text() string

TextBytes一样,只不过是将token转化成string再返回。

同样的,连续多次调用Text也会得到一模一样的结果。

小结

Scanner如果能比较好的理解tokenSplitFunc的话,用起来的确比Reader要方便很多。不过和Reader一样,获得的数据都是[]bytestring,如果要获取其他类型的数据的话需要再进行一次转换,不过这时候就不如直接用fmtScan函数来做了。

总结

为了用Go语言做一个“Hello World”的大水题我也是够拼的,搞了这么多事情出来。通过这次的折腾,找资料,看源码了解到了许多平常不会注意到的事情:

反正不看源码我是不会知道ReadSliceReadString是一次性的 =。=

最后还是贴一发我的“Hello world”的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
var scan = bufio.NewScanner(os.Stdin)
if scan.Scan() {
fmt.Printf("Hello, World.\n%s", scan.Text())
}
}