【问题标题】:Go: receive os.cmd stdout and stderr in orderGo:按顺序接收 os.cmd stdout 和 stderr
【发布时间】:2015-08-19 09:57:54
【问题描述】:

我需要从 go 执行子命令并分别处理它的 stdout 和 stderr,同时保持 stdin/stdout 的输出顺序。我尝试了几种不同的方法,但无法实现正确的输出顺序;以下代码显示输出处理顺序是绝对随机的:

package main

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

var (
    result = ""
)

type writer struct {
    result string
    write  func(bytes []byte)
}

func (writer *writer) Write(bytes []byte) (int, error) {
    writer.result += string(bytes) // process result later
    result += string(bytes)
    return len(bytes), nil
}

func main() {
    cmd := exec.Command("bash", "-c", "echo TEST1; echo TEST2 1>&2; echo TEST3")

    stderr := &writer{}
    cmd.Stderr = stderr

    stdout := &writer{}
    cmd.Stdout = stdout

    err := cmd.Start()
    if err != nil {
        log.Fatal(err)
    }

    err = cmd.Wait()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(result)
}

多次运行代码可以输出如下:

$ go run main.go
TEST1
TEST3
TEST2

我希望在所有情况下都有以下结果:

$ go run main.go
TEST1
TEST2
TEST3

我无法调用 cmd.CombinedOutput,因为我需要分别实时处理 stdout/stderr。

【问题讨论】:

  • 嗯.. 出于某种原因,我无法重现您的问题。我总是得到 TEST1 TEST2 TEST3
  • @bshuster13 我可以在 Ubuntu 14.04 上重现它。
  • 一般你不能,因为许多操作系统会缓冲标准输出(特别是如果它没有连接到终端)但不缓冲标准错误。我不知道一种与操作系统无关的方式来更改缓冲(例如,FreeBSD 有 stdbuf(1))。
  • 我尝试在 Arch Linux 上添加带有 stdbuf -o 0 -e 0 的命令并得到相同的结果。有没有办法告诉操作系统不要缓冲结果或模拟终端行为?操作系统特定的方法可以做到这一点。

标签: go exec stdout stderr


【解决方案1】:

您正在执行的命令没有“顺序”。它们是并行管道,因此它们实际上是并发的,就像两个 goroutine 并发一样。您当然可以按照收到它们的顺序存储它们,并通过使用通道或互斥锁来标记它们的来源。为了使合成示例的输出不随机,您需要添加一点暂停。但是,我已经成功地将这种方法与实际命令一起使用:

package main

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

var (
    result = ""
)

type write struct {
    source string
    data   string
}

type writer struct {
    source string

    mu     *sync.Mutex
    writes *[]write
}

func (w *writer) Write(bytes []byte) (int, error) {
    w.mu.Lock()
    defer w.mu.Unlock()
    *w.writes = append(*w.writes, write{
            source: w.source,
            data:   string(bytes),
    })
    return len(bytes), nil
}

func main() {
    cmd := exec.Command("bash", "-c", "echo TEST1; sleep .1; echo TEST2 1>&2; sleep .1; echo TEST3")

    var mu sync.Mutex
    var writes []write

    cmd.Stderr = &writer{
            source: "STDERR",
            mu:     &mu,
            writes: &writes,
    }
    cmd.Stdout = &writer{
            source: "STDOUT",
            mu:     &mu,
            writes: &writes,
    }

    err := cmd.Start()
    if err != nil {
            log.Fatal(err)
    }

    err = cmd.Wait()
    if err != nil {
            log.Fatal(err)
    }

    fmt.Printf("%q\n", writes)
}

会产生

[{"STDOUT" "TEST1\n"} {"STDERR" "TEST2\n"} {"STDOUT" "TEST3\n"}]

【讨论】:

  • 问题是stdout和stderr可以产生一个程序,并且没有办法在该程序的stdout和stderr消息之间设置暂停;例如而不是"bash", "-c" 可以是另一个同时产生标准输出和标准错误的程序。
  • @Leo 标准输出和错误就像从子命令流向程序的水管。如果您几乎同时在两个管道中放入一些东西,则无法保证它们会以相同的顺序到达您手中。上面的解决方案在不修改目标的情况下尽可能好,因为它按照您收到它们的顺序存储它们。您需要处理程序中出现轻微故障的情况。
  • 因此,互斥锁提供了比不加锁更好的排序。在我的机器的 20 次尝试中,只有 3 次顺序错误,所以我认为最好可以使用管道......我只是想知道 CombinedOutput 的默认 go 实现如何工作(golang.org/src/os/exec/exec.go?s=10901:10947#L404),因为它们只需分配应该与具有互斥锁的 writer 完全相同的 bytes.Buffer 。我错过了什么吗?
猜你喜欢
  • 2018-10-31
  • 2020-05-30
  • 2017-04-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-13
  • 2019-03-24
  • 2013-09-02
相关资源
最近更新 更多