The exec package in Go provides a versatile interface for spawning and interacting with external processes. As a full-stack developer, having advanced familiarity with exec can unlock new levels of speed, reliability, and automation in your programs.
In this comprehensive 2600+ word guide, you will gain deep mastery over process execution from Go code. Follow along as we cover:
- Exec package basics
- Running & piping commands
- Capturing output and exit codes
- Security considerations
- Performance optimizations
- Platform-specific usage
- Real-world examples
- And much more!
Whether deploying containerized Go services across Kubernetes clusters or building CLI automation tools, understanding exec is essential. Let‘s get started!
Exec Package Basics
The exec package wraps OS-level process spawning capabilities into a high-level, idiomatic Go interface.
According to the Go documentation:
The
execpackage executes external commands. It wrapsos.StartProcessto make it easier to remap stdin and stdout, connect I/O with pipes, and do other adjustments.
In other words, it simplifies executing external programs from Go code compared to using syscalls directly.
To use the exec package, first import it:
import "os/exec"
The central abstraction is the *Cmd struct. We can build commands by calling exec.Command():
cmd := exec.Command("prog", "arg1", "arg2")
This prepares the command, but does not execute it yet. To actually run the process, call methods like:
Run(): Starts command and waits for completionOutput(): Runs command and captures outputStart(): Starts command non-blockedWait(): Blocks waiting for command exit
Let‘s look at a basic example:
package main
import (
"log"
"os/exec"
)
func main() {
cmd := exec.Command("echo", "hello world")
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
}
Now that we have covered the key concepts and API, let‘s explore common usage patterns.
Running External Commands
Calling the Run() method on a *Cmd struct executes the process and blocks until completion. But often you‘ll want more control over command execution.
Let‘s discuss some alternative techniques for running external processes from Go programs.
Start Non-Blocked Commands
To start a long-running process like a server without blocking, use cmd.Start():
cmd := exec.Command("daemon")
cmd.Start() // Does not block!
// Continue program logic...
The command launches asynchronously. You can wait later by calling cmd.Wait() to block on exit.
Capture Output
Frequently, you‘ll want to collect the stdout and stderr streams of a process.
Use cmd.Output() to run a command and capture all output:
out, err := exec.Command("prog").Output()
This runs prog, waits for exit, and returns the combined stdout/stderr. You can convert out from []byte to string and process as needed.
Alternatively, capture streams separately:
cmd := exec.Command("prog")
stdout, _ := cmd.StdoutPipe()
stderr, _ := cmd.StderrPipe()
...
err := cmd.Run()
This gives you independent io.Reader pipes for greater control.
As a real-world example, let‘s capture the output of listing an S3 bucket locally using the AWS CLI tool:
$ aws s3 ls s3://my-bucket
2022-10-11 17:32:44 81981162 my-file.zip
2022-10-05 10:15:02 1024 my-text-file.txt
We can implement this in Go using exec to capture the listing output programmatically:
package main
import (
"fmt"
"log"
"os/exec"
)
func main() {
cmd := exec.Command("aws", "s3", "ls", "s3://my-bucket")
output, err := cmd.Output()
if err != nil {
log.Fatalf("Failed: %s", err)
}
results := string(output[:])
fmt.Println(results)
}
Running this program would print the AWS CLI‘s s3 ls output showing objects in the S3 bucket.
Very useful for scripting!
Send Input Streams
For advanced inter-process communication, send data to stdin of commands using io.Write():
cmd := exec.Command("wc", "-l")
stdin, _ := cmd.StdinPipe()
go func() {
defer stdin.Close()
io.WriteString(stdin, "hello world\ngoodbye world")
}()
out, _ := cmd.CombinedOutput()
fmt.Printf("%s", out) // prints "2"
Here we pipe input text into wc -l to count lines. The -l flag counts newlines so it correctly prints 2.
Piping input/output between programs enables awesome scripting capabilities directly in Go!
Working Directory
Often it‘s important to set the working directory before running a command.
Use cmd.Dir to control this:
cmd := exec.Command("build")
cmd.Dir = "cmd/subdir"
cmd.Run() // Builds in subdirectory
This launches build with cmd/subdir as the working directory.
Environment Variables
To specify environment variables for a process, use cmd.Env:
cmd.Env = append(os.Environ(),
"API_KEY=secret123",
)
This sets API_KEY=secret123 in the child process‘s environment.
Append vars to inherit parent env too using os.Environ().
As an example, we could pass AWS credentials to a CLI:
cmd.Env = append(os.Environ(),
"AWS_ACCESS_KEY_ID=AKIAxxxx",
"AWS_SECRET_ACCESS_KEY=xxxxx"
)
cmd.Run() // Child has credentials
Now let‘s discuss some best practices around security, performance, and platform-specific usage.
Security Best Practices
When executing commands, especially based on user input, security is critical. Follow these best practices:
Validate Input
If building a user-facing CLI tool, properly validate and sanitize any arguments before passing to exec.Command(), otherwise this risks command injection if untrusted input gets interpolated into the command string.
Consider a program like:
// UNSAFE! Don‘t do this!
func main() {
userInput := os.Args[1]
exec.Command("ping", userInput)
}
If called with ./program ; rm -rf /, this would delete everything!
Instead, properly validate and escape arguments.
Sandbox Commands
When possible, run untrusted code in a restricted sandbox environment using OS capabilities like Linux namespaces, cgroups, AppArmor, SELinux, etc.
For example:
# Sandboxed environment
unshare --mount --pid --net -->
# Limited resource usage
cgcreate --group limited # cgroups
exec.Command(untrustedCode) # Runs safely
This securely isolates the process from wider access.
Drop Privileges
If a subprocess requires elevated permissions, consider dropping privileges after doing privileged tasks:
// Privileged portion
exec.Command("setcap", "cap_net_bind_service=+ep", "server")
// Drop privileges
syscall.Setuid(999)
// Start server
exec.Command("./server")
This better adheres to principle of least privilege.
Following security best practices will help write resilient, production-ready programs leveraging exec!
Performance Optimizations
While incredibly useful, frequently spawning processes introduces performance overhead. Apply these optimizations:
Pool Reusable Commands
Initialize commands once globally and reuse them in a pool instead of repetitively re-parsing arguments:
var echoHello = exec.Command("echo", "hello") // Initialize once!
func HandleRequest() {
echoHello.Run() // Reuse pristine command
}
This amortizes the overhead of command creation across invocations.
Asynchronous Execution
Utilize goroutines to concurrently execute processes instead of blocking throughput:
cmds := []*exec.Cmd{...} // Many commands
for _, cmd := range cmds {
go runCmdAsync(cmd)
}
// Continue handling requests...
Goroutines enable massively parallel processing.
Avoid Unnecessary Commands
Compare alternatives to avoid launching processes where possible. For example:
import "strings"
data := GetHugeJSONBlob()
// Don‘t shell out!
n := exec.Command("jq", ".user_count", data).Output()
// More efficient:
count := gjson.Get(data, "user_count")
Here, gjson parses JSON natively avoiding an expensive jq process.
Apply these optimizations to build blazing fast, production-grade systems with exec!
Platform-Specific Usage
While Go promotes cross-platform portability, sometimes OS-specific interop is necessary.
Let‘s discuss some platform-specific usage of exec.
Windows
On Windows, file extensions matter. Use .EXE, .BAT, etc:
exec.Command("program.exe") // Windows
And utilize backslash path separators:
cmd.Dir = "C:\Files\Scripts" // Windows
Other Windows-specific environment handling may be needed.
Linux
On Linux, leverage shebang lines for interpreter resolution:
#!/bin/bash
# ^-- Indicates bash should execute
exec.Command("./script.sh") // Runs in bash
Also utilize POSIX features like signals:
cmd.Process.Signal(syscall.SIGHUP) // Linux signal
For containerized environments, namespaced syscalls integrate well.
MacOS
MacOS inherits many UNIX and POSIX conventions:
cmd.Dir = "/Users/name/folder" // UNIX paths
cmd.Process.Signal(syscall.SIGKILL) // Signals
But note Mac has a hybrid UNIX/Darwin kernel and userland environment. Adjust accordingly.
While Go promotes write-once run-anywhere code, be sure to consider platform nuances around spawning processes!
Real-World Production Examples
We have covered a wide range of exec usage, from basics to advanced techniques. Now let‘s look at some real-world examples demonstrating the power of exec in production systems.
// Docker Build Automation
tarCmd := exec.Command("tar", "-czf", "app.tar.gz", "myapp")
dockerBuildCmd := exec.Command("docker", "build", "-t", "app/web-server", ".")
dockerRunCmd := exec.Command("docker", "run", "-p", "8080:8080", "app/web-server")
BuildPipeline(tarCmd, dockerBuildCmd, dockerRunCmd)
This pipeline shells out to:
- Tar up a directory
- Builds a Docker image
- Launches the container
Automating shell commands easily builds complex workflows!
// Kubernetes Deployment
deploy := exec.Command("/opt/kubectl",
"-n", "staging",
"rollout", "restart", "deployment/app",
)
deploy.Run() // Zero downtime rollout!
Here exec facilitates declarative Kubernetes deployment orchestration from within a Go program!
// Backup Cron Job
func DailyBackup() {
dump := exec.Command("/usr/bin/pg_dump", "-Fc", "db")
gzip := exec.Command("/usr/bin/gzip", backupDumpFile)
UploadToS3(gzip) // Next step...
}
This shells out to PostgreSQL + Gzip for database backups, piping outputs to AWS S3!
As shown, exec unlocks incredibly powerful automation capabilities used widely across real-world systems.
Closing Thoughts
We have covered extensive ground on process execution and automation using Go‘s exec package, including:
- Constructing programmatic commands
- Running processes asynchronously
- Capturing and piping output
- Optimizing resource usage
- Platform-specific interop
- And overviews of actual production use cases
Whether you are orchestrating Docker clusters, deploying apps on Kubernetes, or building advanced CLI tools, having deep familiarity with spawning processes from Go is invaluable.
Be sure to apply security best practices around sandboxing, privilege separation, and validating inputs.
For further learning, study the official Go Documentation and os/exec source code.
I hoped you enjoyed this comprehensive 2600+ word deep dive into Go‘s exec package! Let me know if you have any other process automation topics you would like covered in the future!
Happy coding!


