Go by Example

Exec'ing Processes

syscall.Exec (or unix.Exec) replaces the current process image entirely - deferred functions never run, so use it only when you intentionally want to hand off to another binary.

syscall.Exec (or golang.org/x/sys/unix.Exec on Unix) replaces the running process with a new executable. Unlike exec.Command, there is no child process - the current process image is overwritten. On success it never returns.

exec.LookPath resolves a binary name to a full path using $PATH, which syscall.Exec requires. Pass the full argument list as the second argument - by convention args[0] is the binary name.

package main
 
import (
    "fmt"
    "os"
    "os/exec"
    "syscall"
)
 
func main() {
    binary, err := exec.LookPath("ls")
    if err != nil {
        fmt.Println("could not find ls:", err)
        os.Exit(1)
    }
 
    args := []string{"ls", "-la", "/tmp"}
    env := os.Environ()
 
    // This call does not return on success.
    if err := syscall.Exec(binary, args, env); err != nil {
        fmt.Println("exec failed:", err)
        os.Exit(1)
    }
}

A common use case: a launcher or supervisor that re-execs itself after downloading an updated binary. Run any cleanup before calling Exec - deferred functions will not run.

package main
 
import (
    "fmt"
    "os"
    "os/exec"
    "syscall"
)
 
func reexec(newBinary string) error {
    path, err := exec.LookPath(newBinary)
    if err != nil {
        return fmt.Errorf("look path: %w", err)
    }
    return syscall.Exec(path, os.Args, os.Environ())
}
 
func main() {
    // Perform any cleanup before re-exec; defer won't run after Exec.
    fmt.Println("handing off to updated binary")
    if err := reexec("/usr/local/bin/myapp-new"); err != nil {
        fmt.Println("re-exec failed:", err)
        os.Exit(1)
    }
}

In production

syscall.Exec is for process-replacing wrappers - supervisors that re-exec on config reload, entrypoints that hand off to a different binary after initial setup, or shims that wrap another program. It does not return on success: any deferred functions, goroutines, and open file descriptors (other than those explicitly inherited) are lost. Do not use it as a substitute for exec.Command when you just want to run a subprocess and wait for it. On Linux the process keeps its PID, which is useful for PID-file-based process managers. Always flush logs and close non-inherited resources before calling Exec - there is no cleanup window after it succeeds. On non-Unix systems (Windows), syscall.Exec is not available; use exec.Command + os.Exit as an approximation, accepting that the process image is not replaced in place.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free