Disabling the Cache
By default,go-homedir caches the home directory after the first lookup for performance. You can disable this:
package main
import (
"fmt"
"log"
"os"
"github.com/mitchellh/go-homedir"
)
func main() {
// Disable caching globally
homedir.DisableCache = true
// Now each call to Dir() will re-detect the home directory
dir1, err := homedir.Dir()
if err != nil {
log.Fatal(err)
}
fmt.Println("First call:", dir1)
// Simulate environment change
os.Setenv("HOME", "/tmp/newhome")
// This will detect the new home directory
dir2, err := homedir.Dir()
if err != nil {
log.Fatal(err)
}
fmt.Println("Second call:", dir2)
// Output:
// First call: /home/username
// Second call: /tmp/newhome
}
Disabling the cache means every call to
Dir() or Expand() will re-detect the home directory, which may involve executing shell commands. Only disable caching if you need to detect changes to the home directory during runtime.Using Reset() in Tests
TheReset() function is particularly useful in testing scenarios:
package main
import (
"os"
"testing"
"github.com/mitchellh/go-homedir"
)
// patchEnv temporarily sets an environment variable and returns
// a function to restore the original value
func patchEnv(key, value string) func() {
bck := os.Getenv(key)
deferFunc := func() {
os.Setenv(key, bck)
}
if value != "" {
os.Setenv(key, value)
} else {
os.Unsetenv(key)
}
return deferFunc
}
func TestWithCustomHome(t *testing.T) {
// Disable cache for testing
homedir.DisableCache = true
defer func() { homedir.DisableCache = false }()
// Set custom HOME and restore it after test
defer patchEnv("HOME", "/custom/home")()
// Clear any cached values
homedir.Reset()
// Now Dir() will use /custom/home
dir, err := homedir.Dir()
if err != nil {
t.Fatal(err)
}
if dir != "/custom/home" {
t.Errorf("Expected /custom/home, got %s", dir)
}
}
func TestExpandWithCustomHome(t *testing.T) {
// Disable cache to ensure fresh lookups
homedir.DisableCache = true
defer func() { homedir.DisableCache = false }()
// Set custom HOME environment
defer patchEnv("HOME", "/custom/path")()
// Reset cache to pick up new environment
homedir.Reset()
// Test expansion
actual, err := homedir.Expand("~/foo/bar")
if err != nil {
t.Fatal(err)
}
expected := "/custom/path/foo/bar"
if actual != expected {
t.Errorf("Expected %s, got %s", expected, actual)
}
}
Platform-Specific Considerations
Handle platform differences gracefully:package main
import (
"fmt"
"log"
"runtime"
"github.com/mitchellh/go-homedir"
)
func main() {
dir, err := homedir.Dir()
if err != nil {
log.Fatal(err)
}
// On Unix-like systems, check for common directories
if runtime.GOOS != "windows" {
fmt.Println("Unix-like system detected")
fmt.Println("Home:", dir)
// Typical output: /home/username or /Users/username
// Common Unix paths
configPath, _ := homedir.Expand("~/.config")
fmt.Println("Config:", configPath)
}
}
Combining with filepath Operations
Combinego-homedir with Go’s filepath package for robust path handling:
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/mitchellh/go-homedir"
)
func main() {
// Expand base path
basePath, err := homedir.Expand("~/.myapp")
if err != nil {
log.Fatal(err)
}
// Use filepath.Join for cross-platform path construction
configFile := filepath.Join(basePath, "config", "settings.yaml")
dataDir := filepath.Join(basePath, "data")
logFile := filepath.Join(basePath, "logs", "app.log")
// Create all necessary directories
dirs := []string{
filepath.Dir(configFile),
dataDir,
filepath.Dir(logFile),
}
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0755); err != nil {
log.Fatal(err)
}
}
fmt.Println("Application structure created:")
fmt.Println(" Config:", configFile)
fmt.Println(" Data:", dataDir)
fmt.Println(" Logs:", logFile)
// Walk the directory tree
filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, _ := filepath.Rel(basePath, path)
if info.IsDir() {
fmt.Printf(" [DIR] %s\n", rel)
}
return nil
})
}
Advanced Error Handling Patterns
Implement robust error handling for production code:package main
import (
"errors"
"fmt"
"log"
"os"
"github.com/mitchellh/go-homedir"
)
// GetConfigPath returns the path to a config file with proper error handling
func GetConfigPath(filename string) (string, error) {
if filename == "" {
return "", errors.New("filename cannot be empty")
}
// Try to expand home directory
path := fmt.Sprintf("~/.config/myapp/%s", filename)
expanded, err := homedir.Expand(path)
if err != nil {
return "", fmt.Errorf("failed to expand home directory: %w", err)
}
// Verify parent directory exists or create it
dir := filepath.Dir(expanded)
if err := os.MkdirAll(dir, 0755); err != nil {
return "", fmt.Errorf("failed to create config directory: %w", err)
}
return expanded, nil
}
// LoadConfig loads a config file with fallback to system location
func LoadConfig(filename string) ([]byte, error) {
// Try user home directory first
userPath, err := GetConfigPath(filename)
if err == nil {
if data, err := os.ReadFile(userPath); err == nil {
return data, nil
}
}
// Fallback to system-wide config
systemPath := filepath.Join("/etc/myapp", filename)
data, err := os.ReadFile(systemPath)
if err != nil {
return nil, fmt.Errorf("config not found in user or system location: %w", err)
}
return data, nil
}
func main() {
config, err := LoadConfig("settings.yaml")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Loaded %d bytes of configuration\n", len(config))
}
Concurrent Usage
The library is safe for concurrent use across goroutines:package main
import (
"fmt"
"log"
"sync"
"github.com/mitchellh/go-homedir"
)
func main() {
var wg sync.WaitGroup
results := make(chan string, 10)
// Launch multiple goroutines
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// Safe to call concurrently
dir, err := homedir.Dir()
if err != nil {
log.Printf("Goroutine %d error: %v", id, err)
return
}
results <- fmt.Sprintf("Goroutine %d: %s", id, dir)
}(i)
}
// Close results channel when all goroutines finish
go func() {
wg.Wait()
close(results)
}()
// Collect results
for result := range results {
fmt.Println(result)
}
}
The library uses
sync.RWMutex internally to ensure thread-safe cache access. The first call may be slightly slower as it detects the home directory, but subsequent calls are fast due to caching.Performance Considerations
Understand the performance characteristics:package main
import (
"fmt"
"testing"
"time"
"github.com/mitchellh/go-homedir"
)
func BenchmarkWithCache(b *testing.B) {
// Warmup the cache
for i := 0; i < 10; i++ {
homedir.Dir()
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
homedir.Dir()
}
}
func BenchmarkWithoutCache(b *testing.B) {
homedir.DisableCache = true
defer func() { homedir.DisableCache = false }()
b.ResetTimer()
for i := 0; i < b.N; i++ {
homedir.Dir()
}
}
func main() {
// Demonstrate cache performance
// First call (uncached)
start := time.Now()
homedir.Dir()
firstCall := time.Since(start)
// Second call (cached)
start = time.Now()
homedir.Dir()
secondCall := time.Since(start)
fmt.Printf("First call (uncached): %v\n", firstCall)
fmt.Printf("Second call (cached): %v\n", secondCall)
fmt.Printf("Speedup: %.2fx\n", float64(firstCall)/float64(secondCall))
// Typical output:
// First call (uncached): 245.7µs
// Second call (cached): 1.2µs
// Speedup: 204.75x
}
Test Helper for Environment Patching
Reuse thepatchEnv pattern from the library’s tests:
package myapp
import (
"os"
"testing"
"github.com/mitchellh/go-homedir"
)
// patchEnv temporarily changes an environment variable for testing.
// Returns a cleanup function that restores the original value.
func patchEnv(key, value string) func() {
bck := os.Getenv(key)
deferFunc := func() {
os.Setenv(key, bck)
}
if value != "" {
os.Setenv(key, value)
} else {
os.Unsetenv(key)
}
return deferFunc
}
func TestMultipleEnvironments(t *testing.T) {
tests := []struct {
name string
homeDir string
input string
expected string
}{
{
name: "standard home",
homeDir: "/home/user",
input: "~/config",
expected: "/home/user/config",
},
{
name: "custom home",
homeDir: "/custom/path",
input: "~/.app",
expected: "/custom/path/.app",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup
homedir.DisableCache = true
defer func() { homedir.DisableCache = false }()
defer patchEnv("HOME", tt.homeDir)()
homedir.Reset()
// Test
actual, err := homedir.Expand(tt.input)
if err != nil {
t.Fatal(err)
}
if actual != tt.expected {
t.Errorf("Expected %s, got %s", tt.expected, actual)
}
})
}
}
Always call
homedir.Reset() after modifying environment variables in tests when DisableCache is false. This ensures the cache is cleared and the new environment is detected.