Go语言中处理标准输入:避免bufio.Scanner重复创建导致输入丢失

Go语言中处理标准输入:避免bufio.Scanner重复创建导致输入丢失

本文深入探讨了go语言中处理标准输入时,使用`bufio.Scanner`可能遇到的一个常见问题:当程序从键盘或重定向文件读取多行输入时,重复创建`bufio.Scanner`实例会导致后续输入丢失。文章详细分析了问题根源,并提供了两种解决方案:使用全局变量(简单但不推荐)和通过自定义类型封装`bufio.Scanner`实例(推荐的面向对象方法),以确保输入流的正确处理和资源的有效复用。

理解bufio.Scanner与输入丢失问题

Go语言中,bufio.Scanner是处理行分隔输入(如从标准输入或文件)的常用工具。它通过内部缓冲区读取数据,每次调用Scan()方法时,会从缓冲区中提取一行文本。然而,如果程序在每次需要读取输入时都创建一个新的bufio.Scanner实例来处理os.Stdin,尤其是在输入源是重定向的文件时,就会出现输入数据丢失的问题。

考虑以下示例代码,其中prompt函数每次被调用时都会创建一个新的bufio.Scanner:

package main  import (     "bufio"     "fmt"     "os" )  func print(format string, a ...interface{}) {     fmt.Printf(format+"n", a...) }  func prompt(format string) string {     fmt.Print(format)     in := bufio.NewScanner(os.Stdin) // 每次调用都创建新的Scanner     in.Scan()     return in.Text() }  func greet() {     name := prompt("enter name: ")     print(`Hello %s!`, name) }  func humor() {     color := prompt("enter favorite color: ")     print(`I like %s too!`, color) }  func main() {     greet()     humor() }

当程序通过键盘交互运行时,上述代码通常表现正常。但如果我们将一个包含多行输入的文本文件重定向到程序(例如 .test < a.txt),其中a.txt内容如下:

立即学习go语言免费学习笔记(深入)”;

bobby bill soft, blue-ish turquoise

程序输出可能会是这样:

enter name: Hello bobby bill! enter favorite color: I like  too!

这与预期不符。问题在于,bufio.Scanner在内部会预读并缓冲多于一行的输入。当greet()函数中的prompt()创建了一个bufio.Scanner并读取了第一行(”bobby bill”)后,它可能已经将第二行(”soft, blue-ish turquoise”)甚至更多数据缓冲到了其内部。当prompt()函数返回,这个bufio.Scanner实例被垃圾回收时,其内部缓冲区中尚未读取的数据(即”soft, blue-ish turquoise”)也随之丢失了。接着,humor()函数再次调用prompt()时,会创建一个全新的bufio.Scanner。这个新的扫描器会从os.Stdin的当前位置开始读取,但此时,第二行数据已经被前一个扫描器读取并丢弃了,导致它无数据可读,最终返回空字符串

解决方案一:使用全局共享的bufio.Scanner实例

最直接的解决方案是确保所有需要从os.Stdin读取的函数都共享同一个bufio.Scanner实例。这可以通过将bufio.Scanner声明为全局变量来实现。

Go语言中处理标准输入:避免bufio.Scanner重复创建导致输入丢失

标书对比王

标书对比王是一款标书查重工具,支持多份投标文件两两相互比对,重复内容高亮标记,可快速定位重复内容原文所在位置,并可导出比对报告。

Go语言中处理标准输入:避免bufio.Scanner重复创建导致输入丢失 58

查看详情 Go语言中处理标准输入:避免bufio.Scanner重复创建导致输入丢失

package main  import (     "bufio"     "fmt"     "os" )  var sharedScanner *bufio.Scanner // 声明一个全局的Scanner  func init() {     // 在程序启动时初始化一次     sharedScanner = bufio.NewScanner(os.Stdin) }  func print(format string, a ...interface{}) {     fmt.Printf(format+"n", a...) }  func prompt(format string) string {     fmt.Print(format)     sharedScanner.Scan() // 使用共享的Scanner     return sharedScanner.Text() }  func greet() {     name := prompt("enter name: ")     print(`Hello %s!`, name) }  func humor() {     color := prompt("enter favorite color: ")     print(`I like %s too!`, color) }  func main() {     greet()     humor() }

通过这种方式,无论prompt()被调用多少次,它都操作同一个bufio.Scanner实例。这样,扫描器内部的缓冲区会持续保存未读取的数据,确保所有行都能被正确处理。

注意事项: 虽然这种方法有效,但在大型或复杂应用中,过度使用全局变量可能导致代码难以维护和测试,因为它引入了全局状态依赖。通常,应优先考虑更具封装性的设计模式。

解决方案二:通过自定义类型封装bufio.Scanner实例(推荐)

为了避免全局变量带来的潜在问题,更优雅的解决方案是将bufio.Scanner实例封装在一个自定义类型中,并将其作为该类型的方法来使用。这遵循了面向对象的设计原则,将相关的状态和行为聚合在一起。

我们可以创建一个InputReader类型来持有bufio.Scanner,并提供一个Prompt方法来读取输入。

package main  import (     "bufio"     "fmt"     "os" )  // InputReader 封装了 bufio.Scanner,用于管理标准输入 type InputReader struct {     scanner *bufio.Scanner }  // NewinputReader 创建并返回一个初始化好的 InputReader 实例 func NewInputReader() *InputReader {     return &InputReader{         scanner: bufio.NewScanner(os.Stdin),     } }  // Prompt 方法用于显示提示并读取一行输入 func (ir *InputReader) Prompt(format string) string {     fmt.Print(format)     ir.scanner.Scan()     return ir.scanner.Text() }  func print(format string, a ...interface{}) {     fmt.Printf(format+"n", a...) }  func greet(reader *InputReader) {     name := reader.Prompt("enter name: ")     print(`Hello %s!`, name) }  func humor(reader *InputReader) {     color := reader.Prompt("enter favorite color: ")     print(`I like %s too!`, color) }  func main() {     // 创建一个 InputReader 实例,并在需要时传递给相关函数     reader := NewInputReader()      greet(reader)     humor(reader) }

在这个改进后的代码中:

  1. 我们定义了一个InputReader结构体,其中包含一个*bufio.Scanner字段。
  2. NewInputReader()函数作为构造器,负责创建并初始化InputReader实例。
  3. Prompt()方法现在是InputReader类型的方法,它使用封装在InputReader实例中的scanner来读取输入。
  4. 在main函数中,我们只创建了一个InputReader实例,并将其作为参数传递给greet()和humor()函数。

这种方法的好处在于:

  • 封装性 将bufio.Scanner及其相关操作封装在一个类型中,提高了代码的模块化。
  • 可维护性: 避免了全局状态,使得代码更容易理解和维护。
  • 可测试性: 方便进行单元测试,可以通过模拟InputReader的行为来测试依赖它的函数。
  • 灵活性: 如果将来需要从其他源(如文件)读取输入,可以轻松扩展InputReader类型或创建新的读取器类型。

总结与最佳实践

当在Go语言中处理多行标准输入(无论是来自键盘还是重定向文件)时,核心原则是:对os.Stdin使用且仅使用一个bufio.Scanner实例。

  • 问题根源: bufio.Scanner会预读并缓冲数据。重复创建实例会导致前一个实例的缓冲区数据丢失
  • 解决方案:
    1. 全局共享: 将bufio.Scanner声明为全局变量并在init()中初始化。简单但可能引入全局状态问题。
    2. 自定义类型封装(推荐): 创建一个包含bufio.Scanner的自定义结构体,并提供方法来访问它。这种方式提供了更好的封装、可维护性和可测试性。

通过采用自定义类型封装的方法,我们可以编写出更健壮、更易于管理和扩展的Go程序,有效避免因bufio.Scanner的重复创建而导致的输入数据丢失问题。

上一篇
下一篇
text=ZqhQzanResources