Go中的标准输入处理及测试
前言
fmt.Scan
, fmt.Scanln
, 和 fmt.Scanf
是 Go 语言标准库中用于读取用户输入的函数。在使用过程中,我发现了他们不符合预期的表现,于是进行了多种测试,并通过源码分析了其背后的原理。
测试
fmt.Scanln
读取输入到指定变量,直到遇到换行符,如果不符合要求会返回错误
test1 读入整数
func main() {
scanln_test()
}
func scanln_test() {
var a int
n, err := fmt.Scanln(&a)
fmt.Println("n=", n, "err=", err, "a=", a)
}
测试结果
PS D:\code\GO\test> go run .\main.go
123 56
n= 1 err= expected newline a= 123
PS D:\code\GO\test> go run .\main.go
s999
n= 0 err= expected integer a= 0
PS D:\code\GO\test> go run .\main.go # 第一个字符
1s1
n= 1 err= expected newline a= 1
PS D:\code\GO\test> go run .\main.go
222
n= 1 err= <nil> a= 222
PS D:\code\GO\test> go run .\main.go
n= 0 err= unexpected newline a= 0
test2 读入字符串
func scanln_test() {
var a string
n, err := fmt.Scanln(&a)
fmt.Println("n=", n, "err=", err, "a=", a)
}
测试结果
PS D:\code\GO\test> go run .\main.go # 空格及其后面的内容同样没有被读入
111111 00000
n= 1 err= <nil> a= 111111
fmt.Scan
忽略输入前面的空格和换行,读取到输入后,遇到换行或空格返回
test1 读入字符串
func scan_test() {
var a string
n, err := fmt.Scan(&a)
fmt.Println("n=", n, "err=", err, "a=", a)
}
测试结果
PS D:\code\GO\test> go run .\main.go
1d1d1 asd
n= 1 err= <nil> a= 1d1d1
PS D:\code\GO\test> go run .\main.go
n= 1 err= <nil> a= 1s1s1ddd
PS D:\code\GO\test> go run .\main.go // 后面输入了一些回车和换行
333
n= 1 err= <nil> a= 333
test2 读入Int
func scan_test() {
var a int
n, err := fmt.Scan(&a)
fmt.Println("n=", n, "err=", err, "a=", a)
}
测试结果
PS D:\code\GO\test> go run .\main.go
11 11
n= 1 err= expected newline a= 11
1111
n= 1 err= <nil> a= 1111
PS D:\code\GO\test> go run .\main.go
11 11
n= 1 err= <nil> a= 11
PS D:\code\GO\test> go run .\main.go
1s1
n= 1 err= <nil> a= 1
PS D:\code\GO\test> go run .\main.go
asdas111
n= 0 err= expected integer a= 0
PS D:\code\GO\test> go run .\main.go
2
n= 1 err= <nil> a= 2
PS D:\code\GO\test> go run .\main.go
s
n= 0 err= expected integer a= 0
fmt.Scanf
格式化读入,暗含强制类型转换,如果格式中没有前导空格,会自动开头忽略空格但不会忽略换行
test1 读入int
func scanf_test() {
var a int
n, err := fmt.Scanf("%d", &a)
fmt.Println("n=", n, "err=", err, "a=", a)
}
测试结果
PS D:\code\GO\test> go run .\main.go
111
n= 1 err= <nil> a= 111
PS D:\code\GO\test> go run .\main.go
44 44
n= 1 err= <nil> a= 44
PS D:\code\GO\test> go run .\main.go
44
n= 1 err= <nil> a= 44
PS D:\code\GO\test> go run .\main.go
asdas11
n= 0 err= expected integer a= 0
PS D:\code\GO\test> go run .\main.go
1222ss
n= 1 err= <nil> a= 1222
PS D:\code\GO\test> go run .\main.go
n= 0 err= unexpected newline a= 0
PS D:\code\GO\test> go run .\main.go
sss
n= 0 err= expected space in input to match format a= 0
test2 格式中前面有空格
但如果在格式参数中写入了前导空格,则输入变量值前面的前必须也有空格,如果空格数目多余格式中的,会自动忽略多余的,即时a是string类型也是一样的,其他字符前面的空格永远不会写入变量。
func scanf_test() {
var a int
n, err := fmt.Scanf(" %d", &a)
fmt.Println("n=", n, "err=", err, "a=", a)
}
测试结果
PS D:\code\GO\test> go run .\main.go
11
n= 0 err= expected space in input to match format a= 0
PS D:\code\GO\test> go run .\main.go
1
n= 1 err= <nil> a= 1
PS D:\code\GO\test> go run .\main.go
111
n= 1 err= <nil> a= 111
test3 格式中用\n
如果格式中有\n同样会要求输入对应数目的换行
func scanf_test() {
var a int
n, err := fmt.Scanf("\n%d", &a)
fmt.Println("n=", n, "err=", err, "a=", a)
}
测试结果
PS D:\code\GO\test> go run .\main.go
1
n= 1 err= <nil> a= 1
PS D:\code\GO\test> go run .\main.go
10
n= 0 err= newline in format does not match input a= 0
PS D:\code\GO\test> go run .\main.go
n= 0 err= unexpected newline a= 0
test4 格式后面有空格
格式后面的空格不被严格要求
func scanf_test() {
var a int
n, err := fmt.Scanf("%d ", &a)
fmt.Println("n=", n, "err=", err, "a=", a)
}
测试结果
PS D:\code\GO\test> go run .\main.go // 下面值输入了一个44,并没有其他空格
44
n= 1 err= <nil> a= 44
test5 格式的变量之间有空格
多个变量之间的空格也不会被严格要求
func scanf_test() {
var a, b int
n, err := fmt.Scanf("%d %d", &a, &b)
fmt.Println("n=", n, "err=", err, "a=", "b=", b)
}
测试结果
PS D:\code\GO\test> go run .\main.go
11 44
n= 2 err= <nil> a= 11
PS D:\code\GO\test> go run .\main.go
11 11
n= 2 err= <nil> a= b= 11
PS D:\code\GO\test> go run .\main.go
11 11
n= 2 err= <nil> a= b= 11
test6 存在其他字符的情况
空格之间有其他字符时,也不会严格要求空格数目, 但必须有空格
func scanf_test() {
var a, b int
n, err := fmt.Scanf("%d * %d", &a, &b)
fmt.Println("n=", n, "err=", err, "a=", "b=", b)
}
测试结果
PS D:\code\GO\test> go run .\main.go
11*11
n= 1 err= expected space in input to match format a= b= 0
PS D:\code\GO\test> go run .\main.go
11 * 11
n= 2 err= <nil> a= b= 11
PS D:\code\GO\test> go run .\main.go
11 * 11
n= 2 err= <nil> a= b= 11
PS D:\code\GO\test> go run .\main.go
222 222
n= 1 err= input does not match format a= b= 0
PS D:\code\GO\test> go run .\main.go
11 * 11
n= 2 err= <nil> a= b= 11
PS D:\code\GO\test> go run .\main.go
11 * 11
n= 2 err= <nil> a= b= 11
test7 格式的变量之间没有空格
哪怕格式中没有空格也要至少有一个空格
func scanf_test() {
var a, b int
n, err := fmt.Scanf("%d%d", &a, &b)
fmt.Println("n=", n, "err=", err, "a=", "b=", b)
}
测试结果
PS D:\code\GO\test> go run .\main.go
11111
n= 1 err= unexpected newline a= b= 0
PS D:\code\GO\test> go run .\main.go
11 11
n= 2 err= <nil> a= b= 11
源码剖析
调用流程
fmt
中Scan
、Scanln
、Scanf
,分别指向了Fscan
、Fscanln
、Fscanf
三个函数。传入的第一个参数是用户标准输入作为io
的输入。
// C:\Program Files\Go\src\fmt\scan.go
// 扫描扫描从标准输入读取的文本,将连续的空格分隔值存储到连续的参数中。换行算作空格。
// 它返回成功扫描的项目数。如果这小于参数的数量,err将报告原因。
func Scan(a ...any) (n int, err error) {
return Fscan(os.Stdin, a...)
}
// Scanln类似于Scan,但在换行处停止扫描,并且在最后一项之后必须有换行符或EOF。
func Scanln(a ...any) (n int, err error) {
return Fscanln(os.Stdin, a...)
}
// Scanf扫描从标准输入读取的文本,将空格分隔的连续值存储到由格式决定的连续参数中。
// 它返回成功扫描的项目数。如果这小于参数的数量,err将报告原因。
// 输入中的换行符必须与格式中的换行符相匹配。
// 唯一的例外是: %c总是扫描输入中的下一个符文,即使它是空格(或制表符等)或换行符。
func Scanf(format string, a ...any) (n int, err error) {
return Fscanf(os.Stdin, format, a...)
}
而这三个函数最终都是使用了newScanState
函数,其中Scan
和Scanln
都使用了s
的doScan
方法,而Scanf
使用的是doScanf
方法。
// Fscan扫描从r读取的文本,将连续的空格分隔值存储为连续的参数。换行符算作空格。
// 它返回成功扫描的项目数。如果这小于参数的数量,err将报告原因。
func Fscan(r io.Reader, a ...any) (n int, err error) {
s, old := newScanState(r, true, false) // 通过后两个变量区分对空格和回车的处理
n, err = s.doScan(a)
s.free(old)
return
}
// Fscanln类似于Fscan,但在换行处停止扫描,并且在最后一项之后必须有换行符或EOF。
func Fscanln(r io.Reader, a ...any) (n int, err error) {
s, old := newScanState(r, false, true)
n, err = s.doScan(a)
s.free(old)
return
}
// Fscanf扫描从r读取的文本,将空格分隔的连续值存储到由格式决定的连续参数中。
// 它返回成功解析的项数。
// 输入中的换行符必须与格式中的换行符相匹配。
func Fscanf(r io.Reader, format string, a ...any) (n int, err error) {
s, old := newScanState(r, false, false)
n, err = s.doScanf(format, a)
s.free(old)
return
}
newScanState
,后面两个布尔型的参数分别代表截止标志,Scan
的截止标志是空格,因此将nlIsSpace
设置为true,Scanln
将nlIsEnd
设置为true,而Scanf
两个都是false。
ssFree是一个sync.pool
对象,sync.pool
是Go1.3
发布的一个特性,它是一个临时对象存储池,能减少重复的对象创建,降低GC
的压力,这里不作深究。
// newScanState分配一个新的ss结构或抓取一个缓存的ss结构。
func newScanState(r io.Reader, nlIsSpace, nlIsEnd bool) (s *ss, old ssave) {
s = ssFree.Get().(*ss)
if rs, ok := r.(io.RuneScanner); ok {
s.rs = rs
} else {
s.rs = &readRune{reader: r, peekRune: -1}
}
s.nlIsSpace = nlIsSpace
s.nlIsEnd = nlIsEnd
s.atEOF = false
s.limit = hugeWid
s.argLimit = hugeWid
s.maxWid = hugeWid
s.validSave = true
s.count = 0
return
}
ss
类型的定义如下,用于处理IO
type ss struct {
rs io.RuneScanner // 输入读取位置
buf buffer // 令牌累加器
count int // 到目前位置已经处理的字符
atEOF bool // 是否到达文件尾
ssave
}
ssave
结构体中ssave保存在递归扫描中需要保存和恢复的ss部分,主要是一些控制和状态标识。
// ssave保存在递归扫描中需要保存和恢复的ss部分。
type ssave struct {
validSave bool // 是或曾经是实际ss的一部分。
nlIsEnd bool // 遇到换行符是否终止扫描
nlIsSpace bool // 遇到空格是否终止扫描
argLimit int // 此参数ss.count的最大值 argLimit <= limit
limit int // ss.count的最大值
maxWid int // 此参数的宽度
}
doScan的处理
在doScan
中,会调用scanOne
方法,逐个扫描所需参数。scanOne
方法的第一个参数被指定为’v’,参考Go中的占位符,%v代表普通占位符,表示使用变量的默认格式,所以这个’v’代表按照变量的默认类型处理。
如果是Scanln
则会进一步找到换行符,如果在所有参数扫描完成后,出现了换行、文件尾和空格以外的字符,则会抛出"expected newline"
异常。
// doScan在没有格式字符串的情况下执行真正的扫描工作。
func (s *ss) doScan(a []any) (numProcessed int, err error) {
defer errorHandler(&err)
for _, arg := range a {
s.scanOne('v', arg)
numProcessed++
}
// Check for newline (or EOF) if required (Scanln etc.).
if s.nlIsEnd {
for {
r := s.getRune()
if r == '\n' || r == eof {
break
}
if !isSpace(r) {
s.errorString("expected newline")
break
}
}
}
return
}
在scanOne
中,会首先尝试调用对象本身的Scan
方法,也就是说,如果我们自定义的对象中实现了Scan
接口,也是可以直接scanOne
调用从标准输入中赋值的。否则的话,如果是内置变量,则会进行相应的处理,其他则会报错。
// scanOne扫描一个值,从参数类型派生扫描程序。
func (s *ss) scanOne(verb rune, arg any) {
s.buf = s.buf[:0]
var err error
// If the parameter has its own Scan method, use that.
if v, ok := arg.(Scanner); ok {
err = v.Scan(s, verb)
if err != nil {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
s.error(err)
}
return
}
switch v := arg.(type) {
case *bool:
*v = s.scanBool(verb)
case *complex64:
*v = complex64(s.scanComplex(verb, 64))
case *complex128:
*v = s.scanComplex(verb, 128)
case *int:
*v = int(s.scanInt(verb, intBits))
...
default:
s.errorString("can't scan type: " + val.Type().String())
}
}
}
int类型的处理
int
类型会调用scanInt
方法进行解析,verb变量根据前面被指定为’v’, intBits指的是变量长度(位),在64位机器上为64,即8个字节。
case *int:
*v = int(s.scanInt(verb, intBits))
在scanInt
方法中会根据verb的具体值进行相应的处理。如果是’c’则直接返回二进制表示。
之后会使用方法跳过空格。并判断是否到文件尾。getBase
方法可以根据参数verb
直接确定数字是几进制。但如果是’v’的话则需要进一步判断。
在进行预处理之后,如果是’v’ 的话,会先调用scanBasePrefix
方法处理,再通过scanNumber
方法和ParseInt
方法进行进一步计算。最后判断一下是否越界后返回。
// scanInt返回下一个标记表示的整数的值,检查是否溢出。任何错误都存储在s.err中。
func (s *ss) scanInt(verb rune, bitSize int) int64 {
if verb == 'c' { // 二进制类型
return s.scanRune(bitSize)
}
s.SkipSpace() // 跳过空格,所谓的前缀空格处理
s.notEOF() // 判断一下是否在文件尾
base, digits := s.getBase(verb) // 如果是非v类型,则可以根据verb类型值确定是多少进制
haveDigits := false
if verb == 'U' { // Unicode格式处理
if !s.consume("U", false) || !s.consume("+", false) {
s.errorString("bad unicode format ")
}
} else {
s.accept(sign) // 对正负号的特殊处理
if verb == 'v' {
base, digits, haveDigits = s.scanBasePrefix()
}
}
tok := s.scanNumber(digits, haveDigits)
i, err := strconv.ParseInt(tok, base, 64)
if err != nil {
s.error(err)
}
n := uint(bitSize) // 检查是否越界
x := (i << (64 - n)) >> (64 - n) // 将 i 向左移动 64 - n 位,然后再向右移动相同数量的位数。这将清除 i 中在 n 个最低有效位之外的位。结果被赋值给 x。
if x != i { // 检查 x 是否等于 i。如果它们不相等,就表示 i 的值在 n 个最低有效位之外有一些位被设置了,这表明发生了整数溢出。
s.errorString("integer overflow on token " + tok)
}
return i
}
SkipSpace
会尝试跳过多于的空格并根据标志位, 如果遇到’\r\n’(回车换行)会直接当做换行处理,否则将’\r’当做其他字符处理。对于’\n’(换行)则会根据直接的标志位处理,如果是扫描到空格即终止(s.nlIsSpace==true
)则会忽略换行,否则抛出unexpected newline
错误,这也是Scan
能跳过换行继续读取的原因。
// SkipSpace为扫描方法提供了跳过空格和换行符的能力,以符合由格式字符串和Scan/Scanln设置的当前扫描模式。
func (s *ss) SkipSpace() {
for {
r := s.getRune()// 这里的get操作应该是取出字符
if r == eof {
return
}
if r == '\r' && s.peek("\n") { // 处理回车换行
continue
}
if r == '\n' { // 处理换行
if s.nlIsSpace {
continue
}
s.errorString("unexpected newline")
return
}
if !isSpace(r) { // 终止条件
s.UnreadRune() // 会将计数器减一,并且将在文件尾的表示设为否
break
}
}
}
scanBasePrefix
报告整数是否以基数前缀开头,并返回基数、数字字符串以及是否找到零。所谓基数前缀即0x
、0o
、0b
等用于标识数字是几进制数的标识。
这里base统一返回0,猜测是因为数据是带前缀的,所以需要后续函数特殊处理。至于为什么不在这里把前缀也去掉,猜测一方面是架构设计问题,需要调用consume将基数前缀写入buf,另外一方面也可能是为了方便打印?
// scanBasePrefix报告整数是否以基数前缀开头,并返回基数、数字字符串以及是否找到零。只有当动词为%v时才调用它。
func (s *ss) scanBasePrefix() (base int, digits string, zeroFound bool) {
if !s.peek("0") { // peek的操作应该是取出字符后又放回,不是0的话就没有基数前缀认为是十进制
return 0, decimalDigits + "_", false
}
s.accept("0") // accept实际会调用consume,等价于 s.consume('0', true)
// consume的操作是判断下一字符是否在ok字符串中,如果在就将其写入s的buf
// Special cases for 0, 0b, 0o, 0x.
switch { // 这里base会统一返回0,让后续程序处理
case s.peek("bB"): // 二进制
s.consume("bB", true)
return 0, binaryDigits + "_", true
case s.peek("oO"): // 八进制
s.consume("oO", true)
return 0, octalDigits + "_", true
case s.peek("xX"): // 十六进制
s.consume("xX", true)
return 0, hexadecimalDigits + "_", true
default: // 注意,其他字母也会统一当8进制处理
return 0, octalDigits + "_", true
}
}
所以使用scanf和scan时,如果你需要的是一个十进制数,就前面不要带0,否则会报错或被当成8进制
eg:
PS D:\code\GO\test> go run .\main.go
083
n= 1 err= expected newline a= 0
PS D:\code\GO\test> go run .\main.go
0085
n= 1 err= expected newline a= 0
PS D:\code\GO\test> go run .\main.go
055
n= 1 err= a= 45
这里所谓的数字字符串指的是可接受的字符的集合。具体而言
const (
binaryDigits = "01"
octalDigits = "01234567"
decimalDigits = "0123456789"
hexadecimalDigits = "0123456789aAbBcCdDeEfF"
sign = "+-"
period = "."
exponent = "eEpP"
)
scanNumber
用于扫描数字区域,将数字写入buf并返回buf。
// scanNumber返回从这里开始的指定数字的数字字符串。
func (s *ss) scanNumber(digits string, haveDigits bool) string {
if !haveDigits { // 非0开头要先检查一下首个数字是否合规
s.notEOF()
if !s.accept(digits) { // 如果是异常值则抛出错误
s.errorString("expected integer")
}
}
for s.accept(digits) { // 将符合规则的数写入s.buf,遇到不符合的则返回,代码较为简单,不进一步展开
// 因此即使输入有异常, 异常值前面的数字也会被解析
}
return string(s.buf)
}
/* 测试
PS D:\code\GO\test> go run .\main.go
sdd
n= 0 err= expected integer a= 0
PS D:\code\GO\test> go run .\main.go
0ss
n= 1 err= expected newline a= 0
PS D:\code\GO\test> go run .\main.go
111s
n= 1 err= expected newline a= 111
*/
strconv.ParseInt
用于将输入转换为整数, 首先处理正负号,之后调用ParseUint
进一步处理数据部分。处理完成后进行越界判断,并根据符号决定是否取负。
// ParseInt以给定的基数(0,2到36)和位大小(0到64)解释字符串s,并返回相应的值i。
// 字符串可以以前导符号开头:“+”或“-”。
// 如果基参数为0,则符号后面的字符串前缀(如果存在)表示真基:2表示“0b”,8表示“0”或“0o”,16表示“0x”,否则为10。
// 此外,仅对于参数基0,允许使用[integer literals]的Go语法定义的下划线字符。
// bitSize参数指定结果必须适合的整数类型。位大小0、8、16、32和64对应于int、int8、int16、int32和int64。
// itSize低于0或高于64,则返回错误。
// seInt返回的错误具有具体类型*NumError,并包含err.Num=s。
// 如果s为空或包含无效数字,则err.err=ErrStax,返回值为0;
// 如果与s对应的值不能用给定大小的带符号整数表示,则err.err=ErrRange,返回的值是相应位size和符号的最大大小整数。
func ParseInt(s string, base int, bitSize int) (i int64, err error) {
const fnParseInt = "ParseInt"
if s == "" {
return 0, syntaxError(fnParseInt, s)
}
// Pick off leading sign.
s0 := s
neg := false
if s[0] == '+' { // 符号处理
s = s[1:]
} else if s[0] == '-' {
neg = true
s = s[1:]
}
// Convert unsigned and check range.
var un uint64
un, err = ParseUint(s, base, bitSize) // 处理数据部分
if err != nil && err.(*NumError).Err != ErrRange { // 异常处理
err.(*NumError).Func = fnParseInt
err.(*NumError).Num = cloneString(s0)
return 0, err
}
if bitSize == 0 { // 传入0代表按Int类型处理
bitSize = IntSize
}
cutoff := uint64(1 << uint(bitSize-1)) // 左移得到取值范围的最大值
if !neg && un >= cutoff { // 越界处理,返回取值范围最大值,并报错
return int64(cutoff - 1), rangeError(fnParseInt, s0)
}
if neg && un > cutoff {
return -int64(cutoff), rangeError(fnParseInt, s0)
}
n := int64(un)
if neg {
n = -n
}
return n, nil
}
ParseUint
用来处理数值部分,并进行进制转化。
// ParseUint类似于ParseInt,但适用于无符号数字。不允许使用符号前缀。
func ParseUint(s string, base int, bitSize int) (uint64, error) {
const fnParseUint = "ParseUint"
if s == "" {
return 0, syntaxError(fnParseUint, s)
}
base0 := base == 0
s0 := s
switch {
case 2 <= base && base <= 36: // 不合理进制,不做操作
// valid base; nothing to do
case base == 0: // 0代表传入的是一个有基数前缀的字符串,要处理并判断是几进制数
// Look for octal, hex prefix.
base = 10
if s[0] == '0' {
switch {
case len(s) >= 3 && lower(s[1]) == 'b':
base = 2
s = s[2:]
case len(s) >= 3 && lower(s[1]) == 'o':
base = 8
s = s[2:]
case len(s) >= 3 && lower(s[1]) == 'x':
base = 16
s = s[2:]
default:
base = 8
s = s[1:]
}
}
default:
return 0, baseError(fnParseUint, s0, base)
}
if bitSize == 0 { // 位数处理
bitSize = IntSize
} else if bitSize < 0 || bitSize > 64 {
return 0, bitSizeError(fnParseUint, s0, bitSize)
}
// Cutoff 是使得 cutoff*base > maxUint64成立的最小值.
// Use compile-time constants for common cases.
var cutoff uint64
switch base { // 上界处理
case 10:
cutoff = maxUint64/10 + 1
case 16:
cutoff = maxUint64/16 + 1
default:
cutoff = maxUint64/uint64(base) + 1
}
maxVal := uint64(1)<<uint(bitSize) - 1
underscores := false
var n uint64
for _, c := range []byte(s) { // 也是根据几进制的原理进行转换的,从高位往后
var d byte
switch {
case c == '_' && base0:
underscores = true
continue
case '0' <= c && c <= '9':
d = c - '0'
case 'a' <= lower(c) && lower(c) <= 'z':
d = lower(c) - 'a' + 10
default:
return 0, syntaxError(fnParseUint, s0)
}
if d >= byte(base) {
return 0, syntaxError(fnParseUint, s0)
}
if n >= cutoff { // 这样可以提前判断出越界
// n*base overflows
return maxVal, rangeError(fnParseUint, s0)
}
n *= uint64(base)
n1 := n + uint64(d)
if n1 < n || n1 > maxVal {
// n+d overflows
return maxVal, rangeError(fnParseUint, s0)
}
n = n1
}
if underscores && !underscoreOK(s0) { // 数字中下划线的处理,是否接受下划线
return 0, syntaxError(fnParseUint, s0)
}
return n, nil
}
至此,转换的过程分析完毕。
string类型的处理
string类型的处理相对简单一些,调用convertString
处理,verb
同样传入’v’,跳过空格后根据verb处理字符串,'v’的时候调用的是s.token
函数。
// onvertString返回由之后输入字符表示的字符串。
// 输入的格式由verb决定。
func (s *ss) convertString(verb rune) (str string) {
if !s.okVerb(verb, "svqxX", "string") {
return ""
}
s.SkipSpace()
s.notEOF()
switch verb {
case 'q':
str = s.quotedString()
case 'x', 'X':
str = s.hexString()
default:
str = string(s.token(true, notSpace)) // %s and %v just return the next word
}
return
}
s.token
函数的第一个参数是是否跳过空格,如果为true的话,会再调用一遍s.SkipSpace
(这里不是很明白为什么要重复调用)。第二个参数是一个函数变量,用来判断是否是可用字符。传入的noSapce
函数实际就是调用了isSpace
函数并取反,所以遇到空格时会返回false。因此即使要写入的变量是string类型的,调用Scanlan
和Scan
时同样遇到空格时就会返回。
// token返回输入中的下一个空格分隔字符串。对于Scanln,它在换行符处停止。Scan,换行符被视为空格。
func (s *ss) token(skipSpace bool, f func(rune) bool) []byte {
if skipSpace { // 重新跳过前导空格
s.SkipSpace()
}
// 读取到空格或换行就返回
for {
r := s.getRune()
if r == eof {
break
}
if !f(r) {
s.UnreadRune()
break
}
s.buf.writeRune(r)
}
return s.buf
}
测试
func scanln_test() {
var a string
n, err := fmt.Scanln(&a)
fmt.Println("n=", n, "err=", err, "a=", a)
}
PS D:\code\GO\test> go run .\main.go
asdas sd
n= 1 err= expected newline a= asdas
func scanln_test() {
var a, b string
n, err := fmt.Scanln(&a, &b)
fmt.Println("n=", n, "err=", err, "a=", a, "b=", b)
}
PS D:\code\GO\test> go run .\main.go
zxcxz sadas
n= 2 err= <nil> a= zxcxz b= sadas
PS D:\code\GO\test> go run .\main.go
asdas
n= 1 err= unexpected newline a= asdas b=
如何读入完整的一行?
那么,如果要类似于python的 input()
或者c 中的gets()
忽略空格读入完整的一行要如何操作呢?答案是 bufio
包的 NewReader()
函数和 ReadString()
方法。例子如下
package main
import (
"bufio"
"fmt"
"os"
)
func bufio_test(){
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter text: ")
input, err := reader.ReadString('\n') // 括号中的字符用于指定截止标识
if err != nil {
fmt.Println("An error occurred while reading input:", err)
return
}
fmt.Println("Your input:", input)
}
在上面的代码中,我们使用 bufio.NewReader() 函数创建了一个新的读取器,它可以从标准输入中读取数据。接下来,我们使用 ReadString(‘\n’) 方法读取了一行输入,并将其存储在变量 input 中。如果读取过程中出现错误,我们将输出错误信息并退出程序。最后,我们将处理后的输入打印出来。
测试结果
PS D:\code\GO\test> go run .\main.go
Enter text: a123 asd 999
Your input: a123 asd 999
doScanf的处理
(看起来和doScan的注释风格都不一样,应该不是一个人写的)
doScanf
的主要逻辑是根据用户输入的格式化字符串format
遍历来处理数据。在循环中,首先调用了advance
来对格式和用户输入中的空格即其他字符进行匹配,直到遇到%,之后根据百分号后面的内容来决定具体动作。
// doScanf在使用格式字符串进行扫描时执行实际工作。目前只处理指向基本类型的指针
func (s *ss) doScanf(format string, a []any) (numProcessed int, err error) {
defer errorHandler(&err)
end := len(format) - 1
// We process one item per non-trivial format
for i := 0; i <= end; {
w := s.advance(format[i:]) // 这里会逐个处理字符直到遇到百分号
if w > 0 {
i += w // 根据返回值移动指针
continue
}
// Either we failed to advance, we have a percent character, or we ran out of input.
if format[i] != '%' { // 没有%, 抛出异常
// Can't advance format. Why not?
if w < 0 {
s.errorString("input does not match format")
}
// Otherwise at EOF; "too many operands" error handled below
break
}
i++ // % is one byte,跳过百分号,查看下一项
// do we have 20 (width)?
var widPresent bool
s.maxWid, widPresent, i = parsenum(format, i, end) // 处理百分号后面的数字
if !widPresent {
s.maxWid = hugeWid
}
c, w := utf8.DecodeRuneInString(format[i:]) // 获取%后面的第一个非数字字符
i += w
if c != 'c' { // 如果不是%c则跳过用户输入的空格
s.SkipSpace()
}
if c == '%' { // %%的意思是打印一个百分号
s.scanPercent()// 这个函数的作用就是从输入中扫描一个百分号
continue // Do not consume an argument.
}
s.argLimit = s.limit
if f := s.count + s.maxWid; f < s.argLimit {
s.argLimit = f
}
if numProcessed >= len(a) { // out of operands 这里判断一下已经处理的变量数,如果已经比要接收的参数还多,那么抛出异常
s.errorString("too few operands for format '%" + format[i-w:] + "'")
break
}
arg := a[numProcessed] // 获得下一个需要为其赋值的变量
s.scanOne(c, arg) // 这里的c有fomat中的用户输入决定,而不再是固定的'v'
numProcessed++ // 处理变量数目的计数器+1
s.argLimit = s.limit
}
if numProcessed < len(a) { // 循环结束后,依旧有变量没有得到赋值,说明输入过多,抛出异常
s.errorString("too many operands")
}
return
}
这里我们进一步查看advance
方法的处理逻辑,其他函数相对简单或在前面已经介绍,因此不再赘述。
// Advance确定输入中的下一个字符是否与格式中的字符匹配。它返回以该格式消耗的字节数(sic)。
// 输入或格式中的所有空格字符都表现为单个空格。
// 换行符是特殊的:格式中的换行符必须与输入中的换行符匹配,反之亦然。这个例程还处理%%的情况。
// 如果返回值为0,则说明格式以%开头(后面没有%)或输入为空。如果是负值,则说明输入的字符串不匹配。
func (s *ss) advance(format string) (i int) {
for i < len(format) {
fmtc, w := utf8.DecodeRuneInString(format[i:]) // 对格式字符串进行utf-8解码, 返回解码结果及其字节数
// 空格处理。在此注释的其余部分,“空格”指的是换行符以外的空格。
// 格式中的换行符匹配零或多个空格的输入,然后是换行符或输入结束符。
// 换行符之前格式中的空格被折叠成换行符。换行符后的格式中的空格匹配对应输入换行符后的零个或多个空格。
// 格式中的其他空格匹配一个或多个空格或输入结束的输入。
if isSpace(fmtc) { // 如果这个字符是空格
newlines := 0
trailingSpace := false
for isSpace(fmtc) && i < len(format) { // 尝试处理多个空格
if fmtc == '\n' { // 处理格式中的换行符
newlines++
trailingSpace = false
} else {
trailingSpace = true
}
i += w
fmtc, w = utf8.DecodeRuneInString(format[i:])
}
for j := 0; j < newlines; j++ { // 要求输入中的换行与格式中匹配,否则抛出异常
inputc := s.getRune()
for isSpace(inputc) && inputc != '\n' {
inputc = s.getRune()
}
if inputc != '\n' && inputc != eof {
s.errorString("newline in format does not match input")
}
}
if trailingSpace {
inputc := s.getRune()
if newlines == 0 {
// 如果尾部有单独存在的空格(后面没有换行),则至少要消耗一个空格
if !isSpace(inputc) && inputc != eof {
s.errorString("expected space in input to match format")
}
if inputc == '\n' {
s.errorString("newline in input does not match format")
}
}
for isSpace(inputc) && inputc != '\n' {
inputc = s.getRune()
}
if inputc != eof {
s.UnreadRune()
}
}
continue
}
// Verbs. 对于其他字符的处理
if fmtc == '%' {
// % 在格式字符串尾,异常
if i+w == len(format) {
s.errorString("missing verb: % at end of format string")
}
// %% 两个百分号实际上指的是要求输入一个百分号
nextc, _ := utf8.DecodeRuneInString(format[i+w:]) // will not match % if string is empty
if nextc != '%' { // 下一个不是百分号,则是一个格式化输入,返回之
return
}
i += w // skip the first %
}
// Literals.
inputc := s.mustReadRune() // 必须读入一个字符,否则抛出异常
if fmtc != inputc { // 格式要求的字符与用户输入的字符不一致,抛出异常
s.UnreadRune()
return -1
}
i += w
}
return
}
总结
Scan
和Scanln
都会把空格当做多个变量间的分割符。- 在读取变量之前,
Scan
和Scanln
会跳过空格,读取变量时,遇到空格结束读取。 Scanln
会把换行当做结束符,而Scan
会视作空格处理。Scanf
的fomat
和用户输入中连续的空格都会被当做一个处理,其他字符则要求严格一致。%c
在Scanf
中会被特殊处理,这是唯一一种能够把用户输入的空格读入到变量中的情况。