Go’s os and runtime packages expose the operating system environment to your program. Environment variables, process information, file permissions, signal handling, and platform-specific paths are all accessible through a consistent API that works across Linux, macOS, and Windows.
The most important topic for production services is signal handling — a server that ignores SIGTERM will be force-killed when the orchestrator (Kubernetes, systemd) shuts it down, dropping in-flight requests. Graceful shutdown via signal handling is not optional for production Go services.
For file system operations see Go file system operations. For subprocess management see Go process management.
Environment Variables
os.Getenv returns the value of an environment variable, or an empty string if it’s not set. Use os.LookupEnv when you need to distinguish “not set” from “set to empty string”:
// Getenv: returns "" for both unset and empty
host := os.Getenv("APP_HOST")
if host == "" {
host = "localhost" // ambiguous — is it unset or explicitly empty?
}
// LookupEnv: distinguishes unset from empty
host, exists := os.LookupEnv("APP_HOST")
if !exists {
host = "localhost" // definitely not set
}
A helper that combines LookupEnv with a default is useful throughout a codebase:
func envOr(key, fallback string) string {
if v, ok := os.LookupEnv(key); ok {
return v
}
return fallback
}
func envOrInt(key string, fallback int) int {
v, ok := os.LookupEnv(key)
if !ok {
return fallback
}
n, err := strconv.Atoi(v)
if err != nil {
log.Printf("invalid %s=%q, using default %d", key, v, fallback)
return fallback
}
return n
}
// Usage
addr := envOr("APP_HOST", "localhost") + ":" + strconv.Itoa(envOrInt("APP_PORT", 8080))
os.Environ() returns all environment variables as "KEY=VALUE" strings. Use it when you need to pass the current environment to a subprocess, or inspect all variables for debugging.
Process Information
fmt.Println("PID:", os.Getpid()) // current process ID
fmt.Println("PPID:", os.Getppid()) // parent process ID
fmt.Println("Args:", os.Args) // command-line arguments (os.Args[0] = program name)
wd, err := os.Getwd() // current working directory
if err != nil {
log.Fatal(err)
}
hostname, err := os.Hostname()
if err != nil {
log.Fatal(err)
}
u, err := user.Current() // current user (os/user package)
if err != nil {
log.Fatal(err)
}
fmt.Printf("user: %s (%s)\n", u.Username, u.HomeDir)
runtime.GOOS and runtime.GOARCH give you the platform at compile time — useful for platform-specific behavior:
fmt.Printf("running on %s/%s with Go %s\n",
runtime.GOOS, runtime.GOARCH, runtime.Version())
fmt.Printf("CPUs: %d, goroutines: %d\n",
runtime.NumCPU(), runtime.NumGoroutine())
Signal Handling and Graceful Shutdown
When Kubernetes sends SIGTERM or the user presses Ctrl+C (SIGINT), a server should stop accepting new requests, let in-flight requests complete, and then exit cleanly. Without signal handling, the process is force-killed immediately — dropping connections and potentially corrupting state.
The canonical pattern uses signal.NotifyContext (Go 1.16+):
func main() {
// ctx is cancelled on SIGINT or SIGTERM
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop()
srv := &http.Server{
Addr: ":8080",
Handler: buildRouter(),
}
// Start server in background
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
log.Println("server started on :8080")
// Wait for shutdown signal
<-ctx.Done()
stop() // stop receiving signals
log.Println("shutdown signal received")
// Give in-flight requests 30 seconds to complete
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown error: %v", err)
}
log.Println("server stopped")
}
signal.NotifyContext is cleaner than the older signal.Notify + channel pattern. It integrates with the context.Context system that all middleware, handlers, and downstream calls should already be using.
For services that do other cleanup (close database pools, flush buffers, commit offsets), the 30*time.Second shutdown context propagates to all outstanding operations:
// All operations respect the shutdown context
func handleRequest(w http.ResponseWriter, r *http.Request) {
// r.Context() is cancelled on client disconnect
// AND when the server starts shutting down
result, err := db.QueryContext(r.Context(), "SELECT ...")
if errors.Is(err, context.Canceled) {
return // shutdown in progress — no need to write response
}
// ...
}
Cross-Platform Paths
File paths differ between platforms: /home/user/.config on Linux, C:\Users\user\AppData\Roaming on Windows. Use os.UserHomeDir(), os.UserConfigDir(), and os.UserCacheDir() for standard locations:
// Standard user directories — cross-platform
homeDir, err := os.UserHomeDir() // ~/. on Linux/macOS, C:\Users\user on Windows
configDir, _ := os.UserConfigDir() // ~/.config on Linux, AppData/Roaming on Windows
cacheDir, _ := os.UserCacheDir() // ~/.cache on Linux, AppData/Local on Windows
// Build platform-appropriate paths
appConfig := filepath.Join(configDir, "myapp", "config.yaml")
appCache := filepath.Join(cacheDir, "myapp")
// Ensure directories exist
if err := os.MkdirAll(filepath.Dir(appConfig), 0755); err != nil {
log.Fatal(err)
}
filepath.Join handles path separators correctly on all platforms — always use it instead of string concatenation with / or \.
File Permissions
On Unix, file permissions are represented as a bitmask (os.FileMode). Common patterns:
// Create file with owner read/write, group and others read-only
f, err := os.OpenFile("data.txt", os.O_CREATE|os.O_WRONLY, 0644)
// Create directory with owner full access, group and others read+execute
err = os.MkdirAll("data/uploads", 0755)
// Read current permissions
info, err := os.Stat("data.txt")
fmt.Printf("mode: %v\n", info.Mode()) // -rw-r--r--
// Change permissions
err = os.Chmod("script.sh", 0755) // add execute bit
// Check specific permissions
mode := info.Mode()
isReadable := mode&0400 != 0 // owner can read
isWritable := mode&0200 != 0 // owner can write
isExecutable := mode&0100 != 0 // owner can execute
On Windows, os.Chmod only handles the read-only bit — full ACL control requires the golang.org/x/sys/windows package.
Runtime System Information
// Build information — what was this binary compiled with?
info, ok := debug.ReadBuildInfo()
if ok {
fmt.Printf("go version: %s\n", info.GoVersion)
fmt.Printf("main module: %s@%s\n", info.Main.Path, info.Main.Version)
}
// CPU and memory stats
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
fmt.Printf("heap alloc: %.2f MB\n", float64(mem.Alloc)/1e6)
fmt.Printf("goroutines: %d\n", runtime.NumGoroutine())
// Control GOMAXPROCS (usually leave at default = NumCPU)
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0)) // 0 = read current value
runtime.ReadMemStats is useful in health check endpoints or diagnostic handlers. runtime.NumGoroutine() trending upward is a signal of a goroutine leak.
Exit Codes
Exit codes matter for scripts and process orchestration. Use os.Exit sparingly — always at the top level of main, never inside library code:
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
// All application logic here — returns errors instead of calling os.Exit
cfg, err := loadConfig()
if err != nil {
return fmt.Errorf("load config: %w", err)
}
// ...
return nil
}
os.Exit bypasses deferred functions — never call it when there are cleanup defers registered.
Summary
- Use
os.LookupEnvwhen you need to distinguish “not set” from “empty string” —os.Getenvconflates both - Use
signal.NotifyContextfor graceful shutdown — it integrates with the context system and handles SIGINT/SIGTERM cleanly - Give in-flight requests time to complete:
http.Server.Shutdown(ctx)with a 30-second context is the standard pattern - Use
os.UserConfigDir(),os.UserHomeDir(),os.UserCacheDir()for cross-platform standard directories filepath.Joinfor all path construction — never string concatenation with/- Call
os.Exitonly inmain(), and only after all cleanup — it bypasses defer
Resources
- os package documentation
- signal package documentation
- runtime package documentation
- Go Blog: Contexts and HTTP Servers
Comments