管道
Source: Dev.to

问题
我想在 Go 中运行一个管道,使其行为类似于下面的 shell 命令:
tail -f test.log | head -3
tail -f 本身永远不会退出,所以管道必须能够处理第一个命令一直运行,而第二个(head)在读取到所需行数后结束的情况。
在 Shell 中的演示
# 在一个终端
echo line1 > test.log
tail -f ./test.log | head -3
在另一个终端:
echo line2 >> ./test.log
head 会在新行出现时立即打印。当写入第三行时,head 在收到第三行后退出。第四次写入时,tail 会收到 SIGPIPE,因为管道的读取端已经关闭,导致 tail 终止。
Bash 文档确认了这种行为:
The output of each command in the pipeline is connected via a pipe to the input of the next command. Each command reads the previous command’s output.
Each command in a multi‑command pipeline is executed in its own subshell, which is a separate process.
— Bash Reference Manual – Pipelines
在 Go 中模拟这种行为
创建命令
cmd1 := exec.Command("tail", "-f", "./test.log")
cmd2 := exec.Command("head", "-n", "5")
连接管道
outPipe, err := cmd1.StdoutPipe()
if err != nil {
// handle error
}
cmd2.Stdin = outPipe
cmd2.Stdout = os.Stdout // 将 head 的输出转发到 Go 程序的 stdout
启动进程
if err := cmd1.Start(); err != nil {
// handle error
}
if err := cmd2.Start(); err != nil {
// handle error
}
等待完成
cmd2(head 命令)会先结束。它退出后我们必须在父进程中关闭管道的读取端;否则管道保持打开,tail 永远收不到 SIGPIPE。
cmd2.Wait() // 等待 head 完成
outPipe.Close() // 在父进程中关闭管道
cmd1.Wait() // 现在 tail 可以退出
完整示例
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
cmd1 := exec.Command("tail", "-f", "/Users/skuf_love/study/go/test_pipes/test.log")
cmd2 := exec.Command("head", "-n", "5")
outPipe, err := cmd1.StdoutPipe()
if err != nil {
fmt.Printf("StdoutPipe error: %v\n", err)
return
}
cmd2.Stdin = outPipe
cmd2.Stdout = os.Stdout
if err = cmd1.Start(); err != nil {
fmt.Printf("cmd1 start error: %v\n", err)
return
}
if err = cmd2.Start(); err != nil {
fmt.Printf("cmd2 start error: %v\n", err)
return
}
// 等待 head 完成,然后关闭管道,使 tail 收到 SIGPIPE
cmd2.Wait()
fmt.Println("cmd2 wait done")
outPipe.Close()
cmd1.Wait()
fmt.Println("cmd1 wait done")
}
该程序复现了 shell 管道的行为:head 读取 test.log 中写入的前五行后退出,tail 收到 SIGPIPE 并终止。