chan是Go内存队列,非系统管道;os.Pipe()才对应Unix pipe,需手动close写端以触发EOF;for range ch要求仅发送方close,否则死锁。

chan 是 Go 里最常被误称为“管道”的东西,但它不是 Unix pipe,也不涉及文件描述符、os.Pipe() 或进程间通信。真要实现类似 ls | grep 的系统级管道,得用 os.Pipe() 或 exec.Command 配合 StdoutPipe() —— 两者完全不在一个抽象层。搞混就卡死、panic、读不到 EOF。
chan 是内存队列,不是系统 pipe
声明 ch := make(chan int) 创建的是 Go 运行时管理的内存结构,只在当前进程内有效。它没有 fd,不能传给子进程,也不能用 os.Read/os.Write 操作。
常见错误现象:
- 把
chan当成io.Reader直接丢进io.Copy—— 编译不过,类型不匹配 - 试图用
syscall.Dup2复制chan—— 完全无效,chan不是文件描述符 - 查 “Go 管道 IPC” 后写
make(chan []byte)想对接net.Conn—— 语义错位,该用io.Pipe()
正确对应关系:
立即学习“go语言免费学习笔记(深入)”;
-
chan T→ goroutine 间同步/异步数据传递(如任务分发、结果收集) -
os.Pipe()→ 返回*os.File读写端,可用于子进程 stdin/stdout 连接 -
io.Pipe()→ 内存中的io.Reader/io.Writer对,适合在单进程内模拟流式行为
os.Pipe() 才是真 pipe,但要用对
os.Pipe() 返回一对 *os.File,本质是调用 pipe(2) 系统调用,有真实 fd 和内核缓冲区。但它**不会自动 close**,必须手动关,否则子进程 hang 在 read 上。
典型用法(cmd1 | cmd2):
pr, pw := os.Pipe()
cmd1 := exec.Command("ls")
cmd1.Stdout = pw
cmd2 := exec.Command("grep", "main")
cmd2.Stdin = pr
<p>var b bytes.Buffer
cmd2.Stdout = &b</p><p>// 必须先 start,再 wait,顺序错就卡死
if err := cmd1.Start(); err != nil {
log.Fatal(err)
}
if err := cmd2.Start(); err != nil {
log.Fatal(err)
}
pw.Close() // 关闭写端,让 cmd2 收到 EOF
cmd1.Wait()
cmd2.Wait()
关键点:
-
pw.Close()必须在cmd1.Wait()前或同时触发,否则cmd2永远等不到 EOF - 不能只靠
cmd1.Wait()自动关pw—— Go 不会帮你关你没显式持有的*os.File - 如果 cmd1 出错退出,
pw仍需 close,否则 cmd2 卡住
exec.Command + sh -c 是最稳的多级管道
写 ps | grep go | awk '{print $2}' 时,别手撸三段 os.Pipe() 链 —— 容易漏关、goroutine 泄漏、超时无处理。
直接用 shell 解析最简单:
cmd := exec.Command("sh", "-c", `ps aux | grep go | awk '{print $2}'`)
out, err := cmd.Output()
优势:
- 不用管 fd 生命周期、close 时机、goroutine 启停顺序
- EOF、信号、退出码都由 shell 统一处理
- Linux/macOS 一致;Windows 换
exec.Command("cmd", "/c", "...")
限制:
- 无法单独捕获中间命令(如
grep)的 stderr - 沙箱环境禁用 shell 时不可用(此时才需手撸
os.Pipe())
chan 关闭和 range 的死锁陷阱
用 for range ch 读 channel 时,如果没人关它,循环永远不退出 —— 这是并发链中最隐蔽的卡死点。
规则只有两条:
- **仅最后一个发送方**调用
close(ch),且只调一次 - 接收方绝不能
close(ch),也绝不能在for range外再读(比如<-ch后又for range)
错误示例:
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // ✅ 正确:发送完才关
}()
for v := range ch { // ✅ 正确:range 自动检测关闭
fmt.Println(v)
}
但如果中间环节提前 return(比如 filter 拒绝空值后不发),而你又没确保下游能收尽,range 就卡住 —— 这种逻辑边界比语法错误更难 debug。


















