Go by Example

Spawning Processes

exec.Command runs a subprocess - use separate string arguments, not shell interpolation, to prevent command injection from user-supplied input.

exec.Command creates a *exec.Cmd that represents an external process. The arguments are passed directly to execve - no shell is involved, so shell metacharacters are inert. Run the command with Output (captures stdout), Run (waits for completion), or Start + Wait (async).

Cmd.Output runs the command, waits for it to finish, and returns stdout as a byte slice. Cmd.CombinedOutput captures both stdout and stderr together.

package main
 
import (
    "fmt"
    "os/exec"
)
 
func main() {
    out, err := exec.Command("ls", "-la", "/tmp").Output()
    if err != nil {
        fmt.Println("error:", err)
        return
    }
    fmt.Print(string(out))
}

Cmd.Start launches the process asynchronously. Cmd.Wait blocks until it exits and collects the exit code. Use this pattern when you want to do other work while the subprocess runs, or when you need to stream output.

package main
 
import (
    "fmt"
    "os/exec"
)
 
func main() {
    cmd := exec.Command("sleep", "1")
    if err := cmd.Start(); err != nil {
        fmt.Println("start error:", err)
        return
    }
    fmt.Println("process started, pid:", cmd.Process.Pid)
 
    if err := cmd.Wait(); err != nil {
        fmt.Println("wait error:", err)
        return
    }
    fmt.Println("process finished")
}

Pipe stdout and stderr for streaming output. Attach os.Stdin to allow the subprocess to receive user input.

package main
 
import (
    "fmt"
    "os"
    "os/exec"
)
 
func main() {
    cmd := exec.Command("go", "version")
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
 
    if err := cmd.Run(); err != nil {
        fmt.Println("error:", err)
        return
    }
}

In production

Never interpolate user input into exec.Command as a shell string - pass arguments as separate strings (exec.Command("ls", "-la", userPath)) which are exec'd directly without a shell interpreter. Shell injection (exec.Command("sh", "-c", "ls "+userInput)) is the most common security vulnerability in process-spawning code and allows arbitrary command execution when userInput contains shell metacharacters. For the rare case where you genuinely need a shell (pipelines, glob expansion), validate and sanitize input aggressively or use a subprocess allowlist. Always set a timeout via context.WithTimeout and pass the context to exec.CommandContext - a subprocess that hangs indefinitely holds the goroutine and its resources. Check the error from cmd.Wait: it is non-nil for non-zero exit codes, which are often meaningful (grep returns 1 when no match is found). To capture the exit code specifically, use var exitErr *exec.ExitError; errors.As(err, &exitErr) and inspect exitErr.ExitCode().

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

Subscribe free