Go exec command
last modified October 30, 2026
In this article we show how to execute external commands and programs in Go.
The os/exec package starts a process, connects its standard input,
output and error streams, and reports how the process ended.
Run starts a command and waits for it to finish. Start
starts it and returns immediately; Wait then waits for the exit and
releases the resources. Output and CombinedOutput are
shortcuts that start the command, collect its output and wait.
The examples were compiled and run with Go 1.27.0 on Linux and use standard
Unix programs such as echo, tr and
sh. The examples that use files or a working directory run in
/tmp/goexec.
Running a command
exec.Command takes the program name followed by its arguments.
Output runs the command and returns its standard output.
package main
import (
"fmt"
"os/exec"
)
func main() {
out, err := exec.Command("echo", "there", "are three", "falcons").Output()
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%q\n", string(out))
}
The arguments are passed as separate strings, so the space inside
"are three" does not split it into two arguments. The output
includes the newline that echo writes.
$ go run main.go "there are three falcons\n"
Standard error and exit codes
Output captures only standard output. When a command writes to
standard error or exits with a non-zero status, the returned error is an
*exec.ExitError, which carries the exit code.
package main
import (
"errors"
"fmt"
"os/exec"
)
func main() {
cmd := exec.Command("sh", "-c", "echo out; echo err >&2; exit 3")
out, err := cmd.CombinedOutput()
fmt.Printf("output: %q\n", string(out))
fmt.Println("error: ", err)
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
fmt.Println("code: ", exitErr.ExitCode())
}
}
CombinedOutput interleaves both streams into one slice. The error
text is exit status 3, and errors.As exposes the
numeric code for program logic.
$ go run main.go output: "out\nerr\n" error: exit status 3 code: 3
Sending standard input
The Stdin field of a command accepts any
io.Reader. A string reader is enough to feed a filter such as
tr.
package main
import (
"fmt"
"os/exec"
"strings"
)
func main() {
cmd := exec.Command("tr", "a-z", "A-Z")
cmd.Stdin = strings.NewReader("and old falcon\n")
out, err := cmd.Output()
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%q\n", string(out))
}
The command reads the string as if it came from a pipe or a terminal. The result is the upper-cased text followed by the original newline.
$ go run main.go "AND OLD FALCON\n"
Connecting two commands
StdoutPipe returns a reader connected to the command's standard
output. Assigning it to the next command's Stdin builds a pipeline
in the same way as the shell's | operator.
package main
import (
"fmt"
"os/exec"
)
func main() {
echo := exec.Command("echo", "one two three")
upper := exec.Command("tr", "a-z", "A-Z")
pipe, err := echo.StdoutPipe()
if err != nil {
fmt.Println(err)
return
}
upper.Stdin = pipe
if err := echo.Start(); err != nil {
fmt.Println(err)
return
}
out, err := upper.Output()
if err != nil {
fmt.Println(err)
return
}
if err := echo.Wait(); err != nil {
fmt.Println(err)
return
}
fmt.Printf("%q\n", string(out))
}
The first command must be started before the second one can read from its
pipe. Both processes run concurrently, and Wait collects the exit
status of echo after the output has been consumed.
$ go run main.go "ONE TWO THREE\n"
Streaming output line by line
For a long-running command, reading the whole output into memory is wasteful.
StdoutPipe together with bufio.Scanner processes each
line as soon as it is written.
package main
import (
"bufio"
"fmt"
"os/exec"
)
func main() {
cmd := exec.Command("sh", "-c", "printf 'alpha\\nbeta\\ngamma\\n'")
stdout, err := cmd.StdoutPipe()
if err != nil {
fmt.Println(err)
return
}
if err := cmd.Start(); err != nil {
fmt.Println(err)
return
}
scanner := bufio.NewScanner(stdout)
n := 0
for scanner.Scan() {
n++
fmt.Printf("%d: %s\n", n, scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Println(err)
}
if err := cmd.Wait(); err != nil {
fmt.Println(err)
}
}
The loop runs while the pipe is open and the scanner has data. The scanner
error is checked after the loop, and Wait reports a non-zero exit
status of the child.
$ go run main.go 1: alpha 2: beta 3: gamma
Writing to a stdin pipe
StdinPipe returns a writer connected to the command's standard
input. Writing must happen while the command runs, so it is usually done in a
goroutine that closes the pipe when it is done.
package main
import (
"fmt"
"io"
"os/exec"
)
func main() {
cmd := exec.Command("cat")
stdin, err := cmd.StdinPipe()
if err != nil {
fmt.Println(err)
return
}
go func() {
defer stdin.Close()
io.WriteString(stdin, "an old falcon\n")
}()
out, err := cmd.Output()
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%q\n", string(out))
}
Closing the pipe is what signals end-of-file to cat. Without it,
the command would wait for more input and Output would block.
$ go run main.go "an old falcon\n"
Working directory and environment
Cmd.Dir sets the working directory of the child, and
Cmd.Env replaces its environment. cmd.Environ
returns a clean copy of the inherited environment that can be extended.
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
cmd := exec.Command("sh", "-c", `pwd; echo "$APP_NAME"`)
cmd.Dir = "work"
cmd.Env = append(cmd.Environ(), "APP_NAME=falcon")
out, err := cmd.Output()
if err != nil {
fmt.Println(err)
return
}
fmt.Print(string(out))
wd, _ := os.Getwd()
fmt.Println("parent cwd:", wd)
}
The child runs in the work subdirectory while the parent keeps its
own directory. The relative path in Dir is resolved against the
parent's working directory.
$ go run main.go /tmp/goexec/work falcon parent cwd: /tmp/goexec
Timeouts
exec.CommandContext ties the process to a context. When the
context expires, the command is killed and the returned error reports the
signal.
package main
import (
"context"
"errors"
"fmt"
"os/exec"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
cmd := exec.CommandContext(ctx, "sleep", "5")
_, err := cmd.Output()
fmt.Println("error: ", err)
fmt.Println("context:", ctx.Err())
fmt.Println("deadline:", errors.Is(ctx.Err(), context.DeadlineExceeded))
}
The command would sleep for five seconds but is killed after one hundred milliseconds. The process error and the context error describe the same event from two sides.
$ go run main.go error: signal: killed context: context deadline exceeded deadline: true
Finding a program
exec.Command searches PATH when the program name
contains no slash. exec.LookPath performs the same search without
starting anything, which is useful for checking dependencies at startup.
package main
import (
"fmt"
"os/exec"
)
func main() {
path, err := exec.LookPath("go")
fmt.Println(path, err)
_, err = exec.LookPath("no-such-program")
fmt.Println(err)
}
The absolute path depends on the installation. A missing program produces an error that names the program and the searched variable.
$ go run main.go /usr/local/go/bin/go <nil> exec: "no-such-program": executable file not found in $PATH
Source
Go os/exec package - reference
In this article we have executed external commands in Go. We captured output
and standard error, inspected exit codes, fed data through standard input,
built pipelines, streamed output line by line, set the working directory and
environment, applied a context timeout and looked up programs on the
PATH.
Author
List all Go tutorials.