管道

发布: (2025年12月31日 GMT+8 03:57)
3 min read
原文: Dev.to

Source: Dev.to

Pipes 的封面图

问题

我想在 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
}

等待完成

cmd2head 命令)会先结束。它退出后我们必须在父进程中关闭管道的读取端;否则管道保持打开,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 并终止。

Back to Blog

相关文章

阅读更多 »

为什么 Bash 语法如此晦涩

Bash的历史根源 Bash在Linux的表面之下深深扎根。只需一两行代码,它就能完全控制你的机器。它常常被视为b...