测试CLI交互式提示:如何让scanner.Scan等待后续写入?
如何让bufio.Scanner等待后续写入以测试CLI提示?
我需要编写测试验证CLI提示,模拟用户输入并响应程序输出,但遇到了scanner.Scan无法等待后续写入的问题,以下是我的尝试及问题:
尝试1:使用bytes.Buffer实现
b := &bytes.Buffer{} fmt.Fprint(b, "0") go func() { time.Sleep(1 * time.Second) for i := 1; i < 4; i++ { fmt.Fprint(b, i) time.Sleep(1 * time.Second) } }() scanner := bufio.NewScanner(b) for scanner.Scan() { log.Print(scanner.Text()) } if err := scanner.Err(); err != nil { log.Println("problem while scanning:", err) }
- 预期结果:输出
0123 - 实际结果:仅输出
0
尝试2:使用io.Pipe实现
r, w := io.Pipe() fmt.Fprint(w, "0") go func() { time.Sleep(1 * time.Second) for i := 1; i < 4; i++ { fmt.Fprint(w, i) time.Sleep(1 * time.Second) } }() scanner := bufio.NewScanner(r) for scanner.Scan() { log.Print(scanner.Text()) } if err := scanner.Err(); err != nil { log.Println("problem while scanning:", err) }
- 结果:触发
fatal error: all goroutines are asleep - deadlock!(致命错误:所有goroutine都处于休眠状态 - 死锁!)
问题原因及解决方案
问题分析
bytes.Buffer版本:
bufio.Scanner默认按行扫描(分隔符为\n),写入的内容没有换行符,且bytes.Buffer是内存缓冲区,scanner读完当前所有内容后就会返回false退出循环,不会主动等待后续写入。io.Pipe版本:
io.Pipe的Write操作是阻塞的,直到对应Read操作读取数据;而scanner默认按行扫描,写入的内容无换行符,导致Read一直等待换行,goroutine中的Write也随之阻塞,最终所有goroutine卡住引发死锁。
修正方案
方案1:基于io.Pipe的行扫描(推荐,贴近真实CLI场景)
给每个写入内容添加换行符,写完后关闭写端,让scanner能正确识别每行数据,且不会无限阻塞:
r, w := io.Pipe() go func() { defer w.Close() // 写完后关闭写端,触发scanner退出循环 fmt.Fprintln(w, "0") // 写入带换行的内容 time.Sleep(1 * time.Second) for i := 1; i < 4; i++ { fmt.Fprintln(w, i) // 每个输入都带换行 time.Sleep(1 * time.Second) } }() scanner := bufio.NewScanner(r) for scanner.Scan() { log.Print(scanner.Text()) } if err := scanner.Err(); err != nil { log.Println("problem while scanning:", err) }
此代码会依次输出0、1、2、3,符合预期逻辑。
方案2:基于bytes.Buffer的字节扫描
若需要读取单个字符而非行,可自定义scanner的分割函数为bufio.ScanBytes,并通过通道同步等待goroutine写入完成:
b := &bytes.Buffer{} done := make(chan struct{}) go func() { defer close(done) fmt.Fprint(b, "0") time.Sleep(1 * time.Second) for i := 1; i < 4; i++ { fmt.Fprint(b, i) time.Sleep(1 * time.Second) } }() scanner := bufio.NewScanner(b) scanner.Split(bufio.ScanBytes) // 按单个字节扫描 for { select { case <-done: // 读取goroutine写完后的剩余内容 for scanner.Scan() { log.Print(scanner.Text()) } goto exitLoop default: if scanner.Scan() { log.Print(scanner.Text()) } time.Sleep(100 * time.Millisecond) // 避免空转占用CPU } } exitLoop: if err := scanner.Err(); err != nil { log.Println("problem while scanning:", err) }
内容的提问来源于stack exchange,提问作者piotrjablonski
相关产品推荐
相关产品推荐

