Question
I maintain a list of process IDs (PIDs) for processes currently running on a Linux system. How can I retrieve details about a process from its PID in Go?
I found syscall.Getrusage(), but it does not return the information I need for an arbitrary process. What should I use instead?
Short Answer
By the end of this page, you will understand why syscall.Getrusage is not a general process-inspection API and how to read process information from Linux's /proc filesystem in Go. You will also learn how to handle missing processes, permissions, command-line parsing, and process-lifetime races.
Concept
On Linux, the usual way to inspect a running process is through procfs, a virtual filesystem mounted at /proc.
Each running process has a directory named after its PID:
/proc/<pid>/
For example, a process with PID 1234 may expose files such as:
/proc/1234/status— human-readable status data, including name, state, UID, and memory fields./proc/1234/cmdline— command-line arguments, separated by NUL (\x00) bytes./proc/1234/exe— a symbolic link to the executable being run./proc/1234/stat— compact, machine-readable process statistics./proc/1234/fd— file descriptors currently opened by the process, subject to permissions.
syscall.Getrusage has a different purpose. It reports resource usage for the calling process (RUSAGE_SELF) or its waited-for child processes (RUSAGE_CHILDREN). It does not accept a PID and cannot inspect an arbitrary process.
This distinction matters because process monitoring, diagnostics tools, service managers, and observability agents need information about other processes. On Linux, procfs is the operating system interface designed for that task.
Mental Model
Think of /proc as a live, read-only directory of process report cards.
- A PID is the process's student number.
/proc/<pid>is that student's folder.- Files such as
statusandcmdlineare individual report pages. - When the process exits, its folder disappears.
Because the process can exit at any moment, you must treat a missing /proc/<pid> directory as a normal possibility rather than an exceptional programming failure.
Syntax and Examples
Use os.ReadFile to read procfs files and os.Readlink to resolve symbolic links such as /proc/<pid>/exe.
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
)
func main() {
pid := 1234
procDir := filepath.Join("/proc", fmt.Sprint(pid))
status, err := os.ReadFile(filepath.Join(procDir, "status"))
if err != nil {
fmt.Println("could not read process status:", err)
return
}
for _, line := range strings.Split(string(status), "\n") {
if strings.HasPrefix(line, "Name:") || strings.HasPrefix(line, "State:") {
fmt.Println(line)
}
}
}
The status file contains tab-separated lines similar to:
Name: python3
State: S (sleeping)
Pid: 1234
PPid: 1
Uid: 1000 1000 1000 1000
To read the command line, remember that /proc/<pid>/cmdline uses NUL bytes rather than spaces to separate arguments:
raw, err := os.ReadFile("/proc/1234/cmdline")
if err != nil {
// Handle the error.
}
args := strings.FieldsFunc(string(raw), func(r rune) bool {
return r == '\x00'
})
fmt.Println(strings.Join(args, " "))
Do not simply print string(raw) if you want a readable command line: the embedded NUL separators will not display as normal spaces.
Step by Step Execution
Consider this small function:
func readCommandLine(pid int) (string, error) {
path := fmt.Sprintf("/proc/%d/cmdline", pid)
raw, err := os.ReadFile(path)
if err != nil {
return "", err
}
args := strings.FieldsFunc(string(raw), func(r rune) bool {
return r == '\x00'
})
return strings.Join(args, " "), nil
}
Step by step:
fmt.Sprintfbuilds a procfs path such as/proc/1234/cmdline.os.ReadFileasks the kernel for the process's command-line data.- If the PID no longer exists, or access is denied,
os.ReadFilereturns an error. - The raw file may look like
python3\x00server.py\x00--port\x008080\x00. strings.FieldsFuncsplits that data wherever it finds\x00.strings.Joinproduces a display-friendly command line:python3 server.py --port 8080.
The data is a snapshot. The process may change state or exit immediately after it is read.
Real World Use Cases
Reading process details by PID is useful in many Linux programs:
- Monitoring agents collect process names, states, memory information, and command lines.
- Service supervisors verify that a saved PID still refers to the expected executable.
- Diagnostic CLIs show why a worker is consuming resources or whether it is sleeping, running, or stopped.
- Deployment scripts check whether a server process started successfully.
- Security and audit tools inspect executable paths and process owners, subject to system permissions.
- Job runners record the PID of a launched task and later inspect whether it is still running.
For system-wide tools, be careful with PID reuse: after a process exits, Linux may eventually assign the same number to a different process. A PID alone is not a permanent process identity.
Real Codebase Usage
In production Go code, process inspection is usually wrapped in a small package instead of scattering /proc paths throughout the application.
Common patterns include:
- Validate input first. Reject
pid <= 0; PID0and negative values have special meanings in some system APIs. - Use guard clauses. Return immediately when a procfs file cannot be read.
- Treat disappearance as expected. A process may exit between listing PIDs and reading its files.
- Read only required fields. Reading
statusandcmdlineis often enough; avoid collecting every procfs file without a need. - Handle partial access. You may read
statusbut be unable to inspectfdor another sensitive path for a process owned by a different user. - Keep Linux-specific code isolated.
/proclayout is Linux-specific. Put it in a file such asprocess_linux.go, potentially with a//go:build linuxconstraint when your project supports multiple operating systems. - Use a library when breadth matters. A library such as
github.com/shirou/gopsutil/v3/processcan provide a higher-level API, but direct procfs access is lightweight when you need only a few Linux fields.
A useful verification pattern is to store both the PID and a process start-time value from /proc/<pid>/stat. Later, compare both values to reduce the risk of confusing a reused PID with the original process.
Common Mistakes
Expecting Getrusage to accept a PID
This is not how Getrusage works:
// There is no arbitrary PID argument here.
err := syscall.Getrusage(1234, &usage)
The first argument is a selector such as syscall.RUSAGE_SELF, not a process ID. Use /proc/<pid> for another process's details.
Assuming a PID still exists
This can fail even if you just saw the PID in a previous list:
raw, err := os.ReadFile("/proc/1234/status")
The process could have exited in the meantime. Check err and handle os.IsNotExist(err) as a normal outcome.
Parsing cmdline with spaces
This is incorrect:
args := strings.Split(string(raw), " ")
Arguments are NUL-separated, and an individual argument can itself contain spaces. Split on \x00 instead.
Building paths from untrusted strings
Avoid concatenating arbitrary input directly into a path:
path := "/proc/" + userInput + "/status"
Parse the input as an integer, require a positive value, then construct the path from that integer.
Treating an empty command line as an error
Kernel threads may have an empty cmdline file. An empty command line can be valid; use the process name from status as a fallback display name.
Assuming every procfs field is portable
/proc and the exact contents of its files are Linux interfaces. Code that reads them will not work unchanged on Windows or most non-Linux systems.
Comparisons
| Need | Best option | Why |
|---|---|---|
| Resource usage of the current Go program | syscall.Getrusage(syscall.RUSAGE_SELF, ...) | Reports usage for the calling process. |
| Resource usage of waited-for child processes | syscall.Getrusage(syscall.RUSAGE_CHILDREN, ...) | Aggregates completed child-process usage. |
| Details for an arbitrary running Linux PID | /proc/<pid>/... | Exposes kernel-maintained information for that process. |
| Start, signal, or wait for a process created by your Go program | os/exec and os.Process | Go manages the process you launched. |
| Cross-platform process inspection | A library such as gopsutil | Provides a common API, with platform-specific implementation underneath. |
os.Process is not a full inspection API. It is mainly useful for interacting with a process, such as sending it a signal. On Unix, constructing an os.Process from a PID does not prove that the PID is still the same process you intended to inspect.
Cheat Sheet
// Read a text-like procfs file.
raw, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid))
// Read a process command line (NUL-separated arguments).
raw, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
args := strings.FieldsFunc(string(raw), func(r rune) bool {
return r == '\x00'
})
command := strings.Join(args, " ")
// Find the executable path.
exe, err := os.Readlink(fmt.Sprintf("/proc/%d/exe", pid))
Key rules:
- Require
pid > 0before building a procfs path. - Use
/proc/<pid>/statusfor fields such asName,State,PPid, andUid. - Split
cmdlineon NUL (\x00), not on spaces. - Check every I/O error: processes can exit while being inspected.
- Expect permission errors for processes owned by other users or protected by system security settings.
- An empty
cmdlinemay be valid, particularly for kernel threads. - A PID can eventually be reused; do not rely on a PID as a permanent identity.
Getrusageis for the current process or child-process accounting, not arbitrary PID lookup.
FAQ
Can Go get details for any Linux PID?
Go can read many details through /proc/<pid>, but permissions and Linux security settings can restrict what a user may inspect for another user's process.
Why does /proc/<pid> sometimes disappear?
The process exited. Procfs entries exist only while their associated process exists.
What does an empty /proc/<pid>/cmdline mean?
It can mean the process has no user-space command line, which is common for kernel threads. It does not always mean reading failed.
Is /proc/<pid>/status stable to parse?
It is intended for human-readable process status and is commonly parsed line by line. Parse field names rather than relying on a fixed line number.
Can I use syscall.Getrusage for another process's PID?
No. Its selector refers to the current process or child-process usage categories, not an arbitrary PID.
How do I find a process's executable path?
Use os.Readlink on /proc/<pid>/exe. This can fail if the process exits or access is restricted.
Is procfs available on macOS or Windows?
No. The /proc/<pid> layout discussed here is Linux-specific. Use platform-specific APIs or a cross-platform library for other operating systems.
Mini Project
Description
Build a Linux command-line tool that accepts a PID and prints a small process report. It demonstrates practical procfs reading, command-line parsing, basic status-field parsing, and reliable error handling when a process has already exited.
Goal
Create a Go program that prints a process's PID, name, state, parent PID, user ID, executable path, and command line.
Requirements
Accept exactly one PID argument from the command line.|Reject invalid or non-positive PID values.|Read process fields from /proc/<pid>/status.|Read and format /proc/<pid>/cmdline correctly using NUL separators.|Attempt to display the executable path from /proc/<pid>/exe.|Print a clear error if the process cannot be inspected.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.