Performance
The env library is optimized for high-performance scenarios. This document covers concurrency safety, object pooling, memory management, and other performance-related features.
Concurrency Safety
Thread Safety Guarantees
All methods on Loader are thread-safe:
loader, _ := env.New(env.DefaultConfig())
defer loader.Close()
var wg sync.WaitGroup
// Concurrent reads
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
loader.GetString("KEY")
}()
}
// Concurrent writes
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
loader.Set(fmt.Sprintf("KEY_%d", n), "value")
}(i)
}
wg.Wait()Package-Level Function Thread Safety
Package-level functions use a global loader and are also thread-safe:
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
env.GetString("KEY", "default")
}()
}
wg.Wait()Internal Implementation
The library uses sharded storage to reduce lock contention:
┌─────────────────────────────────────────┐
│ Loader (8 shards) │
├─────────────────────────────────────────┤
│ ┌─────────┐ ┌─────────┐ ┌────────┐ │
│ │ Shard 0 │ │ Shard 1 │... │ Shard 7│ │
│ │ Lock │ │ Lock │ │ Lock │ │
│ │ Data │ │ Data │ │ Data │ │
│ └─────────┘ └─────────┘ └────────┘ │
└─────────────────────────────────────────┘- Keys are assigned to different shards based on hash value
- Each shard has its own lock
- Reduces lock contention and improves concurrency performance
Object Pool
Why Use an Object Pool
Frequent object creation and destruction increases GC pressure:
Without object pool:
Create → Use → GC Collect → Create → Use → GC Collect ...
With object pool:
Create → Use → Return to Pool → Get → Use → Return to Pool ...SecureValue Pool
SecureValue objects use pooled management:
// Get a SecureValue (may be reused from pool)
secret := env.GetSecure("API_KEY")
// Use it (Reveal returns plaintext, String/Masked return mask)
value := secret.Reveal()
// Release back to pool
secret.Close() // or secret.Release()Using the Object Pool Correctly
Release promptly:
func processData() {
secret := env.GetSecure("SECRET")
defer secret.Close() // Ensure release
// Use secret...
}Do not hold references:
// Wrong: holding a reference to a released object
var globalSecret *env.SecureValue
func init() {
globalSecret = env.GetSecure("KEY")
globalSecret.Close() // After release, the object may be reused
}
func later() {
// Dangerous: globalSecret may already be in use by other code
globalSecret.String()
}
// Correct: acquire each time you need it
func getSecret() string {
secret := env.GetSecure("KEY")
defer secret.Close()
return secret.Reveal()
}Check closed state:
secret := env.GetSecure("KEY")
// Check before use
if secret.IsClosed() {
// Object is closed, cannot be used
}
// Close after use
secret.Close()
// Check after closing
if secret.IsClosed() {
// Already closed
}Memory Safety
Memory Locking
Enable memory locking to prevent sensitive data from being swapped to disk:
// Check platform support
if env.IsMemoryLockSupported() {
env.SetMemoryLockEnabled(true)
}Platform support:
| Platform | Supported |
|---|---|
| Linux | Yes |
| macOS | Yes |
| Windows | Yes |
| FreeBSD | Yes |
| wasm | No |
See Also
See SecureValue API - Memory Lock Configuration for complete configuration details.
Strict Mode
In strict mode, memory locking failure returns an error:
env.SetMemoryLockStrict(true)
secret, err := env.NewSecureValueStrict("sensitive_data")
if err != nil {
// Memory locking failed
}Secure Zeroing
SecureValue automatically zeros memory when closed:
secret := env.GetSecure("PASSWORD")
// Internal storage: ['p', 'a', 's', 's', ...]
secret.Close()
// Internal storage: [0, 0, 0, 0, ...]Manually zero a byte slice:
sensitiveBytes := []byte("secret")
env.ClearBytes(sensitiveBytes)
// sensitiveBytes is now all zerosPerformance Patterns
Read-Only After Initialization
The most efficient pattern: load configuration at startup, read-only at runtime:
var config *Config
func init() {
env.Load(".env")
config = &Config{}
env.ParseInto(config)
}
// Safe to read from any goroutine
func getValue() string {
return config.Key
}Dynamic Configuration Refresh
Pattern for dynamic configuration updates:
type ConfigManager struct {
loader *env.Loader
mu sync.RWMutex
}
func (m *ConfigManager) Refresh() error {
m.mu.Lock()
defer m.mu.Unlock()
return m.loader.LoadFiles(".env")
}
func (m *ConfigManager) Get(key string) string {
m.mu.RLock()
defer m.mu.RUnlock()
return m.loader.GetString(key)
}Reduce Lock Hold Time
// Not recommended: performing expensive operations inside the lock
func (l *Loader) ProcessValue(key string) {
value := l.GetString(key)
// Expensive operation...
processValue(value)
}
// Recommended: quick read, process outside the lock
func ProcessValue(key string) {
value := loader.GetString(key) // Quick acquisition
go processValue(value) // Async processing
}Batch Operations
// Get all needed values at once
func LoadAllConfig(loader *env.Loader) *Config {
return &Config{
Host: loader.GetString("HOST"),
Port: loader.GetInt("PORT"),
Debug: loader.GetBool("DEBUG"),
Timeout: loader.GetDuration("TIMEOUT"),
}
}Avoid Frequent Calls
// Not recommended: reading on every request
func Handler(w http.ResponseWriter, r *http.Request) {
apiKey := env.GetString("API_KEY") // Locks on every request
// ...
}
// Recommended: cache at startup
var apiKey string
func init() {
env.Load(".env")
apiKey = env.GetString("API_KEY")
}
func Handler(w http.ResponseWriter, r *http.Request) {
// Use the cached value directly
// ...
}Performance Impact
Object Pool Benefits
| Operation | Without Pool | With Pool |
|---|---|---|
| Allocations | N | ~constant |
| GC Pressure | High | Low |
| Latency | Unstable | Stable |
Memory Locking Overhead
Memory locking (mlock on Linux / VirtualLock on Windows) incurs a one-time extra syscall overhead only when creating a SecureValue; read operations (Reveal / String / Masked) are unaffected. Keep SecureValue instances small and short-lived — release them back to the pool immediately via Close() / Release() to avoid holding large locked memory regions for long periods.
Benchmarks
Read Performance
func BenchmarkConcurrentRead(b *testing.B) {
loader, _ := env.New(env.DefaultConfig())
loader.Set("KEY", "value")
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
loader.GetString("KEY")
}
})
}Write Performance
func BenchmarkConcurrentWrite(b *testing.B) {
loader, _ := env.New(env.DefaultConfig())
var i int64
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
n := atomic.AddInt64(&i, 1)
loader.Set(fmt.Sprintf("KEY_%d", n), "value")
}
})
}Mixed Read/Write
func BenchmarkMixedReadWrite(b *testing.B) {
loader, _ := env.New(env.DefaultConfig())
loader.Set("KEY", "value")
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
if i%10 == 0 {
loader.Set("KEY", "new_value")
} else {
loader.GetString("KEY")
}
i++
}
})
}Caveats
Avoid Blocking Inside Locks
// Dangerous: may cause deadlock
func (l *Loader) BadMethod() {
// Calling potentially blocking operations inside the lock
l.Set("KEY", computeValue()) // computeValue may be slow
}
// Safe: compute first, then set
func GoodMethod() {
value := computeValue() // Compute outside lock
loader.Set("KEY", value) // Quick set
}Concurrent Access After Close
loader, _ := env.New(cfg)
// Start goroutine
go func() {
time.Sleep(1 * time.Second)
loader.GetString("KEY") // Returns empty string (GetString does not return an error)
}()
loader.Close() // Main goroutine closesGlobal Loader Reset
// Not concurrency-safe: do not call at runtime
env.ResetDefaultLoader()
// Safe: call only during tests or startup
func init() {
env.ResetDefaultLoader()
env.Load(".env")
}Related Documentation
- SecureValue API - Secure value handling and memory locking
- Loader API - Loader methods
- Testing Scenarios - Benchmark examples