使用相同频道的不同呼叫

different call using the same channel

您使用的是哪个版本的 Go (go version)?

$ go version 1.13.1

最新版本是否重现此问题?

我不确定。

您使用的是什么操作系统和处理器架构 (go env)?

$ go env
GO111MODULE="auto"
GOARCH="amd64"
GOBIN="/usr/local/go/bin"
GOCACHE="/data/xieyixin/.cache/go-build"
GOENV="/data/xieyixin/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/data/xieyixin/go"
GOPRIVATE=""
GOPROXY="http://10.0.12.201:8989/"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/data/xieyixin/hxagent/go.mod"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build474907248=/tmp/go-build"

你做了什么?

我写了一个函数来执行exec命令,并自己控制超时情况。 我也是这样测试的

package utils

import (
    "bytes"
    "context"
    "log"
    "os/exec"
    "syscall"
    "time"
)

func ExecCommand(command string, timeout time.Duration) (string, error) {
    log.Printf("command:%v, timeout:%v", command, timeout)
    var (
        cmd    *exec.Cmd
        stdout bytes.Buffer
        stderr bytes.Buffer
        result string
        err    error
        //timeouterr error
    )
    ctx, cancelFn := context.WithTimeout(context.Background(), timeout)
    defer cancelFn()

    cmd = exec.Command("bash", "-c", "--", command)
    cmd.Stdout = &stdout
    cmd.Stderr = &stderr
    cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

    var waitDone = make(chan struct{})
    defer func() {
        log.Printf("waitDone addr:%v\n", &waitDone)
        log.Printf("close waitdone channel\n")
        close(waitDone)
    }()
    go func() {
        err = cmd.Run()
        log.Printf("waitDone addr:%v\n", &waitDone)
        waitDone <- struct{}{}
    }()

    select {
    case <-ctx.Done():
        log.Printf("timeout to kill process, %v", cmd.Process.Pid)
        syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
        result = convertStr(stdout)
        err = ctx.Err()
    case <-waitDone:
        if err != nil {
            result = convertStr(stderr)
        } else {
            result = convertStr(stdout)
        }
    }

    log.Printf("result:%v,err:%v", result, err)

    return result, err
}

func convertStr(buffer bytes.Buffer) string {
    data := buffer.String()
    return data
}
package utils

import (
    "context"
    "testing"
    "time"
)

func TestExecCommand(t *testing.T) {
    tests := []struct {
        command string
        timeout time.Duration
        wantErr string
        want    string
    }{
        {
            command: "sleep 10",
            timeout: time.Second * 5,
            wantErr: context.DeadlineExceeded.Error(),
        },
        {
            command: "watch -n 1 date +%s",
            timeout: time.Second * 10,
            wantErr: context.DeadlineExceeded.Error(),
            want:    "timeout, but still have result.",
        },
        {
            command: "hostname",
            timeout: time.Second * 5,
            wantErr: "",
            want:    "anything result would be fine.",
        },
    }

    for _, tt := range tests {
        // got panic here. 
        // send on closed channel.
        got, gotErr := ExecCommand(tt.command, tt.timeout)
        if gotErr == nil {
            if tt.wantErr == "" {
                t.Logf("succeed")
            } else {
                t.Errorf("failed case: %+v, got:%v, gotErr:%v\n", tt, got, gotErr)
            }
        } else if gotErr.Error() == tt.wantErr {
            t.Logf("succeed")
        } else {
            t.Errorf("failed case: %+v, got:%v, gotErr:%v\n", tt, got, gotErr)
        }

    }
}

您希望看到什么?

测试正常。

你看到了什么?

恐慌:在关闭的频道上发送。

edit1 : 我自己控制上下文的原因 [https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773]

edit2:这里更混乱。

我好像有点明白了。但我还有一些问题。 [https://golang.org/src/os/exec/exec.go?s=11462:11489#L440],

if c.ctx != nil {
        c.waitDone = make(chan struct{}) // here
        go func() {
            select {
            case <-c.ctx.Done():
                c.Process.Kill()
            case <-c.waitDone: // and here
            }
        }()
    }

如您所见,它类似于@Cerise Limón 的代码。我们为什么要写这个频道。有必要吗?

当您的 ExecCommand 函数退出时,您正在关闭频道。 由于您是在 goroutine 中发送消息,因此没有 gua运行tee 它会在函数退出之前发送。事实上,我运行的所有时间都是在之后发生的。 如果没有第一个 deferable,您的测试将正常工作。

    defer func() {
        log.Printf("waitDone addr:%v\n", &waitDone)
        log.Printf("close waitdone channel\n")
        close(waitDone) // <- here 
    }()

    go func() {
        err = cmd.Run()
        log.Printf("waitDone addr:%v\n", &waitDone)
        waitDone <- struct{}{}  // <- and here
    }()

更新: @Cerise-Limón 指出您可以在 cmd 调用中使用上下文。 由于您已经在使用超时上下文,因此这将适合 perfeclty

cmd = exec.CommandContext(ctx, "bash", "-c", "--", command)
// cmd = exec.Command("bash", "-c", "--", command)

这可以避免您使用这种复杂的逻辑来检查超时。

考虑使用 exec.CommandContext 而不是自己编写此代码。

在命令完成之前上下文超时的场景下,ExecCommand函数可以在运行 goroutine发送到通道之前关闭通道。这会引起恐慌。

因为应用程序在 close(waitDone) 执行后没有在 waitDone 上接收,所以关闭通道没有意义。

如果关闭频道的代码被移除,则会暴露另一个问题。因为 waitDone 是一个无缓冲通道,运行 goroutine 在超时情况下将永远阻塞发送到 waitDone

cmd.Run()的调用启动一个goroutine将数据复制到stdoutstderr。无法保证这些 goroutine 在 ExecCommand 调用 convertStr(stdout)convertStr(stderr).

之前完成执行

这里有一个解决所有问题的办法:

func ExecCommand(command string, timeout time.Duration) (string, error) {
    log.Printf("command:%v, timeout:%v", command, timeout)
    ctx, cancelFn := context.WithTimeout(context.Background(), timeout)
    defer cancelFn()

    var stdout, stderr bytes.Buffer

    cmd := exec.Command("bash", "-c", "--", command)
    cmd.Stdout = &stdout
    cmd.Stderr = &stderr
    cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

    err := cmd.Start()
    if err != nil {
        return "", err
    }

    go func() {
        <-ctx.Done()
        if ctx.Err() == context.DeadlineExceeded {
            log.Printf("timeout to kill process, %v", cmd.Process.Pid)
            syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
        }
    }()

    err = cmd.Wait()
    var result string
    if err != nil {
        result = stderr.String()
    } else {
        result = stdout.String()
    }
}