Overview
gopsutil returns standard Go errors that provide context about failures. Understanding these errors and how to handle them properly is crucial for building robust applications.Common Error Types
Not Implemented Error
Returned when a feature is not available on the current platform:import (
"errors"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/internal/common"
)
func getCPUInfo(ctx context.Context) error {
info, err := cpu.InfoWithContext(ctx)
if err != nil {
if errors.Is(err, common.ErrNotImplementedError) {
log.Println("CPU info not available on this platform")
return nil // Handle gracefully
}
return fmt.Errorf("failed to get CPU info: %w", err)
}
// Process info...
return nil
}
Context Errors
When using context-aware functions, handle context cancellation and timeouts:import (
"context"
"errors"
"time"
"github.com/shirou/gopsutil/v4/process"
)
func getProcessInfo(pid int32) error {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
p, err := process.NewProcessWithContext(ctx, pid)
if err != nil {
return err
}
name, err := p.NameWithContext(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return fmt.Errorf("timeout getting process name: %w", err)
}
if errors.Is(err, context.Canceled) {
return fmt.Errorf("operation canceled: %w", err)
}
return err
}
fmt.Println("Process name:", name)
return nil
}
Process Errors
Specific errors for process operations:import (
"errors"
"github.com/shirou/gopsutil/v4/process"
)
func checkProcess(pid int32) error {
p, err := process.NewProcess(pid)
if err != nil {
return fmt.Errorf("failed to create process: %w", err)
}
running, err := p.IsRunning()
if err != nil {
if errors.Is(err, process.ErrorProcessNotRunning) {
log.Printf("Process %d is not running", pid)
return nil
}
return err
}
if !running {
return process.ErrorProcessNotRunning
}
return nil
}
Permission Errors
Handle permission-related failures:import (
"errors"
"os"
"github.com/shirou/gopsutil/v4/process"
)
func getProcessDetails(pid int32) error {
p, err := process.NewProcess(pid)
if err != nil {
return err
}
cmdline, err := p.Cmdline()
if err != nil {
// Check for permission errors
if errors.Is(err, os.ErrPermission) ||
errors.Is(err, process.ErrorNotPermitted) {
log.Printf("Permission denied for process %d", pid)
return nil // Handle gracefully
}
return fmt.Errorf("failed to get cmdline: %w", err)
}
fmt.Println("Command:", cmdline)
return nil
}
Error Wrapping
Always wrap errors with context usingfmt.Errorf and %w:
import (
"fmt"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/mem"
)
func getSystemMetrics(ctx context.Context) error {
// Wrap errors with context
cpuPercent, err := cpu.PercentWithContext(ctx, 0, false)
if err != nil {
return fmt.Errorf("failed to get CPU percent: %w", err)
}
vmem, err := mem.VirtualMemoryWithContext(ctx)
if err != nil {
return fmt.Errorf("failed to get virtual memory: %w", err)
}
// Use metrics...
_ = cpuPercent
_ = vmem
return nil
}
// Higher-level function can unwrap and inspect
func collectMetrics() {
if err := getSystemMetrics(context.Background()); err != nil {
// Can use errors.Is() or errors.As() to check wrapped errors
log.Printf("Error collecting metrics: %v", err)
}
}
Handling Multiple Errors
When collecting data from multiple sources, handle partial failures:import (
"sync"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/mem"
"github.com/shirou/gopsutil/v4/disk"
)
type SystemMetrics struct {
CPUPercent []float64
Memory *mem.VirtualMemoryStat
DiskUsage *disk.UsageStat
Errors []error
}
func collectAllMetrics(ctx context.Context) *SystemMetrics {
metrics := &SystemMetrics{
Errors: make([]error, 0),
}
var wg sync.WaitGroup
var mu sync.Mutex
// Collect CPU metrics
wg.Add(1)
go func() {
defer wg.Done()
cpu, err := cpu.PercentWithContext(ctx, 0, false)
mu.Lock()
defer mu.Unlock()
if err != nil {
metrics.Errors = append(metrics.Errors,
fmt.Errorf("cpu: %w", err))
} else {
metrics.CPUPercent = cpu
}
}()
// Collect memory metrics
wg.Add(1)
go func() {
defer wg.Done()
mem, err := mem.VirtualMemoryWithContext(ctx)
mu.Lock()
defer mu.Unlock()
if err != nil {
metrics.Errors = append(metrics.Errors,
fmt.Errorf("memory: %w", err))
} else {
metrics.Memory = mem
}
}()
// Collect disk metrics
wg.Add(1)
go func() {
defer wg.Done()
disk, err := disk.UsageWithContext(ctx, "/")
mu.Lock()
defer mu.Unlock()
if err != nil {
metrics.Errors = append(metrics.Errors,
fmt.Errorf("disk: %w", err))
} else {
metrics.DiskUsage = disk
}
}()
wg.Wait()
return metrics
}
func main() {
metrics := collectAllMetrics(context.Background())
if len(metrics.Errors) > 0 {
log.Println("Encountered errors:")
for _, err := range metrics.Errors {
log.Printf(" - %v", err)
}
}
// Use whatever metrics we successfully collected
if metrics.CPUPercent != nil {
fmt.Printf("CPU: %.2f%%\n", metrics.CPUPercent[0])
}
if metrics.Memory != nil {
fmt.Printf("Memory: %.2f%%\n", metrics.Memory.UsedPercent)
}
}
Retry Logic
Implement retry logic for transient failures:import (
"context"
"time"
"github.com/shirou/gopsutil/v4/cpu"
)
func getCPUPercentWithRetry(ctx context.Context, maxRetries int) ([]float64, error) {
var lastErr error
for i := 0; i < maxRetries; i++ {
percent, err := cpu.PercentWithContext(ctx, 100*time.Millisecond, false)
if err == nil {
return percent, nil
}
lastErr = err
// Don't retry on context errors or not implemented
if errors.Is(err, context.Canceled) ||
errors.Is(err, context.DeadlineExceeded) ||
errors.Is(err, common.ErrNotImplementedError) {
return nil, err
}
// Exponential backoff
if i < maxRetries-1 {
backoff := time.Duration(i+1) * 100 * time.Millisecond
select {
case <-time.After(backoff):
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
return nil, fmt.Errorf("failed after %d retries: %w", maxRetries, lastErr)
}
Logging Best Practices
Provide useful context in error messages:import (
"log/slog"
"github.com/shirou/gopsutil/v4/process"
)
func monitorProcess(pid int32, logger *slog.Logger) error {
p, err := process.NewProcess(pid)
if err != nil {
logger.Error("failed to create process",
"pid", pid,
"error", err)
return err
}
cpuPercent, err := p.CPUPercent()
if err != nil {
logger.Warn("failed to get CPU percent",
"pid", pid,
"error", err)
// Continue with other metrics
} else {
logger.Info("process CPU usage",
"pid", pid,
"cpu_percent", cpuPercent)
}
return nil
}
Custom Error Types
Create domain-specific errors for your application:import (
"errors"
"fmt"
)
var (
ErrSystemOverloaded = errors.New("system resources overloaded")
ErrThresholdExceeded = errors.New("threshold exceeded")
)
type MetricError struct {
Metric string
Value float64
Err error
}
func (e *MetricError) Error() string {
return fmt.Sprintf("%s metric error (value: %.2f): %v",
e.Metric, e.Value, e.Err)
}
func (e *MetricError) Unwrap() error {
return e.Err
}
func checkCPUUsage(ctx context.Context, threshold float64) error {
percent, err := cpu.PercentWithContext(ctx, 0, false)
if err != nil {
return &MetricError{
Metric: "cpu",
Value: 0,
Err: err,
}
}
if len(percent) > 0 && percent[0] > threshold {
return &MetricError{
Metric: "cpu",
Value: percent[0],
Err: ErrThresholdExceeded,
}
}
return nil
}
Error Recovery
Implement graceful degradation when errors occur:type MetricsCollector struct {
lastValidCPU []float64
lastValidMemory *mem.VirtualMemoryStat
logger *slog.Logger
}
func (m *MetricsCollector) GetCPU(ctx context.Context) []float64 {
cpu, err := cpu.PercentWithContext(ctx, 0, false)
if err != nil {
m.logger.Warn("failed to get current CPU, using cached value",
"error", err)
return m.lastValidCPU
}
m.lastValidCPU = cpu
return cpu
}
func (m *MetricsCollector) GetMemory(ctx context.Context) *mem.VirtualMemoryStat {
mem, err := mem.VirtualMemoryWithContext(ctx)
if err != nil {
m.logger.Warn("failed to get current memory, using cached value",
"error", err)
return m.lastValidMemory
}
m.lastValidMemory = mem
return mem
}
Testing Error Handling
func TestErrorHandling(t *testing.T) {
tests := []struct {
name string
ctx context.Context
wantErr error
}{
{
name: "timeout",
ctx: func() context.Context {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
defer cancel()
time.Sleep(10 * time.Millisecond)
return ctx
}(),
wantErr: context.DeadlineExceeded,
},
{
name: "canceled",
ctx: func() context.Context {
ctx, cancel := context.WithCancel(context.Background())
cancel()
return ctx
}(),
wantErr: context.Canceled,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := cpu.PercentWithContext(tt.ctx, 100*time.Millisecond, false)
if !errors.Is(err, tt.wantErr) {
t.Errorf("expected error %v, got %v", tt.wantErr, err)
}
})
}
}
Best Practices Summary
Related Resources
Cross-Platform
Handle platform-specific errors
Performance
Error handling impact on performance