os.Pipe本质是创建内核中一对同步的读写端文件描述符,不涉及磁盘、不支持seek,需成对使用且显式关闭;典型用途是子进程流重定向和goroutine间字节流通信。

os.Pipe 本质是创建一对同步的文件描述符
os.Pipe 返回的 *os.File 对象不是“管道文件”,而是内核中一对关联的读写端文件描述符(类似 Unix pipe(2))。它不涉及磁盘、不支持 seek,只适用于单向字节流传递,且必须成对使用:一端写、另一端读,否则会阻塞或死锁。
常见错误现象:read | write: broken pipe(写端提前关闭,读端还在等);read | write: file already closed(重复 Close);或者程序卡住——大概率是没关对端或没启动读/写协程。
- 必须用
defer r.Close()和defer w.Close()显式关闭,不能依赖 GC - 读写操作默认阻塞,若需非阻塞,得用
syscall.SetNonblock()(跨平台差,不推荐) - 不要在同一个 goroutine 里先写后读——会卡死,因为写满缓冲区(通常 64KiB)就阻塞,而读还没开始
配合 exec.Cmd 实现子进程标准流重定向
这是 os.Pipe 最典型的应用场景:把子进程的 Stdout 或 Stdin 指向内存管道,避免临时文件、绕过 shell。
例如,让 ls -l 的输出直接进 Go 程序内存:
r, w, _ := os.Pipe()
cmd := exec.Command("ls", "-l")
cmd.Stdout = w
_ = cmd.Start()
w.Close() // 关闭写端,告诉读端“不会再有新数据”
buf, _ := io.ReadAll(r)
r.Close()
fmt.Printf("%s", buf)
-
w.Close()必须在cmd.Start()后、io.ReadAll(r)前调用,否则ReadAll不知道 EOF - 若还需捕获
Stderr,要再调一次os.Pipe(),并赋给cmd.Stderr - 注意
cmd.Run()会阻塞等待结束,但用Start()+ 手动 wait 更灵活(比如超时控制)
在两个 goroutine 间安全传递字节流
不用外部进程,纯 Go 内部用 os.Pipe 做 goroutine 通信,适合需要字节流语义(如模拟网络流、复用 io.Copy)的场景,比 channel 更贴近 I/O 接口。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
示例:一个 goroutine 写 JSON,另一个解析:
r, w, _ := os.Pipe()
go func() {
defer w.Close()
json.NewEncoder(w).Encode(map[string]int{"x": 1})
}()
var v map[string]int
json.NewDecoder(r).Decode(&v)
r.Close()
- 必须确保写 goroutine 在写完后
Close(),否则Decode会一直等 - 不能并发读写同一端,
os.File的 Read/Write 方法本身是线程安全的,但逻辑上仍应单读单写 - 性能上不如
bytes.Buffer轻量,但优势在于可直接传给任何接受io.Reader/io.Writer的函数(比如http.ServeContent)
注意 Windows 下 CreatePipe 的兼容性边界
Go 的 os.Pipe 在 Windows 底层调用 CreatePipe,行为和 Unix 几乎一致,但有两个硬限制容易被忽略:
- 管道缓冲区大小固定为 64KiB,无法通过参数调整(Unix 也一样,但常被误以为可调)
- Windows 不支持
O_CLOEXEC语义,所以子进程继承管道句柄的风险更高;务必在exec.Cmd前设置cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}或显式cmd.ExtraFiles控制 - 若遇到
The handle is invalid错误,大概率是某端已被关闭多次,或跨 goroutine 误用了已关闭的*os.File
真正难调试的点往往不在创建管道,而在谁该关、何时关、关几次——尤其是混用 exec.Cmd 和手动 os.Pipe 时,Cmd 内部可能悄悄接管了 Close 行为。










