Process Management and Subprocess Control in Go
Introduction
Managing subprocesses is essential for CLI tools that need to execute external programs. Go provides powerful packages for process management, I/O handling, and signal management. This guide covers comprehensive subprocess control.
Proper subprocess management ensures your applications can reliably execute external programs and handle their output and errors.
Basic Process Execution
Simple Command Execution
package main
import (
"fmt"
"log"
"os/exec"
)
// ExecuteCommand executes a simple command
func ExecuteCommand(name string, args ...string) error {
cmd := exec.Command(name, args...)
if err := cmd.Run(); err != nil {
return fmt.Errorf("command failed: %w", err)
}
return nil
}
// ExecuteCommandWithOutput executes command and captures output
func ExecuteCommandWithOutput(name string, args ...string) (string, error) {
cmd := exec.Command(name, args...)
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("command failed: %w", err)
}
return string(output), nil
}
// Example usage
func SimpleExample() {
// Execute ls command
if err := ExecuteCommand("ls", "-la"); err != nil {
log.Fatal(err)
}
// Execute command with output
output, err := ExecuteCommandWithOutput("echo", "Hello, World!")
if err != nil {
log.Fatal(err)
}
fmt.Println(output)
}
Good: Proper Process Management Implementation
package main
import (
"bytes"
"context"
"fmt"
"io"
"log"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
)
// ProcessResult holds process execution result
type ProcessResult struct {
ExitCode int
Stdout string
Stderr string
Duration time.Duration
Error error
}
// ProcessManager manages subprocess execution
type ProcessManager struct {
timeout time.Duration
}
// NewProcessManager creates a new process manager
func NewProcessManager(timeout time.Duration) *ProcessManager {
return &ProcessManager{
timeout: timeout,
}
}
// Execute executes a command with timeout
func (pm *ProcessManager) Execute(ctx context.Context, name string, args ...string) *ProcessResult {
result := &ProcessResult{}
start := time.Now()
// Create context with timeout
if pm.timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, pm.timeout)
defer cancel()
}
cmd := exec.CommandContext(ctx, name, args...)
// Capture output
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
// Execute command
err := cmd.Run()
result.Duration = time.Since(start)
result.Stdout = stdout.String()
result.Stderr = stderr.String()
if err != nil {
result.Error = err
if exitErr, ok := err.(*exec.ExitError); ok {
result.ExitCode = exitErr.ExitCode()
}
}
return result
}
// ExecuteWithStreaming executes command with streaming output
func (pm *ProcessManager) ExecuteWithStreaming(ctx context.Context, name string, args ...string) error {
cmd := exec.CommandContext(ctx, name, args...)
// Stream stdout and stderr
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
return cmd.Run()
}
// ExecuteWithCallback executes command and calls callback for each line
func (pm *ProcessManager) ExecuteWithCallback(ctx context.Context, name string, callback func(string), args ...string) error {
cmd := exec.CommandContext(ctx, name, args...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
// Read output line by line
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
callback(scanner.Text())
}
return cmd.Wait()
}
// ExecuteParallel executes multiple commands in parallel
func (pm *ProcessManager) ExecuteParallel(ctx context.Context, commands []struct {
Name string
Args []string
}) []error {
results := make([]error, len(commands))
for i, cmd := range commands {
go func(index int, name string, args []string) {
result := pm.Execute(ctx, name, args...)
results[index] = result.Error
}(i, cmd.Name, cmd.Args)
}
// Wait for all to complete
time.Sleep(time.Second)
return results
}
// ExecuteWithSignalHandling executes command with signal handling
func (pm *ProcessManager) ExecuteWithSignalHandling(name string, args ...string) error {
cmd := exec.Command(name, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Handle signals
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
if err := cmd.Start(); err != nil {
return err
}
// Wait for signal or command completion
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
select {
case sig := <-sigChan:
// Forward signal to subprocess
cmd.Process.Signal(sig)
return <-done
case err := <-done:
return err
}
}
Bad: Improper Process Management
package main
import (
"os/exec"
)
// BAD: No error handling
func BadExecute(name string, args ...string) {
cmd := exec.Command(name, args...)
cmd.Run() // Ignoring error
}
// BAD: No timeout
func BadExecuteWithoutTimeout(name string, args ...string) {
cmd := exec.Command(name, args...)
cmd.Run() // Could hang indefinitely
}
// BAD: No output capture
func BadExecuteNoOutput(name string, args ...string) {
cmd := exec.Command(name, args...)
cmd.Run() // Output lost
}
Problems:
- No error handling
- No timeout management
- No output capture
- No signal handling
Advanced Process Control
Process Pooling
package main
import (
"context"
"fmt"
"os/exec"
"sync"
)
// ProcessPool manages a pool of processes
type ProcessPool struct {
maxConcurrent int
semaphore chan struct{}
wg sync.WaitGroup
}
// NewProcessPool creates a new process pool
func NewProcessPool(maxConcurrent int) *ProcessPool {
return &ProcessPool{
maxConcurrent: maxConcurrent,
semaphore: make(chan struct{}, maxConcurrent),
}
}
// Execute executes command with pool limits
func (pp *ProcessPool) Execute(ctx context.Context, name string, args ...string) error {
pp.wg.Add(1)
defer pp.wg.Done()
// Acquire semaphore
pp.semaphore <- struct{}{}
defer func() { <-pp.semaphore }()
cmd := exec.CommandContext(ctx, name, args...)
return cmd.Run()
}
// Wait waits for all processes to complete
func (pp *ProcessPool) Wait() {
pp.wg.Wait()
}
Process Monitoring
package main
import (
"fmt"
"os/exec"
"time"
)
// ProcessMonitor monitors process execution
type ProcessMonitor struct {
name string
args []string
maxRetries int
retryDelay time.Duration
}
// NewProcessMonitor creates a new process monitor
func NewProcessMonitor(name string, args []string) *ProcessMonitor {
return &ProcessMonitor{
name: name,
args: args,
maxRetries: 3,
retryDelay: time.Second,
}
}
// ExecuteWithRetry executes command with retry logic
func (pm *ProcessMonitor) ExecuteWithRetry() error {
var lastErr error
for attempt := 0; attempt < pm.maxRetries; attempt++ {
cmd := exec.Command(pm.name, pm.args...)
if err := cmd.Run(); err == nil {
return nil
} else {
lastErr = err
if attempt < pm.maxRetries-1 {
time.Sleep(pm.retryDelay)
}
}
}
return fmt.Errorf("command failed after %d attempts: %w", pm.maxRetries, lastErr)
}
Environment Variables
package main
import (
"fmt"
"os"
"os/exec"
)
// ExecuteWithEnv executes command with custom environment
func ExecuteWithEnv(name string, env map[string]string, args ...string) error {
cmd := exec.Command(name, args...)
// Set environment variables
cmd.Env = os.Environ()
for key, value := range env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value))
}
return cmd.Run()
}
// Example usage
func EnvExample() {
env := map[string]string{
"DEBUG": "true",
"LOG_LEVEL": "info",
}
ExecuteWithEnv("myapp", env, "--config", "config.yaml")
}
Best Practices
1. Always Set Timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, name, args...)
2. Capture Output
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
3. Handle Signals
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
4. Check Exit Code
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode := exitErr.ExitCode()
}
Common Pitfalls
1. No Timeout
Always set timeouts to prevent hanging.
2. Ignoring Errors
Always check and handle errors.
3. No Output Capture
Capture output for debugging.
4. No Signal Handling
Handle signals for graceful shutdown.
Resources
Summary
Proper process management is essential for CLI tools. Key takeaways:
- Always set timeouts for subprocess execution
- Capture stdout and stderr
- Handle errors properly
- Implement signal handling
- Use context for cancellation
- Implement retry logic when appropriate
- Monitor process execution
By mastering process management, you can build reliable CLI tools.
Comments