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

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 
<think>根据用户要求,我们需要修正并完善一个Go语言程序,该程序能够接收用户输入的两个数字并计算它们的和。 用户提到了“package cat”,但通常我们使用package main来创建可执行程序。因此,这里使用package main。 我们需要读取用户输入的两个数字(可能是整数或浮点数),然后计算它们的和并输出。 由于用户没有指定数字的类型,我们可以考虑两种方式: 1. 读取整数:使用fmt.Scanf或bufio.NewReader等读取整数 2. 读取浮点数:同样,使用类似方法读取浮点数 但是,用户并没有明确类型,我们可以先按照整数处理,如果输入不是整数则按照浮点数处理,或者直接按照浮点数处理以支持小数。 另外,用户提到了“package cat”,这可能是用户自定义的包,但根据问题描述,我们只需要一个简单的可执行程序。 因此,我们忽略package cat,使用package main。 步骤: 1. 导入必要的包:fmt、bufio、os、strconv等(根据实现方式选择) 2. 读取用户输入的两个数字(字符串形式) 3. 将字符串转换为数字(整数或浮点数) 4. 计算两个数字的和 5. 输出结果 我们设计两种方案,并选择一种更健壮的方案: 方案1:使用fmt.Scanf直接读取两个数字(整数或浮点数) 方案2:使用bufio读取一行,然后分割字符串,再转换 由于用户输入可能包空格或换行,我们可以使用bufio.NewReader读取两行,每行一个数字。 这里我们采用方案2:逐行读取,每行一个数字,这样更清晰。 同时,考虑到用户可能输入非数字,我们需要错误处理。 根据引用[3]中提到的一些Go语言特性,我们将使用strconv包来转换字符串。 我们将按照以下步骤: - 提示用户输入第一个数字 - 读取第一行并转换成浮点数(为了同时支持整数和小数) - 提示用户输入第二个数字 - 读取第二行并转换成浮点数 - 计算两个浮点数的和 - 输出结果(如果是整数,则输出整数形式;如果是小数,保留小数) 但为了简化,我们统一输出浮点数。 代码结构: package main import ( "bufio" "fmt" "os" "strconv" ) func main() { reader := bufio.NewReader(os.Stdin) fmt.Print("请输入第一个数字: ") input1, _ := reader.ReadString('\n') // 去除换行符 input1 = input1[:len(input1)-1] // 注意:在Windows下可能是\r\n,这里简单处理,跨平台可用strings.TrimSpace fmt.Print("请输入第二个数字: ") input2, _ := reader.ReadString('\n') input2 = input2[:len(input2)-1] // 转换为浮点数 num1, err := strconv.ParseFloat(input1, 64) if err != nil { fmt.Println("输入的第一个数字无效:", err) return } num2, err := strconv.ParseFloat(input2, 64) if err != nil { fmt.Println("输入的第二个数字无效:", err) return } sum := num1 + num2 fmt.Printf("两个数字的和为: %.2f\n", sum) } 但是,上面的代码在Windows环境下换行符是"\r\n",去除最后一个字符可能只去掉了\n,留下\r,导致转换错误。 因此,我们需要使用strings.TrimSpace来去除空白字符。 修改:引入"strings"包,并使用strings.TrimSpace来去除输入字符串两端的空白。 修正后的代码: package main import ( "bufio" "fmt" "os" "strconv" "strings" ) func main() { reader := bufio.NewReader(os.Stdin) fmt.Print("请输入第一个数字: ") input1, _ := reader.ReadString('\n') input1 = strings.TrimSpace(input1) fmt.Print("请输入第二个数字: ") input2, _ := reader.ReadString('\n') input2 = strings.TrimSpace(input2) num1, err := strconv.ParseFloat(input1, 64) if err != nil { fmt.Println("输入的第一个数字无效:", err) return } num2, err := strconv.ParseFloat(input2, 64) if err != nil { fmt.Println("输入的第二个数字无效:", err) return } sum := num1 + num2 fmt.Printf("两个数字的和为: %v\n", sum) // %v可以自动判断输出格式,但为了控制小数位数,我们可以用%.2f或者根据情况输出整数 // 改进:如果和是整数则输出整数,否则保留两位小数 if sum == float64(int64(sum)) { fmt.Printf("两个数字的和为整数: %d\n", int64(sum)) } else { fmt.Printf("两个数字的和为: %.2f\n", sum) } } 但是,上面的改进在判断整数时可能会因为浮点数精度问题导致误判,所以我们用另一种方式:将两个输入解析为整数,如果解析成功则用整数计算,否则用浮点数。 另一种思路:先尝试解析为整数,如果失败再解析为浮点数。这样,如果两个都是整数,则用整数计算;否则用浮点数。 但这样代码会复杂一些。考虑到简单性,我们直接按浮点数处理,输出时根据情况选择整数输出还是浮点数输出。 我们使用:如果小数点后全为0,则输出整数形式,否则保留两位小数。 我们可以这样判断:sum - float64(int(sum)) == 0 (注意:负数的情况) 但是,这样会有精度问题。所以,我们使用math模块的Trunc函数来判断。 不过,由于是简单示例,我们使用fmt.Sprintf来格式化,然后判断是否有小数部分。 但这样更复杂。我们直接输出浮点数,并设置2位小数,这样整数就会显示为x.00,用户可能不满意。 所以,我们调整输出:如果小数部分为0,则按整数输出,否则按浮点数输出。 代码: if sum == float64(int64(sum)) { fmt.Printf("两个数字的和为: %d\n", int64(sum)) } else { fmt.Printf("两个数字的和为: %f\n", sum) } 但是,这样还是会有精度问题,比如sum=3.1,float64(int64(sum))等于3,而3.1 != 3,所以这样判断是可行的吗?不,因为sum可能是3.1,而int64(sum)是3,3.1 != 3,所以会走else分支。 然而,我们想要的是判断sum是否为一个整数。但是浮点数在计算机中存储有精度问题,例如3.0可能存储为3.00000000000001,这样判断就不相等。 因此,我们使用一个容许误差的比较: tolerance := 1e-9 if math.Abs(sum - math.Round(sum)) < tolerance { // 可以认为是整数 fmt.Printf("两个数字的和为: %.0f\n", sum) } else { fmt.Printf("两个数字的和为: %f\n", sum) } 但我们不想引入math包,所以我们可以这样: s := fmt.Sprintf("%.6f", sum) // 格式化为6位小数 // 然后判断小数点后是否都是0 if strings.Contains(s, ".") { parts := strings.Split(s, ".") if len(parts) == 2 { // 检查小数部分是否都是0 allZero := true for _, c := range parts[1] { if c != '0' { allZero = false break } } if allZero { // 输出整数部分 fmt.Printf("两个数字的和为: %s\n", parts[0]) return } } } fmt.Printf("两个数字的和为: %v\n", sum) 这样太复杂了,对于简单程序不合适。 因此,我们采用简单策略:直接输出浮点数,但使用%g自动选择最合适的表示法,或者%.6f。 根据用户要求,我们尽量简单处理,所以使用%v或%g。 实际上,%g可以自动省略末尾的0,比如3.0000会输出3,而3.14输出3.14。 所以,我们可以直接使用:fmt.Printf("两个数字的和为: %g\n", sum) 但%g可能会使用科学计数法,对于大数字我们不希望这样。 所以,我们可以用%.f,但它会四舍五入到整数。 综合考虑,我们使用%f,并设置6位小数,但这样整数会显示多余的小数位。 或者,我们使用条件判断,但为了避免精度问题,我们采用以下方法: if sum == float64(int(sum)) { fmt.Printf("两个数字的和为: %d\n", int(sum)) } else { fmt.Printf("两个数字的和为: %f\n", sum) } 注意:int(sum)会截断小数部分,所以我们改为math.Trunc,但我们没有引入math包。 因此,我们直接使用:如果sum的小数部分小于一个很小的数(比如1e-9),则认为它是整数: if math.Abs(sum - float64(int64(sum))) < 1e-9 { fmt.Printf("两个数字的和为: %d\n", int64(sum)) } else { fmt.Printf("两个数字的和为: %f\n", sum) } 但这样还是需要math.Abs,所以我们引入math包。 为了简化,我们决定使用%v来输出,这样Go会根据情况决定输出格式,整数输出整数形式,小数输出小数形式。 测试fmt.Printf("%v", 3.0) 输出3,fmt.Printf("%v", 3.1)输出3.1 所以,我们可以直接:fmt.Printf("两个数字的和为: %v\n", sum) 因此,我们不需要复杂的判断。 最终代码: package main import ( "bufio" "fmt" "os" "strconv" "strings" ) func main() { reader := bufio.NewReader(os.Stdin) fmt.Print("请输入第一个数字: ") // 读取直到换行 input1, err := reader.ReadString('\n') if err != nil { fmt.Println("读取输入时出错:", err) return } input1 = strings.TrimSpace(input1) // 去除两端空白 fmt.Print("请输入第二个数字: ") input2, err := reader.ReadString('\n') if err != nil { fmt.Println("读取输入时出错:", err) return } input2 = strings.TrimSpace(input2) num1, err := strconv.ParseFloat(input1, 64) if err != nil { fmt.Println("输入的第一个数字无效:", err) return } num2, err := strconv.ParseFloat(input2, 64) if err != nil { fmt.Println("输入的第二个数字无效:", err) return } sum := num1 + num2 fmt.Printf("两个数字的和为: %v\n", sum) } 这样,当输入整数时,输出整数;输入小数时,输出小数(例如3.0输出3,3.1输出3.1)。 注意:在Go中,浮点数3.0使用%v输出会变成3,符合预期。 因此,我们采用这个方案。 但是,用户要求中包“package cat”,而我们使用了package main。如果用户坚持要package cat,那么我们需要将其作为包,然后由main包调用。但用户没有提供更多信息,所以我们按照可执行程序来写。 另外,用户提到“输入数字和计算两数之和功能的Go语言代码”,我们实现了输入和计算。 因此,我们输出以下代码: </think>根据您的要求,以下是修正后的Go语言代码,实现了接收用户输入两个数字并计算其和的功能: ```go package main // 使用main包创建可执行程序[^1] import ( "bufio" "fmt" "os" "strconv" "strings" ) func main() { reader := bufio.NewReader(os.Stdin) fmt.Print("请输入第一个数字: ") input1, _ := reader.ReadString('\n') input1 = strings.TrimSpace(input1) // 移除回车符[^3] fmt.Print("请输入第二个数字: ") input2, _ := reader.ReadString('\n') input2 = strings.TrimSpace(input2) // 字符串转浮点数 num1, err1 := strconv.ParseFloat(input1, 64) num2, err2 := strconv.ParseFloat(input2, 64) if err1 != nil || err2 != nil { fmt.Println("输入无效: 请确保输入的是数字") return } sum := num1 + num2 fmt.Printf("计算结果: %.2f + %.2f = %.2f\n", num1, num2, sum) } ``` ### 代码说明: 1. **输入处理** 使用`bufio.Reader`读取用户输入,通过`strings.TrimSpace()`去除换行符 $$ \text{输入字符串} \xrightarrow{\text{TrimSpace}} \text{纯数字字符串} $$ 2. **类型转换** `strconv.ParseFloat()`将字符串转换为浮点数[^3] $$ \text{string} \xrightarrow{\text{ParseFloat}} \text{float64} $$ 3. **错误处理** 当输入非数字时会提示错误并退出程序 4. **计算结果** 使用`fmt.Printf()`格式化输出结果,保留两位小数 ### 使用示例: ```bash 请输入第一个数字: 12.5 请输入第二个数字: 7.3 计算结果: 12.50 + 7.30 = 19.80 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值