go读os.stdin不卡住的唯一可靠方式是用bufio.scanner并检查scanner.err();fmt.scan系函数因缓冲区和换行符处理不当易卡死,scanner需显式错误处理、合理配置缓冲区且不可与reader混用。

Go 读 os.Stdin 不卡住的唯一可靠方式,是用 bufio.Scanner 并检查 scanner.Err();直接调 fmt.Scan、os.Stdin.Read 或混用多种读法,90% 的“卡死”“丢行”“错位”都源于此。
为什么 fmt.Scan 和 fmt.Scanln 容易卡住
它们不控制缓冲区边界,也不区分终端/管道输入,行为完全依赖上一次读取后残留的换行符或空格:
-
fmt.Scan会跳过开头所有空白(包括换行),但不会消费掉它——下一次读可能立刻返回空字符串 -
fmt.Scanln要求输入末尾必须是换行符,否则阻塞;且它不吞掉换行符,后续Scanner第一次Scan()可能直接失败 - 输入中混入全角空格、中文标点或多余回车,
fmt.Scanf("%s %d")就直接停住,连错误都不报
bufio.Scanner 的正确用法和三个关键配置点
它不是“更方便的 fmt.Scan”,而是一个带状态、需显式错误处理的流处理器:
- 必须在循环结束后调
scanner.Err()检查是否因超长行、I/O 错误等提前终止——漏判会导致静默丢数据 - 默认单行上限 64KB,超长触发
scanner.Err() == bufio.ErrTooLong;扩容写法:scanner.Buffer(make([]byte, 1024*1024), 1024*1024)(第二个参数是 max token size) - 不要在循环内重复创建
bufio.NewScanner(os.Stdin);goroutine 中也只应有一个 scanner 实例,否则缓冲区错位
如何判断 stdin 是来自终端还是管道/重定向
os.Stdin.Stat().Mode() & os.ModeCharDevice == 0 在容器、CI 或 Windows Git Bash 下经常误判,不可靠:
- 推荐用
github.com/mattn/go-isatty.IsTerminal(os.Stdin.Fd()),跨平台实测稳定 - 如果只是想“有输入就读,没输入就跳过”,别依赖设备类型——改用带超时的非阻塞读:
time.AfterFunc(100*time.Millisecond, func(){...})包一层os.Stdin.Read,或直接起 goroutine +scanner.Scan() - 注意:Windows 上
Ctrl+Z才是 EOF,不是Ctrl+D;测试时别用错退出信号
需要读二进制、单字符或混合格式时怎么选
不是所有输入都是“一行一行的文本”,硬套 Scanner 反而增加复杂度:
- 读整个输入(如 base64、JSON blob):用
io.ReadAll(os.Stdin),它只在 EOF 或 error 时返回,适合管道场景 - 读单个 Unicode 字符(如 y/n 确认):用
bufio.NewReader(os.Stdin).ReadRune(),比ReadByte()正确支持中文 - 混合读取(先读 int 再读一整行):必须用
bufio.Reader,且全程不能和Scanner共用os.Stdin——否则前者的缓冲区会吃掉后者该读的内容
最常被忽略的一点:无论用哪种方式,os.Stdin 是共享资源。多个 goroutine 同时读、或 Scanner 和 Reader 混用,不会 panic,但输入会随机丢失或重复——这种 bug 很难复现,调试成本远高于一开始就约束读取入口。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











