|
| 1 | +package errors |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "fmt" |
| 6 | + "regexp" |
| 7 | + "sync" |
| 8 | +) |
| 9 | + |
| 10 | +// Constants defining default configuration and context keys. |
| 11 | +const ( |
| 12 | + ctxTimeout = "[error] timeout" // Context key marking timeout errors. |
| 13 | + ctxRetry = "[error] retry" // Context key marking retryable errors. |
| 14 | + |
| 15 | + contextSize = 4 // Initial size of fixed-size context array for small contexts. |
| 16 | + bufferSize = 256 // Initial buffer size for JSON marshaling. |
| 17 | + warmUpSize = 100 // Number of errors to pre-warm the pool for efficiency. |
| 18 | + stackDepth = 32 // Maximum stack trace depth to prevent excessive memory use. |
| 19 | + |
| 20 | + DefaultCode = 500 // Default HTTP status code for errors if not specified. |
| 21 | +) |
| 22 | + |
| 23 | +// spaceRe is a precompiled regex for normalizing whitespace in error messages. |
| 24 | +var spaceRe = regexp.MustCompile(`\s+`) |
| 25 | + |
| 26 | +// jsonBufferPool manages reusable buffers for JSON marshaling to reduce allocations. |
| 27 | +var ( |
| 28 | + jsonBufferPool = sync.Pool{ |
| 29 | + New: func() interface{} { |
| 30 | + return bytes.NewBuffer(make([]byte, 0, bufferSize)) |
| 31 | + }, |
| 32 | + } |
| 33 | +) |
| 34 | + |
| 35 | +// ErrorCategory is a string type for categorizing errors (e.g., "network", "validation"). |
| 36 | +type ErrorCategory string |
| 37 | + |
| 38 | +// ErrorOpts provides options for customizing error creation. |
| 39 | +type ErrorOpts struct { |
| 40 | + SkipStack int // Number of stack frames to skip when capturing the stack trace. |
| 41 | +} |
| 42 | + |
| 43 | +// Config defines the global configuration for the errors package, controlling |
| 44 | +// stack depth, context size, pooling, and frame filtering. |
| 45 | +type Config struct { |
| 46 | + StackDepth int // Maximum stack trace depth; 0 uses default (32). |
| 47 | + ContextSize int // Initial context map size; 0 uses default (4). |
| 48 | + DisablePooling bool // If true, disables object pooling for errors. |
| 49 | + FilterInternal bool // If true, filters internal package frames from stack traces. |
| 50 | + AutoFree bool // If true, automatically returns errors to pool when GC collects them. |
| 51 | +} |
| 52 | + |
| 53 | +// cachedConfig holds the current configuration, updated only by Configure(). |
| 54 | +// Protected by configMu for thread-safety. |
| 55 | +type cachedConfig struct { |
| 56 | + stackDepth int |
| 57 | + contextSize int |
| 58 | + disablePooling bool |
| 59 | + filterInternal bool |
| 60 | + autoFree bool |
| 61 | +} |
| 62 | + |
| 63 | +var ( |
| 64 | + // currentConfig stores the active configuration, read frequently and updated rarely. |
| 65 | + currentConfig cachedConfig |
| 66 | + // configMu protects updates to currentConfig for thread-safety. |
| 67 | + configMu sync.RWMutex |
| 68 | + // errorPool manages reusable Error instances to reduce allocations. |
| 69 | + errorPool = NewErrorPool() |
| 70 | + // stackPool manages reusable stack trace slices for efficiency. |
| 71 | + stackPool = sync.Pool{ |
| 72 | + New: func() interface{} { |
| 73 | + return make([]uintptr, currentConfig.stackDepth) |
| 74 | + }, |
| 75 | + } |
| 76 | + // emptyError is a pre-allocated empty error for lightweight reuse. |
| 77 | + emptyError = &Error{ |
| 78 | + smallContext: [contextSize]contextItem{}, |
| 79 | + msg: "", |
| 80 | + name: "", |
| 81 | + template: "", |
| 82 | + cause: nil, |
| 83 | + } |
| 84 | +) |
| 85 | + |
| 86 | +// contextItem holds a single key-value pair in the smallContext array. |
| 87 | +type contextItem struct { |
| 88 | + key string |
| 89 | + value interface{} |
| 90 | +} |
| 91 | + |
| 92 | +// init sets up the package with default configuration and pre-warms the error pool. |
| 93 | +func init() { |
| 94 | + currentConfig = cachedConfig{ |
| 95 | + stackDepth: stackDepth, |
| 96 | + contextSize: contextSize, |
| 97 | + disablePooling: false, |
| 98 | + filterInternal: true, |
| 99 | + autoFree: false, // opt-in; explicit Free() is the safe default |
| 100 | + } |
| 101 | + WarmPool(warmUpSize) // Pre-allocate errors for performance. |
| 102 | +} |
| 103 | + |
| 104 | +// Configure updates the global configuration for the errors package. |
| 105 | +// It is thread-safe and should be called early to avoid race conditions. |
| 106 | +// Changes apply to all subsequent error operations. |
| 107 | +// Example: |
| 108 | +// |
| 109 | +// errors.Configure(errors.Config{StackDepth: 16, DisablePooling: true}) |
| 110 | +func Configure(cfg Config) { |
| 111 | + configMu.Lock() |
| 112 | + defer configMu.Unlock() |
| 113 | + |
| 114 | + if cfg.StackDepth != 0 { |
| 115 | + currentConfig.stackDepth = cfg.StackDepth |
| 116 | + } |
| 117 | + if cfg.ContextSize != 0 { |
| 118 | + currentConfig.contextSize = cfg.ContextSize |
| 119 | + } |
| 120 | + currentConfig.disablePooling = cfg.DisablePooling |
| 121 | + currentConfig.filterInternal = cfg.FilterInternal |
| 122 | + currentConfig.autoFree = cfg.AutoFree |
| 123 | +} |
| 124 | + |
| 125 | +// WarmPool pre-populates the error pool with count instances. |
| 126 | +// Improves performance by reducing initial allocations. |
| 127 | +// No-op if pooling is disabled. |
| 128 | +// Example: |
| 129 | +// |
| 130 | +// errors.WarmPool(1000) |
| 131 | +func WarmPool(count int) { |
| 132 | + if currentConfig.disablePooling { |
| 133 | + return |
| 134 | + } |
| 135 | + for i := 0; i < count; i++ { |
| 136 | + e := &Error{ |
| 137 | + smallContext: [contextSize]contextItem{}, |
| 138 | + stack: nil, |
| 139 | + } |
| 140 | + errorPool.Put(e) |
| 141 | + stackPool.Put(make([]uintptr, 0, currentConfig.stackDepth)) |
| 142 | + } |
| 143 | +} |
| 144 | + |
| 145 | +// WarmStackPool pre-populates the stack pool with count slices. |
| 146 | +// Improves performance for stack-intensive operations. |
| 147 | +// No-op if pooling is disabled. |
| 148 | +// Example: |
| 149 | +// |
| 150 | +// errors.WarmStackPool(500) |
| 151 | +func WarmStackPool(count int) { |
| 152 | + if currentConfig.disablePooling { |
| 153 | + return |
| 154 | + } |
| 155 | + for i := 0; i < count; i++ { |
| 156 | + stackPool.Put(make([]uintptr, 0, currentConfig.stackDepth)) |
| 157 | + } |
| 158 | +} |
| 159 | + |
| 160 | +// FmtErrorCheck safely formats a string using fmt.Sprintf, catching panics. |
| 161 | +// Returns the formatted string and any error encountered. |
| 162 | +// Internal use by Newf to validate format strings. |
| 163 | +// Example: |
| 164 | +// |
| 165 | +// result, err := FmtErrorCheck("value: %s", "test") |
| 166 | +func FmtErrorCheck(format string, args ...interface{}) (result string, err error) { |
| 167 | + defer func() { |
| 168 | + if r := recover(); r != nil { |
| 169 | + if e, ok := r.(error); ok { |
| 170 | + err = e |
| 171 | + } else { |
| 172 | + err = fmt.Errorf("panic during formatting: %v", r) |
| 173 | + } |
| 174 | + } |
| 175 | + }() |
| 176 | + result = fmt.Sprintf(format, args...) |
| 177 | + return result, nil |
| 178 | +} |
0 commit comments