Что такое `pipe` (|) в Linux/Unix и как этот механизм используется в Go?

Ответ

Что такое pipe?

Pipe (канал или "пайп"), обозначаемый символом |, — это один из фундаментальных механизмов в Unix-подобных системах. Он позволяет направить стандартный вывод (stdout) одной команды на стандартный ввод (stdin) другой. Это дает возможность строить сложные цепочки команд, где каждая выполняет одну простую задачу.

Классический пример:

# Посчитать количество уникальных строк с "error" в лог-файле
cat app.log | grep "error" | sort | uniq -c

Здесь:

  1. cat app.log: Читает файл и выводит его содержимое в stdout.
  2. grep "error": Получает данные от cat, фильтрует строки, содержащие "error", и передает их дальше.
  3. sort: Сортирует полученные строки.
  4. uniq -c: Подсчитывает количество вхождений каждой уникальной строки.

Использование pipe в Go

Философия pipe глубоко интегрирована в Go и используется в двух основных сценариях:

1. Взаимодействие с внешними командами (os/exec)

Вы можете программно воссоздать цепочку команд, связав их ввод и вывод.

package main

import (
    "bytes"
    "fmt"
    "log"
    "os/exec"
)

func main() {
    // Эмуляция команды: ps aux | grep "go"
    cmd1 := exec.Command("ps", "aux")
    cmd2 := exec.Command("grep", "go")

    // Связываем вывод первой команды со вводом второй
    var err error
    cmd2.Stdin, err = cmd1.StdoutPipe()
    if err != nil {
        log.Fatal(err)
    }

    // Записываем вывод второй команды в буфер
    var output bytes.Buffer
    cmd2.Stdout = &output

    // Запускаем команды
    if err := cmd1.Start(); err != nil {
        log.Fatal(err)
    }
    if err := cmd2.Start(); err != nil {
        log.Fatal(err)
    }

    // Ждем их завершения
    if err := cmd1.Wait(); err != nil {
        log.Fatal(err)
    }
    if err := cmd2.Wait(); err != nil {
        log.Fatal(err)
    }

    fmt.Println(output.String())
}

2. Потоковая обработка данных между горутинами (io.Pipe)

io.Pipe() создает в памяти канал (in-memory pipe), который связывает io.Reader и io.Writer. Это мощный инструмент для потоковой передачи данных между горутинами без необходимости использовать временные файлы или большие буферы.

package main

import (
    "fmt"
    "io"
    "os"
    "time"
)

func main() {
    // Создаем pipe: данные, записанные в pr, можно будет прочитать из pw.
    pr, pw := io.Pipe()

    // Горутина-писатель: генерирует данные и пишет в pipe.
    go func() {
        defer pw.Close()
        for i := 0; i < 5; i++ {
            fmt.Fprintf(pw, "Data packet %dn", i)
            time.Sleep(time.Second)
        }
    }()

    // Горутина-читатель (в данном случае main): читает из pipe и выводит на экран.
    // Копирование блокируется, пока в pipe не появятся данные.
    if _, err := io.Copy(os.Stdout, pr); err != nil {
        fmt.Println(err)
    }
}

Таким образом, Go использует концепцию pipe как для работы с ОС, так и для построения эффективных и конкурентных внутренних конвейеров обработки данных.