为何exec.Cmd的Stdout与Stderr指向同一bytes.Buffer时输出超65536会截断?
Go中exec.Cmd同时将Stdout和Stderr指向同一bytes.Buffer时输出被截断的原因
问题现象
使用Go的exec.Cmd执行外部命令时,出现以下异常:
- 当
Stdout和Stderr同时指向同一个bytes.Buffer,且命令输出超过65536字节时,结果会被截断为65536字节; - 其他场景(输出指向文件、仅
Stdout指向bytes.Buffer、Stdout和Stderr分别指向不同bytes.Buffer、同时指向同一文件)均能得到完整的404237字节输出。
测试代码
package main import ( "bytes" "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 // <== 问题场景 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
原因分析
- 并发写入的竞争条件:Go在执行
exec.Cmd.Run()时,会启动两个独立的goroutine分别处理Stdout和Stderr的写入。而bytes.Buffer的Write方法并非线程安全,当两个goroutine同时向同一个bytes.Buffer写入数据时,会导致内部缓冲区的状态(如长度、容量)出现竞争混乱,最终引发数据丢失或截断。 - 65536字节的阈值关联:65536字节是
bytes.Buffer默认的扩容临界值之一,当并发写入触发缓冲区扩容操作时,竞争条件会导致部分写入操作失败,最终输出被截断到这个大小。 - 其他场景正常的原因:
os.File的Write操作由操作系统保证并发安全,因此两个goroutine同时写入同一文件不会出现问题;- 当
Stdout和Stderr指向不同bytes.Buffer时,各自的goroutine独立写入,不存在竞争; - 仅
Stdout指向bytes.Buffer时,只有一个写入goroutine,自然也不会有竞争问题。
解决方案
方案1:使用线程安全的包装Buffer
实现一个带互斥锁的安全Buffer,保护Write操作的并发执行:
import "sync" type SafeBuffer struct { buf bytes.Buffer mu sync.Mutex } func (s *SafeBuffer) Write(p []byte) (n int, err error) { s.mu.Lock() defer s.mu.Unlock() return s.buf.Write(p) } // 使用示例 var safeBuf SafeBuffer execCmd.Stdout = &safeBuf execCmd.Stderr = &safeBuf
方案2:将Stderr重定向到Stdout
直接让Stderr复用Stdout的写入逻辑,避免启动额外的写入goroutine:
execCmd.Stderr = execCmd.Stdout
这种方式Go内部会统一处理输出,不会出现并发写入的竞争问题,同样能得到完整输出。
内容的提问来源于stack exchange,提问作者LiangZheng
相关产品推荐
相关产品推荐

