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 exec package executes external commands. It wraps os.StartProcess to 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 completion
  • Output(): Runs command and captures output
  • Start(): Starts command non-blocked
  • Wait(): 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:

  1. Tar up a directory
  2. Builds a Docker image
  3. 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!

Similar Posts