在Go语言开发中,从标准输入持续读取整数并在输入结束时退出,同时不因为某一行格式不对就导致整个程序终止,是一个常见但容易写错的需求。标准输入可能来自键盘,也可能来自文件重定向或管道,当来源关闭时会收到EOF信号,我们需要在这个信号出现时干净地退出,而不是当作错误处理。

为什么不能直接用fmt.Scan
很多初学者会尝试用fmt.Scan或fmt.Scanf在循环里读取整数。这类函数确实会在遇到EOF时返回错误,但一旦某一行出现非整数内容,比如字母或符号,它会在内部留下未消费的输入,导致后续读取全部错位,甚至陷入死循环。更严重的是,fmt.Scan的错误信息并不区分是EOF还是格式错误,开发者很难针对性处理。
下面的代码展示了一种典型的错误写法:当输入中包含abc这样的字符串时,Scan会持续失败且无法继续往后读,因为错误的令牌一直留在缓冲区。
package main
import (
"fmt"
)
func main() {
var n int
for {
// 如果输入abc,Scan返回错误且n未被赋值
// 错误输入残留在缓冲区,下一次循环依旧失败
_, err := fmt.Scan(&n)
if err != nil {
// 这里无法区分EOF和格式错误
break
}
fmt.Println("read:", n)
}
}
从上面的例子可以看出,fmt.Scan适合交互式、格式严格受控的场景,不适合需要容错和按行处理的批量输入。我们需要一种能逐行读取、遇到坏数据能跳过并继续的机制。
使用bufio.Scanner按行读取
bufio.Scanner是Go标准库中用于按行或按词读取输入的工具。它默认以换行符分割,每次调用Scan方法都会尝试读取下一段内容,通过Text方法拿到字符串。当输入结束时会返回false,此时用Err方法可以判断是正常EOF还是真正的IO错误。
结合strconv.Atoi,我们可以把每一行转换成整数。如果转换失败,就记录错误并继续下一行,而不是中断整个流程。这种做法把EOF和格式错误完全分开:Scanner结束代表输入终止,Atoi错误代表单行数据不合法。
package main
import (
"bufio"
"fmt"
"os"
"strconv"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
lineNo := 0
for scanner.Scan() {
lineNo++
text := scanner.Text()
// 跳过空行
if text == "" {
continue
}
n, err := strconv.Atoi(text)
if err != nil {
// 格式错误:提示并继续,不退出
fmt.Printf("第%d行格式错误: %vn", lineNo, err)
continue
}
fmt.Printf("第%d行整数: %dn", lineNo, n)
}
// 循环结束,区分EOF和其他错误
if err := scanner.Err(); err != nil {
fmt.Println("读取输入时发生错误:", err)
} else {
fmt.Println("已到达输入末尾(EOF),正常退出")
}
}
上面的代码在真实运行中,如果你通过管道输入:
10 abc 20
程序会输出第2行格式错误,然后继续处理20,最后在输入关闭时打印正常退出信息。这种结构非常适合数据清洗任务,比如从日志中提取数字字段。
处理超大行与自定义分割
bufio.Scanner默认单行缓冲区最大为64K,若输入中包含超长行(例如一行几万个字符),Scan会返回错误。此时可以通过Buffer方法扩大容量。此外,若输入以空格而非换行分隔整数,也可以用ScanWords作为分割函数,但仍然建议在拿到每个令牌后用strconv.Atoi校验。
下面的示例展示如何扩容并改用按单词读取,同时保留格式错误的局部处理能力。
package main
import (
"bufio"
"bytes"
"fmt"
"os"
"strconv"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// 将最大令牌长度扩大到1MB
scanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024)
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
token := scanner.Text()
n, err := strconv.Atoi(token)
if err != nil {
fmt.Printf("跳过非整数令牌: %qn", token)
continue
}
fmt.Println("整数:", n)
}
if err := scanner.Err(); err != nil {
fmt.Println("扫描错误:", err)
}
}
这段代码使用bytes包仅为了说明Buffer的初始化方式,实际中直接传入切片即可。ScanWords会让Scanner以空白字符切分,因此即使所有数字写在同一行也能逐个处理。注意,如果输入源是严格每行一个整数的文件,按行读取比按词读取更容易定位错误行号。
与ioutil及流式读取的对比
除了Scanner,也有人用io.ReadAll一次性读入全部内容再拆分。这种方式在输入体积小时简单直观,但会占用大量内存,且无法在读取过程中实时处理。对于未知长度的标准输入,Scanner的流式特性更稳健。
下表简要对比三种常见方案:
| 方案 | 容错能力 | 内存占用 | 适用场景 |
|---|---|---|---|
| fmt.Scan循环 | 弱,易死循环 | 低 | 交互式严格输入 |
| bufio.Scanner | 强,可跳过错误行 | 低 | 管道、文件重定向 |
| io.ReadAll | 中,需自行切分 | 高 | 小体积已知输入 |
从工程角度看,Scanner在EOF处理和格式错误隔离上达到了最好的平衡。只要记住在循环结束后检查Err,就能正确区分正常结束与异常中断。
小结与最佳实践
要在Go中从标准输入读整数直到EOF并妥善处理格式错误,核心是用bufio.Scanner做行或词级别的读取,用strconv.Atoi做转换,并把转换错误当作可恢复事件。循环退出后务必调用scanner.Err来确认是否为EOF。
建议在生产代码中为错误行输出明确上下文(如行号或原始内容),并将合法整数送入后续处理逻辑,比如累加、过滤或写入通道。这样既能保证程序不崩溃,也能让调用方清楚知道哪些数据被丢弃。