Golang中exec.Cmd的Stdin管道与文件输入对比
Golang中exec.Cmd的Stdin管道与文件输入对比 大家好,
我在向 exec.Command 提供缓冲区时注意到一些意外行为。
有一个用于创建图像缩略图的工具,叫做“vipsthumbnail”,我在一个类似这样的流水线架构中使用它:
vipsthumbnail stdin -o thumbnail.webp < original.jpeg
它能按预期创建缩略图。
Go 语言的等效实现也以相同的方式工作:
original, err := os.Open("original.jpeg")
if err != nil {
log.Fatal(err)
}
cmd := exec.Command("vipsthumbnail", "stdin", "-o", "thumbnail.webp")
cmd.Stdin = original
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
然而,使用缓冲区却不行:
original, err := os.ReadFile("original.jpeg")
if err != nil {
log.Fatal(err)
}
cmd := exec.Command("vipsthumbnail", "stdin", "-o", "thumbnail.webp")
cmd.Stdin = bytes.NewReader(original)
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
使用缓冲区时,vipsthumbnail 工具会失败,并显示一些通用的“无法从标准输入读取”消息。
从 Go 文档中我们了解到:
如果 Stdin 是 *os.File,则进程的标准输入直接连接到该文件。
否则,在命令执行期间,一个单独的 goroutine 会从 Stdin 读取数据,并通过管道将该数据传递给命令。
这是 vipsthumbnail 软件中的一个实现错误,导致它无法正确处理管道吗?这对该软件究竟有何不同?在我的 Go 代码中有什么可以做的吗?
感谢所有的帮助!
更多关于Golang中exec.Cmd的Stdin管道与文件输入对比的实战教程也可以访问 https://www.itying.com/category-94-b0.html
你尝试过使用这个Go语言包吗?
GitHub - davidbyttow/govips: 一个闪电般快速的图像处理和调整大小库…
一个闪电般快速的图像处理和调整大小库,适用于Go语言 - GitHub - davidbyttow/govips: 一个闪电般快速的图像处理和调整大小库,适用于Go语言
感谢你测试这个!我原本期望它能像你说的那样工作。
经过更多测试,我可以确认这是 vipsthumbnail 的问题,并且它只在 Windows 上失败。
以下命令在 Windows 和 Linux 上都能工作:
vipsthumbnail stdin -o thumbnail.webp < "image.jpg"
以下命令在 Windows 上失败(在 Windows 上使用 type 代替 cat):
cat "image.jpg" | vipsthumbnail stdin -o thumbnail.webp
同样的命令在 Windows PowerShell 中也失败:
Get-Content -Path "image.jpg" -AsByteStream | vipsthumbnail stdin -o thumbnail.webp
感谢你的测试。
由于这显然是 vipsthumbnail 的问题,我现在将关闭这个主题。
我最初的想法是这可能与vipsthumbnail本身有关。我没有安装vipsthumbnail,所以我尝试将你的思路/代码应用到grep上,看起来cmd.Exec的行为符合预期。
我运行了以下代码(我尽量保持你代码/问题的原意),得到了如下结果:
With file: "Hello, 世界\n"
With bytes reader: "Hello, 世界\n"
func main() {
f, _ := os.Create("input.txt")
f.WriteString("Hello, 世界")
f.Close()
withFile()
withStdin()
}
func run(r io.Reader) []byte {
cmd := exec.Command("grep", "-i", "hello")
cmd.Stdin = r
out, err := cmd.Output()
if err != nil {
panic(err)
}
return out
}
func withFile() {
f, _ := os.Open("input.txt")
fmt.Printf("With file: %q\n", run(f))
}
func withStdin() {
b, _ := os.ReadFile("input.txt")
fmt.Printf("With bytes reader: %q\n", run(bytes.NewReader(b)))
}
你尝试过其他命令行程序吗?
这是一个关于 exec.Cmd 标准输入处理的重要问题。问题确实出在 os.File 和 bytes.Reader 之间的差异上。
当 cmd.Stdin 是 *os.File 时,Go 直接将文件描述符传递给子进程,子进程可以直接从文件中读取。而当使用 bytes.Reader 时,Go 会创建一个管道,并在单独的 goroutine 中从 reader 复制数据到管道。
对于某些需要随机访问或文件大小检测的工具来说,这种差异很重要。vipsthumbnail 可能依赖于文件描述符的特性。
以下是几种解决方案:
方案1:使用临时文件(最可靠)
original, err := os.ReadFile("original.jpeg")
if err != nil {
log.Fatal(err)
}
// 创建临时文件
tmpFile, err := os.CreateTemp("", "vips-*.jpeg")
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpFile.Name())
// 写入数据
if _, err := tmpFile.Write(original); err != nil {
log.Fatal(err)
}
tmpFile.Close()
// 重新打开文件用于读取
file, err := os.Open(tmpFile.Name())
if err != nil {
log.Fatal(err)
}
defer file.Close()
cmd := exec.Command("vipsthumbnail", "stdin", "-o", "thumbnail.webp")
cmd.Stdin = file
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
方案2:使用 io.Pipe 和 goroutine
original, err := os.ReadFile("original.jpeg")
if err != nil {
log.Fatal(err)
}
cmd := exec.Command("vipsthumbnail", "stdin", "-o", "thumbnail.webp")
stdinPipe, err := cmd.StdinPipe()
if err != nil {
log.Fatal(err)
}
// 在 goroutine 中写入数据
go func() {
defer stdinPipe.Close()
if _, err := io.Copy(stdinPipe, bytes.NewReader(original)); err != nil {
log.Printf("写入管道错误: %v", err)
}
}()
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
方案3:检查工具是否支持其他输入方式 有些图像处理工具支持从标准输入读取但需要特定格式:
original, err := os.ReadFile("original.jpeg")
if err != nil {
log.Fatal(err)
}
cmd := exec.Command("vipsthumbnail", "-", "-o", "thumbnail.webp")
cmd.Stdin = bytes.NewReader(original)
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
这不是 vipsthumbnail 的实现错误,而是许多命令行工具在处理标准输入时的常见行为差异。文件描述符提供了更多元数据(如文件大小、seek 能力),而管道是顺序流。
对于图像处理工具,通常推荐使用临时文件方案,因为它最接近原生文件操作,避免了管道可能带来的缓冲问题。


