在Go语言开发中,很多时候我们需要处理标准输入os.Stdin的数据,但默认的读取操作是阻塞的,会一直等待用户输入才会继续执行后续逻辑。如果要实现非阻塞式的判断标准输入是否有数据,就需要借助操作系统层面的能力来调整文件描述符的读取模式。

核心实现思路
标准输入对应的文件描述符是0,我们可以通过系统调用修改这个文件描述符的属性,将其设置为非阻塞模式,之后尝试读取时如果没有数据就会直接返回错误,通过这个错误就能判断是否有输入数据。
Linux/macOS系统实现
在类Unix系统中,我们可以使用syscall.FcntlInt来修改文件描述符的标志位,添加O_NONBLOCK属性来实现非阻塞模式。
package main
import (
"fmt"
"os"
"syscall"
)
// 判断标准输入是否有数据(Linux/macOS)
func isStdinReadyUnix() bool {
// 获取标准输入文件描述符的当前标志
fd := int(os.Stdin.Fd())
flags, err := syscall.FcntlInt(uintptr(fd), syscall.F_GETFL, 0)
if err != nil {
return false
}
// 设置非阻塞标志
_, err = syscall.FcntlInt(uintptr(fd), syscall.F_SETFL, flags|syscall.O_NONBLOCK)
if err != nil {
return false
}
// 尝试读取1字节,非阻塞模式下无数据会返回EAGAIN错误
buf := make([]byte, 1)
n, err := os.Stdin.Read(buf)
// 恢复原标志位,避免影响后续正常读取
_, _ = syscall.FcntlInt(uintptr(fd), syscall.F_SETFL, flags)
if n > 0 {
// 有数据读取到,把数据重新放回输入缓冲区(可选,根据需求调整)
// 这里简单处理,直接返回有数据
return true
}
// 判断错误是否为非阻塞下的无数据错误
if err != nil && err == syscall.EAGAIN {
return false
}
return err == nil
}
func main() {
if isStdinReadyUnix() {
fmt.Println("标准输入有数据")
} else {
fmt.Println("标准输入无数据")
}
}
Windows系统实现
Windows系统的文件操作API和类Unix不同,需要使用syscall.MustLoadDLL调用系统DLL来实现非阻塞判断。
package main
import (
"fmt"
"os"
"syscall"
"unsafe"
)
// 判断标准输入是否有数据(Windows)
func isStdinReadyWindows() bool {
// 加载kernel32.dll
dll := syscall.MustLoadDLL("kernel32.dll")
// 获取GetStdHandle函数
getStdHandle := dll.MustFindProc("GetStdHandle")
// 获取标准输入句柄,STD_INPUT_HANDLE值为-10
handle, _, _ := getStdHandle.Call(uintptr(^uint32(10) + 1))
// 获取PeekConsoleInput函数,用于窥探输入缓冲区
peekConsoleInput := dll.MustFindProc("PeekConsoleInputW")
// 定义输入记录结构体
type inputRecord struct {
EventType uint16
// 忽略其他字段,这里只判断是否有记录
_ [30]byte
}
var record inputRecord
var readCount uint32
// 调用PeekConsoleInput,不移除缓冲区内容
ret, _, err := peekConsoleInput.Call(handle, uintptr(unsafe.Pointer(&record)), 1, uintptr(unsafe.Pointer(&readCount)))
if ret == 0 {
return false
}
return readCount > 0
}
func main() {
if isStdinReadyWindows() {
fmt.Println("标准输入有数据")
} else {
fmt.Println("标准输入无数据")
}
}
跨平台封装方案
为了兼容不同操作系统,我们可以根据运行时系统选择对应的实现方法,封装一个统一的判断函数。
package main
import (
"fmt"
"os"
"runtime"
"syscall"
"unsafe"
)
// 跨平台判断标准输入是否有数据
func IsStdinReady() bool {
switch runtime.GOOS {
case "linux", "darwin":
return isStdinReadyUnix()
case "windows":
return isStdinReadyWindows()
default:
// 其他系统默认返回false,或者根据需求扩展
return false
}
}
// Unix实现
func isStdinReadyUnix() bool {
fd := int(os.Stdin.Fd())
flags, err := syscall.FcntlInt(uintptr(fd), syscall.F_GETFL, 0)
if err != nil {
return false
}
_, err = syscall.FcntlInt(uintptr(fd), syscall.F_SETFL, flags|syscall.O_NONBLOCK)
if err != nil {
return false
}
buf := make([]byte, 1)
n, err := os.Stdin.Read(buf)
_, _ = syscall.FcntlInt(uintptr(fd), syscall.F_SETFL, flags)
if n > 0 {
return true
}
if err != nil && err == syscall.EAGAIN {
return false
}
return err == nil
}
// Windows实现
func isStdinReadyWindows() bool {
dll := syscall.MustLoadDLL("kernel32.dll")
getStdHandle := dll.MustFindProc("GetStdHandle")
handle, _, _ := getStdHandle.Call(uintptr(^uint32(10) + 1))
peekConsoleInput := dll.MustFindProc("PeekConsoleInputW")
type inputRecord struct {
EventType uint16
_ [30]byte
}
var record inputRecord
var readCount uint32
ret, _, _ := peekConsoleInput.Call(handle, uintptr(unsafe.Pointer(&record)), 1, uintptr(unsafe.Pointer(&readCount)))
if ret == 0 {
return false
}
return readCount > 0
}
func main() {
if IsStdinReady() {
fmt.Println("当前标准输入存在待读取数据")
} else {
fmt.Println("当前标准输入无待读取数据")
}
}
注意事项
- 修改文件描述符为非阻塞模式后,建议恢复原标志位,避免影响后续正常的阻塞式读取逻辑。
- 如果标准输入是管道传入的数据,非阻塞判断同样适用,管道关闭时会返回EOF错误。
- Windows的实现依赖控制台输入缓冲区,如果是重定向的输入场景可能需要调整判断逻辑。
- 不要频繁调用非阻塞判断函数,避免不必要的系统调用开销,可以配合定时器按需检测。
非阻塞判断标准输入的核心是通过系统调用调整IO模式,根据读取返回的错误类型判断是否有数据,不同系统的API差异较大,实际使用时需要注意兼容性处理。