Golang中的可选超时

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()
}