Правильно передавать данные по команде stdin в команду и получать данные из stdout этой команды в golang

У меня есть следующая программа:

package main

import "bytes"
import "io"
import "log"
import "os"
import "os/exec"
import "time"

func main() {
    runCatFromStdinWorks(populateStdin("aaa\n"))
    runCatFromStdinWorks(populateStdin("bbb\n"))
}

func populateStdin(str string) func(io.WriteCloser) {
    return func(stdin io.WriteCloser) {
        defer stdin.Close()
        io.Copy(stdin, bytes.NewBufferString(str))
    }
}

func runCatFromStdinWorks(populate_stdin_func func(io.WriteCloser)) {
    cmd := exec.Command("cat")
    stdin, err := cmd.StdinPipe()
    if err != nil {
        log.Panic(err)
    }
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        log.Panic(err)
    }
    err = cmd.Start()
    if err != nil {
        log.Panic(err)
    }
    go populate_stdin_func(stdin)
    go func() {
            // Removing the following lines allow some output
            // to be fetched from cat stdout sometimes
            time.Sleep(5 * time.Second)
            io.Copy(os.Stdout, stdout)
    }()
    err = cmd.Wait()
    if err != nil {
        log.Panic(err)
    }
}

При запуске в цикле я не получаю никаких результатов, например:

$ while true; do go run cat_thingy.go; echo ; done



^C

Этот результат возникает после установки golang-go на Ubuntu 12.04 с apt в виртуальной машине (go version go1). Я не смог воспроизвести установку на Macbook Air (go version go1.0.3). Кажется, это какое-то состояние гонки. На самом деле, если я поставлю сон (1 * time.Second), я никогда не вижу проблему за счет случайного сна в моем коде.

Есть ли что-то, что я делаю неправильно в коде, или это ошибка? Если это ошибка, исправлена ​​ли она?

ОБНОВЛЕНИЕ: Возможная подсказка

Я обнаружил, что Command.Wait закроет каналы для связи с/подкодом cat, даже если у них все еще есть непрочитанные данные. Я не совсем уверен в правильном способе справиться с этим. Думаю, я мог бы создать канал для уведомления, когда запись в stdin будет завершена, но мне все равно нужно будет знать, закончился ли процесс cat, чтобы убедиться, что в его трубе stdout ничего не будет записано. Я знаю, что могу использовать cmd.Process.Wait, чтобы определить, когда закончится процесс, но безопасно ли тогда вызвать cmd.Wait?

ОБНОВЛЕНИЕ: Ускорение

Здесь новый фрагмент кода. Я считаю, что это работает до написания stdin и чтения из stdout. Я думаю, что я могу заставить его правильно передавать данные (вместо того, чтобы буферизировать все это), если я заменил io.Copy на stdout-обработку goroutine без потокового потока.

package main

import "bytes"
import "fmt"
import "io"
import "log"
import "os/exec"
import "runtime"

const inputBufferBlockLength = 3*64*(2<<10) // enough to be bigger than 2x the pipe buffer of 64KiB
const numInputBlocks = 6

func main() {
    runtime.GOMAXPROCS(5)
    runCatFromStdin(populateStdin(numInputBlocks))
}

func populateStdin(numInputBlocks int) func(io.WriteCloser, chan bool) {
    return func(stdin io.WriteCloser) {
        defer stdin.Close()
        repeatedByteBases := []string{"a", "b", "c", "d", "e", "f"}
        for i := 0; i < numInputBlocks; i++ {
          repeatedBytes := bytes.NewBufferString(repeatedByteBases[i]).Bytes()
          fmt.Printf("%s\n", repeatedBytes)
          io.Copy(stdin, bytes.NewReader(bytes.Repeat(repeatedBytes, inputBufferBlockLength)))
        }
    }
}

func runCatFromStdin(populate_stdin_func func(io.WriteCloser)) {
    cmd := exec.Command("cat")
    stdin, err := cmd.StdinPipe()
    if err != nil {
        log.Panic(err)
    }
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        log.Panic(err)
    }
    err = cmd.Start()
    if err != nil {
        log.Panic(err)
    }
    go populate_stdin_func(stdin)
    output_done_channel := make(chan bool)
    go func() {
        out_bytes := new(bytes.Buffer)
        io.Copy(out_bytes, stdout)
        fmt.Printf("%s\n", out_bytes)
        fmt.Println(out_bytes.Len())
        fmt.Println(inputBufferBlockLength*numInputBlocks)
        output_done_channel <- true
    }()
    <-output_done_channel
    err = cmd.Wait()
    if err != nil {
        log.Panic(err)
    }
}

Ответ 1

Вот версия вашего первого кода, которая работает. Обратите внимание на добавление sync.WaitGroup, чтобы убедиться, что вы закончили с процедурами отправки и получения go перед закрытием команды.

package main

import (
    "bytes"
    "io"
    "log"
    "os"
    "os/exec"
    "sync"
    "time"
)

func main() {
    runCatFromStdinWorks(populateStdin("aaa\n"))
    runCatFromStdinWorks(populateStdin("bbb\n"))
}

func populateStdin(str string) func(io.WriteCloser) {
    return func(stdin io.WriteCloser) {
        defer stdin.Close()
        io.Copy(stdin, bytes.NewBufferString(str))
    }
}

func runCatFromStdinWorks(populate_stdin_func func(io.WriteCloser)) {
    cmd := exec.Command("cat")
    stdin, err := cmd.StdinPipe()
    if err != nil {
        log.Panic(err)
    }
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        log.Panic(err)
    }
    err = cmd.Start()
    if err != nil {
        log.Panic(err)
    }
    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
        defer wg.Done()
        populate_stdin_func(stdin)
    }()
    go func() {
        defer wg.Done()
        time.Sleep(5 * time.Second)
        io.Copy(os.Stdout, stdout)
    }()
    wg.Wait()
    err = cmd.Wait()
    if err != nil {
        log.Panic(err)
    }
}

(Это просто еще один способ сказать, что сказал @peterSO, -)

Ответ 2

Операторы Go

Оператор "go" запускает выполнение вызова функции или метода как независимой параллельной нити управления или goroutine в пределах то же адресное пространство.

GoStmt = "go" Выражение.

Выражение должно быть вызовом. Значения и параметры функции как обычно, в вызывающей горутине, но в отличие от обычного вызов, выполнение программы не дожидается, когда вызываемая функция будет полный. Вместо этого функция начинает выполняться независимо в новый горутин. Когда функция завершается, ее горутин также завершается. Если функция имеет какие-либо возвращаемые значения, они отбрасываются когда функция завершается.

Преобразуйте безвозмездные голосовые вызовы в вызовы функций.

package main

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

func main() {
    runCatFromStdinWorks(populateStdin("aaa\n"))
    runCatFromStdinWorks(populateStdin("bbb\n"))
}

func populateStdin(str string) func(io.WriteCloser) {
    return func(stdin io.WriteCloser) {
        defer stdin.Close()
        io.Copy(stdin, bytes.NewBufferString(str))
    }
}

func runCatFromStdinWorks(populate_stdin_func func(io.WriteCloser)) {
    cmd := exec.Command("cat")
    stdin, err := cmd.StdinPipe()
    if err != nil {
        log.Panic(err)
    }
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        log.Panic(err)
    }
    err = cmd.Start()
    if err != nil {
        log.Panic(err)
    }
    populate_stdin_func(stdin)
    io.Copy(os.Stdout, stdout)
    err = cmd.Wait()
    if err != nil {
        log.Panic(err)
    }
}