Creating Processes in Go

Before diving into how to create new processes, it is essential to clarify the core distinction between "processes" and "threads" within an operating system. The Go runtime environment is built upon the OS's user-space multi-threading mechanism. This means a Goroutine runs by default on a single OS thread, while multiple Goroutines share the memory space, stack, and global variable resources of the same process.

However, when applications need to access resources from other processes—for example, connecting to a remote server via net.Dial or invoking external commands—it is necessary to create new operating system-level processes to isolate the execution environment. Go provides native system call interfaces to achieve this goal, making it both efficient and safe to write concurrent network services. Understanding the mapping relationship between "user-space threads" and "OS threads/processes" is a crucial step in mastering Go's underlying mechanisms.
The os/exec package in the Go standard library is an essential tool for building, launching, and managing external processes. It offers rich functionality, including executing commands, redirecting input/output streams, and capturing return status codes. For beginners, the most basic yet commonly used operation involves constructing a command object using exec.Command.

package main

import (
    "fmt"
    "os/exec"
)

func main() {
    // Create an instance of a command to execute "ls -l"
    cmd := exec.Command("ls", "-l")
    
    fmt.Println("Preparing to launch child process...")
    output, err := cmd.Output() // Wait for the command to complete and get standard output
    
    if err != nil {
        fmt.Printf("An error occurred: %v\n", err)
        return
    }
    
    fmt.Println("Execution result:")
    fmt.Print(string(output))
}

In the example above, we first instantiate a Command object. At this point, the child process has not started yet, and memory resources have not been allocated to that command. Only when we invoke methods such as Run(), Start(), or Output() does the Go Runtime actually trigger system calls to create a new OS thread (i.e., the child process).

Asynchronous Execution and Stream Processing Mechanisms

In practical development scenarios, it is often unnecessary to wait for an external command to fully complete before continuing with subsequent logic. In such cases, one can utilize exec.Command in conjunction with the Start() method to achieve non-blocking asynchronous startup. This approach allows the main Goroutine to return immediately after the child process initialization completes, thereby maintaining high concurrency response capabilities.

func runCommandAsync() {
    cmd := exec.Command("sleep", "5") // Simulate a time-consuming command
    
    // Start() returns immediately without waiting for the command execution to finish
    go func() {
        err := cmd.Start()
        if err != nil {
            fmt.Println("Startup failed:", err)
            return
        }
        
        fmt.Println("Child process started, main program continuing...")
        
        // Handle other concurrent tasks here
        time.Sleep(2 * time.Second)
        
        // Wait for the command to end and clean up resources
        cmd.Wait() 
        fmt.Println("Child process finished.")
    }()
    
    // The function returns immediately without blocking on cmd.Start()
}

It is worth noting that after calling Start(), one must explicitly wait for the command to finish using cmd.Wait() and perform resource cleanup. If this step is not executed, the Go Runtime will fail to correctly release the OS thread and system handles allocated to that child process, which may lead to abnormal program termination or file descriptor leaks. Additionally, for large binary files or complex command-line arguments, it is recommended to use absolute paths rather than relative paths to avoid cross-platform compatibility issues.

Input/Output Redirection and Pipe Applications

Beyond simple command execution, os/exec supports flexible input/output control. Developers can achieve unidirectional or bidirectional data stream transmission by setting the Stdin, Stdout, and Stderr fields. This is crucial for building scenarios such as log collectors and data processing pipelines.

func processWithPipe() {
    // Create a child process that writes data to the "cat" command
    cmd := exec.Command("cat") // cat reads from standard input and outputs to standard output
    
    // Redirect the current Goroutine's output to cmd.Stdin
    if err := cmd.Start(); err != nil {
        panic(err)
    }
    
    defer func() {
        stdout, _ := os.Stdout.Dup() // Save original stdout
        
        cmd.Wait()
        
        os.Stdout = stdout           // Restore original output
        fmt.Fprint(os.Stderr, "Child process closed.\n")
    }()
    
    for i := 0; ; i++ {
        time.Sleep(1 * time.Second)
        if _, err := fmt.Fprintf(cmd.Stdin, "%d\n", i); err != nil {
            break
        }
        
        fmt.Printf("Sent data %d, waiting to receive...\n", i)
    }
    
    cmd.Wait() // Ensure child process exits completely before restoring stdout
}

As seen in the code above, we successfully pass our main Goroutine's output stream as input to an external command while continuously generating incrementing numbers in the background. This pattern is ideal for building real-time data monitoring systems or log aggregation tools. Additionally, note using the defer + os.Stdout.Dup() pattern to safely switch and restore standard output streams, avoiding interference with normal program interaction interfaces.

Best Practices and Precautions

When applying process creation techniques in actual production environments, several key aspects must be considered:

  • Error Handling: Always check error information returned by all system calls, especially the return values of critical steps like Start() and Wait().

  • Resource Cleanup: Remember to release related OS threads and file handles via cmd.Wait() after a child process ends to prevent resource leaks that could cause program crashes or performance degradation.

  • Security Considerations: Avoid using shell (/bin/sh) as the method for executing commands, as shell parsers may introduce security risks (such as injection attacks). Prefer directly executing binary files of programs or explicitly specified executable scripts instead.

  • Platform Compatibility: Different operating systems handle command-line arguments, path separators, and special characters differently; code should be written with full consideration for target platform characteristics and appropriate adaptations made accordingly.

By reasonably utilizing the os/exec package and its various methods, developers can efficiently construct multi-process collaborative application structures in Go language, fully leveraging the advantages of concurrent programming while maintaining system stability and maintainability.