使用相同频道的不同呼叫
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将数据复制到stdout
和stderr
。无法保证这些 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()
}
}
您使用的是哪个版本的 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将数据复制到stdout
和stderr
。无法保证这些 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()
}
}