dsc7188
dsc7188
2017-01-06 14:28

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

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享
  • 邀请回答

3条回答

  • douzi8916 douzi8916 4年前

    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
    
    点赞 评论 复制链接分享
  • dtoqemais553654797 dtoqemais553654797 4年前

    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()
    }
    
    点赞 评论 复制链接分享
  • dtpfia3334 dtpfia3334 4年前

    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.

    点赞 评论 复制链接分享

为你推荐