Executing Commands In Go

The ability to execute external commands from within an application is something I often feel is a bit hackish and I haven’t yet discovered a language that handles it as well as I would like. That was until I learned to love how Go tackles the challenge. This post will show you how to make the most of os/exec.

I tend to feel that there are three different types of command execution within applications.

  • Plain output. For when you always expect the command to execute but all you want from it is the output.
  • Exit codes. These perform some cleanup, setup or check and you want to discard any output but assert the exit code.
  • Long running processes. This case is rarer but there are times when I want to spawn sub-processes. Maybe even starting up another server to proxy to for instance.

So how do I handle these in Go? The magic all comes from the wonderful os/exec which is part of the Go standard library. Using this library, commands become a first class citizen within your application.

For all examples I am only going to include the primary logic but I will keep a link to a full copy in the Go Playgroud. The copies won’t execute within the Playground due to the restrictions Google have in place but they should run fine locally.

I have defined the following functions to keep the examples short:

func printCommand(cmd *exec.Cmd) {
  fmt.Printf("==> Executing: %s\n", strings.Join(cmd.Args, " "))

func printError(err error) {
  if err != nil {
    os.Stderr.WriteString(fmt.Sprintf("==> Error: %s\n", err.Error()))

func printOutput(outs []byte) {
  if len(outs) > 0 {
    fmt.Printf("==> Output: %s\n", string(outs))

Collecting output

The first and most obvious use is to collect output from an external command. An easy way to do that is to use the CombinedOutput function.

// Create an *exec.Cmd
cmd := exec.Command("echo", "Called from Go!")

// Combine stdout and stderr
output, err := cmd.CombinedOutput()
printOutput(output) // => go version go1.3 darwin/amd64

// http://play.golang.org/p/-7PWDpt6zS

This works well if you also want to check for any error messages output but if you want finer control over the output of a command then we can route it into different buffers giving us control over both standard output and standard error.

// Create an *exec.Cmd
cmd := exec.Command("go", "version")

// Stdout buffer
cmdOutput := &bytes.Buffer{}
// Attach buffer to command
cmd.Stdout = cmdOutput

// Execute command
err := cmd.Run() // will wait for command to return
// Only output the commands stdout
printOutput(cmdOutput.Bytes()) // => go version go1.3 darwin/amd64

// http://play.golang.org/p/_6xke11GMp

In the above example we manually connect our own buffer to capture the commands stdout stream. We can do the same for stderr and even stdin so long as it adheres to the io.Reader interface.

So far we’ve seen how easy it is to capture command output across multiple file descriptors. It’s more verbose then other languages but it gives us lots of flexibility.

Exit codes

Retrieving the exit code of a command is easy with Go. You may have already noticed in the previous examples that when executing the command Go can return an error. These errors occur if there is an issue with IO or of the command doesn’t return a successful exit code (0).

The following code will output the exit code of a command.

cmd := exec.Command("ls", "/imaginary/directory")
var waitStatus syscall.WaitStatus
if err := cmd.Run(); err != nil {
  // Did the command fail because of an unsuccessful exit code
  if exitError, ok := err.(*exec.ExitError); ok {
    waitStatus = exitError.Sys().(syscall.WaitStatus)
    printOutput([]byte(fmt.Sprintf("%d", waitStatus.ExitStatus())))
} else {
  // Command was successful
  waitStatus = cmd.ProcessState.Sys().(syscall.WaitStatus)
  printOutput([]byte(fmt.Sprintf("%d", waitStatus.ExitStatus())))

// http://play.golang.org/p/m2A17UWSOL

The above code looks more complicated but in traditional Go fashion it handles many situations.

  • Create an *exec.Cmd
  • Execute and test for any errors
  • If we received an error then check it was because of the commands exit
  • If no error then check the commands *os.ProcessState for the exit code (pretty much guaranteed to be 0 but in here for completion)

A caveat to take note of is that if Go failed to locate the command in your $PATH then it won’t ever execute the command and thus you will have no exit code. This is why it is important to assert the type of error returned.

Long running processes

All our above examples are synchronous. They wait for the command to complete before continuing the execution of our application. If we wanted to execute a long running task however it is likely that we want it to happen asynchronously. Once again this is trivially easy.

cmd := exec.Command("cat", "/dev/random")
randomBytes := &bytes.Buffer{}
cmd.Stdout = randomBytes

// Start command asynchronously
err := cmd.Start()

// Create a ticker that outputs elapsed time
ticker := time.NewTicker(time.Second)
go func(ticker *time.Ticker) {
  now := time.Now()
  for _ = range ticker.C {
      []byte(fmt.Sprintf("%s", time.Since(now))),

// Create a timer that will kill the process
timer := time.NewTimer(time.Second * 4)
go func(timer *time.Timer, ticker *time.Ticker, cmd *exec.Cmd) {
  for _ = range timer.C {
    err := cmd.Process.Signal(os.Kill)
}(timer, ticker, cmd)

// Only proceed once the process has finished
  []byte(fmt.Sprintf("%d bytes generated!", len(randomBytes.Bytes()))),

// http://play.golang.org/p/tQRk1xJOqW

Now that was a lot more work but it all makes good sense. I started a computationaly difficult task of generating a collection of random bytes. Next I start that command asynchronously and then begin a ticker to show the elapsed time and a timer to kill the process after 4 seconds. Once the process has been killed then we output the total number of generated bytes.

A small disclaimer. I don’t claim that this is the best way to do this but it demonstrates asynchronous commands and the ability to send signals to the process within our application.

Closing comments

This has been quite a technical post but we have covered lots of ground with the flexibility of os/exec. I have tested all the examples on my Mac using go1.3. Any suggestions/improvements are welcomed.

I hope I’ve been able to get across why I’m beginning to really enjoy working with Go.