Caius Theory

Now with even more cowbell…

exec(3) in Go

Using exec(3) from Go is simple enough, once you figure out to look in the syscall package and how to pass arguments to the new command.

As a simple example, I’m going to exec /bin/echo with a hardcoded string from the go binary. The program built here is in the gecho (Gecko, geddit?) git repo, which each stage as a commit.

In our main function lets setup some variables we’re going to need for arguments to syscall.Exec:

func main() {
  cmdPath := "/bin/echo"
  cmdArgs := []string{"Hello", "World"}
  cmdEnv := []string{}    
}

(We could use os.Environ() for cmdEnv to take the ENV from the go binary, but we don’t require anything from the environmnt here so it doesn’t matter that we aren’t.)

Now we have the arguments for syscall.Exec, lets add that in and see what happens:

err := syscall.Exec(cmdPath, cmdArgs, cmdEnv)
if err != nil {
  panic(err)
}

And running the file (go run gecho.go compiles & runs for us) gives the following output:

World

Err, say what now? Where’s “Hello” gone?!

Took me a while to figure this out when I originally ran into this. The answer is staring us right in the face if we go look at the syscall.Exec docs. Lets have a look at the function signature, argument names and all:

func Exec(argv0 string, argv []string, envv []string) (err error)

Hmm. The first argument is argv0 (and a string), rather than binaryPath or something similar. The second argument is then argv and an array of strings.

At this point I remember that the first element of argv in other runtimes is the name of the binary or command invoked - $0 in a bash script is the name of the script for example.

The answer is simple. cmdArgs in our script should have /bin/echo as the first element, and then we pass cmdArgs[0], cmdArgs as the first two arguments to syscall.Exec. Lets give that a go:

func main() {
  cmdArgs := []string{"/bin/echo", "Hello", "World"}
  cmdEnv := []string{}

  err := syscall.Exec(cmdArgs[0], cmdArgs, cmdEnv)
  if err != nil {
    panic(err)
  }
}

And running it (go run gecho.go remember) gives the expected output:

Hello World

Excellent. Now I just need to remember argv contains the command name as argv[0] and we’re golden.


There is also the os/exec package in the stdlib, which is intended for executing other binaries as child processes from what I can tell. Tellingly, when you create a exec.Cmd struct with exec.Command() you give it the name as first argument, and args as following arguments. Then it has the following snippet in the documentation:

The returned Cmd’s Args field is constructed from the command name followed by the elements of arg, so arg should not include the command name itself. For example, Command("echo", "hello")

So cmd := exec.Command("echo", "hello"); cmd.Args would return []string{"echo", "hello"} - which is recognisable as what we have to pass to syscall.Exec!