Golang中的可选超时
I have a function which runs a command with a timeout. It looks like this:
func run_command(cmdName string, cmdArgs []string, timeout int) (int, string) {
// the command we're going to run
cmd := exec.Command(cmdName, cmdArgs...)
// assign vars for output and stderr
var output bytes.Buffer
var stderr bytes.Buffer
// get the stdout and stderr and assign to pointers
cmd.Stderr = &stderr
cmd.Stdout = &output
// Start the command
if err := cmd.Start(); err != nil {
log.Fatalf("Command not found: %s", cmdName)
}
timer := time.AfterFunc(time.Second*time.Duration(timeout), func() {
err := cmd.Process.Kill()
if err != nil {
panic(err)
}
})
// Here's the good stuff
if err := cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
// Command ! exit 0, capture it
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
// Check it's nagios compliant
if status.ExitStatus() == 1 || status.ExitStatus() == 2 || status.ExitStatus() == 3 {
return status.ExitStatus(), stderr.String()
} else {
// If not, force an exit code 2
return 2, stderr.String()
}
}
} else {
log.Fatalf("cmd.Wait: %v", err)
}
timer.Stop()
}
// We didn't get captured, continue!
return 0, output.String()
}
Now I want to be able to make the timeout optional. In order to fudge this a bit, I tried simply allowing timeout to be set to 0
and then having an if statement around the timer. It ended up looking like this.
if timeout > 0 {
timer := time.AfterFunc(time.Second*time.Duration(timeout), func() {
err := cmd.Process.Kill()
if err != nil {
panic(err)
}
})
}
Of course, this failed because timer is no longer defined timer.Stop()
isn't defined now.
So I wrapped the timer.Stop()
with the if statement as well.
if timeout > 0 {
timer.Stop()
}
This also didn't work.
What is the correct way to do something like this? Golangs strict typing is new to me, so I'm struggling to get my head around it
Using the context
package makes it easy to handle timeouts.
golang.org/x/net/context
has become a standard library since Go 1.7.
The following is an example:
package main
import (
"context"
"os"
"os/exec"
"strconv"
"time"
)
func main() {
timeout, err := strconv.Atoi(os.Args[1])
if err != nil {
panic(err)
}
ctx := context.Background()
if timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
}
cmd := exec.CommandContext(ctx, "sleep", "5")
if err := cmd.Run(); err != nil {
panic(err)
}
}
When timeout is set to 3 seconds, and run sleep 5
:
$ go run main.go 3
panic: signal: killed
goroutine 1 [running]:
panic(0xc7040, 0xc42008c020)
/usr/local/Cellar/go/1.7.4_1/libexec/src/runtime/panic.go:500 +0x1a1
main.main()
/Users/m-morita/work/tmp/20170106/main.go:27 +0x11c
exit status 2
When it is set to 10 seconds or 0(= never timeout), it ends normally:
$ go run main.go 10
$ go run main.go 0
While you could replace the timer func with a noop if there's no duration, the usual solution is to simply defer the timer.Stop
call when you create the timer:
timer := time.AfterFunc(time.Second*time.Duration(timeout), func() {
err := cmd.Process.Kill()
if err != nil {
panic(err)
}
})
defer timer.Stop()
Otherwise, you can declare timer
at the function scope and check if it was assigned before calling timer.Stop()
if timer != nil {
timer.Stop()
}
You should also note that an exec.Cmd
already makes use of a Context
for timeouts, which is exposed via exec.CommandContext
.
Simply define the timer variable before the first if timeout > 0 block and assign the timer to it using = instead of :=.
var timer *time.Timer
if timeout > 0 {
timer = time.AfterFunc(time.Second*time.Duration(timeout), func() {
err := cmd.Process.Kill()
if err != nil {
panic(err)
}
})
}
The check for timeout > 0 before timer.Stop() will still be necessary, or, to diminish dependencies, changed to timer != nil.
if timer != nil {
timer.Stop()
}