深度解析Go中的用户输入获取(fmt.Scan fmt.Scanln fmt.Scanf),含多项测试及源码解读

本文详细分析了Go语言中fmt包的Scan、Scanln和Scanf函数处理标准输入的差异,包括对空格、换行符的处理,以及格式化输入的要求。通过示例测试展示了各种情况下函数的行为,并深入源码探讨了内部的工作原理,如如何扫描、解析和转换输入的数据。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

源码剖析

调用流程

fmtScanScanlnScanf,分别指向了FscanFscanlnFscanf三个函数。传入的第一个参数是用户标准输入作为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函数,其中ScanScanln都使用了sdoScan方法,而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,ScanlnnlIsEnd设置为true,而Scanf两个都是false。

ssFree是一个sync.pool对象,sync.poolGo1.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报告整数是否以基数前缀开头,并返回基数、数字字符串以及是否找到零。所谓基数前缀即0x0o0b等用于标识数字是几进制数的标识。

这里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类型的,调用ScanlanScan时同样遇到空格时就会返回。

// 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
}

总结

  • ScanScanln都会把空格当做多个变量间的分割符。
  • 在读取变量之前,ScanScanln会跳过空格,读取变量时,遇到空格结束读取。
  • Scanln会把换行当做结束符,而Scan会视作空格处理。
  • Scanffomat和用户输入中连续的空格都会被当做一个处理,其他字符则要求严格一致。
  • %cScanf中会被特殊处理,这是唯一一种能够把用户输入的空格读入到变量中的情况。
### Go语言 `fmt` 包下的输入函数及其条件判断 在Go语言中,`fmt` 包提供了多种用于处理输入输出的功能。对于用户输入的读取,主要使用的是 `fmt.Scan`, `fmt.Scanf`, 和 `fmt.Scanln` 函数[^1]。 针对给定的例子,在循环结构中利用 `fmt.Scanln` 获取用户输入并结合条件判断来控制程序流程: ```go package main import ( "fmt" ) func main() { var a int var b string for { fmt.Println("请输入一个整数和一个字符串(用空格分隔,并按回车结束):") if _, err := fmt.Scanln(&a, &b); err != nil || (len(b) == 0 && a == 0){ fmt.Println("错误:无效输入,请重新尝试.") continue // 如果遇到错误或者输入为空,则提示重试 } // 输出结果前先做一些简单的验证 if isValidInput(a, b) { break // 当满足特定条件时退出循环 } else { fmt.Println("注意: 输入不符合预期的要求!") } fmt.Println("整数:", a) fmt.Println("字符串:", b) } fmt.Println("最终接收的数据为:") fmt.Printf("整数=%d 字符串='%s'\n", a, b) } // 定义一个辅助函数来进行额外的输入有效性检查 func isValidInput(num int, str string) bool { return num >= 0 && len(str) > 0 // 假设这里定义有效输入的标准是非负整数加上非空字符串 } ``` 上述代码实现了如下特性: - 使用 `if` 结构捕获可能发生的任何输入错误。 - 对于无效输入(例如只按下Enter键),提供反馈信息并允许用户再次尝试。 - 添加了一个自定义的有效性检验逻辑,确保只有当输入符合预设规则时才会终止循环。 - 利用了 `fmt.Scanln` 的特点——不仅能够读取指定数量的参数还负责确认之后没有多余的未消费字符存在[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值