Golang中当exec.Cmd.Stdout和cmd.Stderr指向同一个bytes.Buffer时,为何输出超过65536会被截断?
Golang中当exec.Cmd.Stdout和cmd.Stderr指向同一个bytes.Buffer时,为何输出超过65536会被截断?
为什么当 exec.Cmd.Stdout 和 cmd.Stderr 指向同一个 bytes.Buffer 时,超过 65536 的 exec 命令输出会被截断,如下所示?
package main
import (
"bytes"
"encoding/json"
"os"
"os/exec"
"k8s.io/klog/v2"
)
func main() {
klog.Infof("==============1==================")
args := []string{"bucket", "stats"}
execCmd := exec.Command("radosgw-admin", args...)
f, err := os.Create("xxxx")
if err != nil {
panic(err)
}
execCmd.Stdout = f
err = execCmd.Run()
if err != nil {
panic(err)
}
f.Close()
bs, err := os.ReadFile("xxxx")
if err != nil {
panic(err)
}
klog.Infof("exec.Command(radosgw-admin bucket stats), length: %d", len(bs))
klog.Infof("==============2==================")
execCmd = exec.Command("radosgw-admin", args...)
var b bytes.Buffer
execCmd.Stdout = &b
err = execCmd.Run()
if err != nil {
panic(err)
}
klog.Infof("exec.Command(radosgw-admin bucket stats), length: %d", b.Len())
klog.Infof("==============3==================")
execCmd = exec.Command("radosgw-admin", args...)
var b2 bytes.Buffer
execCmd.Stdout = &b2
execCmd.Stderr = &b2 // <== here
err = execCmd.Run()
if err != nil {
panic(err)
}
klog.Infof("exec.Command(radosgw-admin bucket stats), length: %d", b2.Len())
klog.Infof("==============4==================")
execCmd = exec.Command("radosgw-admin", args...)
f, err = os.Create("yyyy")
if err != nil {
panic(err)
}
execCmd.Stdout = f
execCmd.Stderr = f
err = execCmd.Run()
if err != nil {
panic(err)
}
f.Close()
bs2, err := os.ReadFile("yyyy")
if err != nil {
panic(err)
}
klog.Infof("exec.Command(radosgw-admin bucket stats), length: %d", len(bs2))
klog.Infof("==============5==================")
execCmd = exec.Command("radosgw-admin", args...)
var b3 bytes.Buffer
var b4 bytes.Buffer
execCmd.Stdout = &b3
execCmd.Stderr = &b4
err = execCmd.Run()
if err != nil {
panic(err)
}
klog.Infof("exec.Command(radosgw-admin bucket stats), length: %d, err: %d", b3.Len(), b4.Len())
}
输出:
root@node01:/# ./test
I1207 10:53:47.150299 57435 test.go:36] ==============1==================
I1207 10:53:48.800277 57435 test.go:56] exec.Command(radosgw-admin bucket stats), length: 404237
I1207 10:53:48.803742 57435 test.go:64] ==============2==================
I1207 10:53:50.421419 57435 test.go:73] exec.Command(radosgw-admin bucket stats), length: 404237
I1207 10:53:50.421446 57435 test.go:76] ==============3==================
I1207 10:53:52.537304 57435 test.go:86] exec.Command(radosgw-admin bucket stats), length: 65536
I1207 10:53:52.537343 57435 test.go:89] ==============4==================
I1207 10:53:55.030006 57435 test.go:108] exec.Command(radosgw-admin bucket stats), length: 404237
I1207 10:53:55.035473 57435 test.go:115] ==============5==================
I1207 10:53:58.429107 57435 test.go:126] exec.Command(radosgw-admin bucket stats), length: 404237, err: 0
root@node01:/# radosgw-admin bucket stats | wc -c
404237
更多关于Golang中当exec.Cmd.Stdout和cmd.Stderr指向同一个bytes.Buffer时,为何输出超过65536会被截断?的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于Golang中当exec.Cmd.Stdout和cmd.Stderr指向同一个bytes.Buffer时,为何输出超过65536会被截断?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
这个问题涉及 Go 标准库 os/exec 包中管道缓冲区的内部实现。当 Stdout 和 Stderr 指向同一个 bytes.Buffer 时,超过 65536 字节的输出被截断,是因为 Go 的 exec.Cmd 内部使用了固定大小的管道缓冲区,并且在并发写入同一个缓冲区时可能发生阻塞。
具体原因是:exec.Cmd 启动子进程后,会为 Stdout 和 Stderr 分别创建管道(如果它们不是文件描述符)。每个管道的缓冲区大小在 Linux 系统上默认为 65536 字节(64KB)。当两个管道同时向同一个 bytes.Buffer 写入时,如果其中一个管道填满了缓冲区,而另一个管道还在等待缓冲区读取,就可能发生死锁,导致输出被截断。
以下是重现问题的简化示例:
package main
import (
"bytes"
"fmt"
"os/exec"
)
func main() {
// 生成超过 64KB 输出的命令
cmd := exec.Command("sh", "-c", "dd if=/dev/zero bs=1024 count=70 2>&1; echo 'stderr' 1>&2")
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf // 指向同一个缓冲区
err := cmd.Run()
if err != nil {
fmt.Printf("命令执行错误: %v\n", err)
}
fmt.Printf("输出长度: %d (预期: 71KB+)\n", buf.Len())
// 实际输出可能只有 65536 字节
}
要解决这个问题,可以使用独立的缓冲区分别捕获标准输出和标准错误:
package main
import (
"bytes"
"fmt"
"io"
"os/exec"
)
func main() {
cmd := exec.Command("sh", "-c", "dd if=/dev/zero bs=1024 count=70 2>&1; echo 'stderr' 1>&2")
var stdoutBuf, stderrBuf bytes.Buffer
cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf
err := cmd.Run()
if err != nil {
fmt.Printf("命令执行错误: %v\n", err)
}
// 合并输出(如果需要)
var combinedBuf bytes.Buffer
combinedBuf.Write(stdoutBuf.Bytes())
combinedBuf.Write(stderrBuf.Bytes())
fmt.Printf("输出长度: %d\n", combinedBuf.Len())
}
或者使用 io.MultiWriter 将输出复制到多个写入器:
package main
import (
"bytes"
"fmt"
"io"
"os/exec"
)
func main() {
cmd := exec.Command("sh", "-c", "dd if=/dev/zero bs=1024 count=70 2>&1; echo 'stderr' 1>&2")
var buf bytes.Buffer
// 创建多写入器,确保并发安全
stdoutWriter := io.MultiWriter(&buf)
stderrWriter := io.MultiWriter(&buf)
cmd.Stdout = stdoutWriter
cmd.Stderr = stderrWriter
err := cmd.Run()
if err != nil {
fmt.Printf("命令执行错误: %v\n", err)
}
fmt.Printf("输出长度: %d\n", buf.Len())
}
在 Linux 系统上,管道缓冲区大小可以通过 fcntl 系统调用设置,但 Go 的 os/exec 包没有暴露这个接口。因此,最可靠的解决方案是避免将 Stdout 和 Stderr 重定向到同一个内存缓冲区,特别是当预期输出较大时。

