How to Set Environment Variables Programmatically in Go

Set environment variables in Go using os.Setenv or os.Unsetenv to modify runtime configuration.

The process boundary

You are building a CLI tool that needs to toggle a debug mode, or you want to pass a configuration value to a subprocess without cluttering your shell. You reach for os.Setenv. It works, but there is a boundary you cannot cross. Environment variables belong to the process. Changing them updates the state of your running program and any children it spawns. The change never ripples back to the parent shell or other processes.

Think of the environment as a whiteboard inside a room. You can write on it, and anyone who enters the room sees what you wrote. You cannot write on the whiteboard in the hallway outside. Your Go program is the room. The shell that launched it is the hallway.

Environment variables are process-local state. os.Setenv modifies the map the operating system gave your process. The change is immediate for your code and visible to any subprocess you start afterward. It is invisible to the world outside.

Setting and reading variables

The os package provides the standard interface for environment manipulation. os.Setenv updates a key-value pair. os.Getenv retrieves a value. os.Unsetenv removes a key. These functions operate on the global environment map maintained by the runtime.

Here is the basic loop: set a variable, read it back, then clean up.

package main

import (
	"fmt"
	"os"
)

func main() {
	// Setenv updates the process environment map.
	// The key and value must be valid strings.
	// Returns an error if the OS rejects the variable.
	err := os.Setenv("APP_MODE", "debug")
	if err != nil {
		fmt.Println("Failed to set env:", err)
		return
	}

	// Getenv retrieves the value associated with the key.
	// Returns an empty string if the key is missing.
	mode := os.Getenv("APP_MODE")
	fmt.Println("Mode:", mode)

	// Unsetenv removes the key from the environment.
	// Subsequent reads return an empty string.
	os.Unsetenv("APP_MODE")
}

The runtime stores the environment in a slice of strings formatted as KEY=VALUE. When you call Setenv, the runtime searches this slice. If the key exists, it updates the value. If the key is missing, it appends a new entry. On Unix systems, this often translates to a setenv system call. On Windows, it calls SetEnvironmentVariable. The abstraction is consistent across platforms.

Environment variables are process-local. Respect the boundary.

The empty string trap

A common mistake is assuming os.Getenv tells you whether a variable exists. It does not. Getenv returns the value if the key is present. It returns an empty string if the key is missing. It also returns an empty string if the key is present but set to an empty string. You cannot distinguish between "missing" and "empty" using Getenv.

This ambiguity breaks logic that checks if os.Getenv("VAR") == "". You don't know if the user forgot to set the variable or explicitly set it to empty. The fix is os.LookupEnv. It returns the value and a boolean indicating presence.

package main

import (
	"fmt"
	"os"
)

func main() {
	// LookupEnv returns the value and a boolean.
	// The boolean is true if the key exists in the environment.
	// It is false if the key is missing, even if the value is empty.
	val, exists := os.LookupEnv("MISSING_VAR")

	if !exists {
		// Handle the missing case separately.
		// This avoids confusion with an empty string value.
		fmt.Println("Variable is not set")
		return
	}

	// Use the value safely.
	// The value might be an empty string, which is valid.
	fmt.Println("Value:", val)
}

LookupEnv saves you from the empty string trap. Use it when the distinction matters.

Isolating variables for subprocesses

Global changes ripple through your whole program. Modifying the environment with Setenv affects every part of your code and every child process you spawn. Often you only need a variable for a specific subprocess. You want to pass a secret or a config flag to a child without polluting the parent's environment.

The solution is to build a custom environment slice for the child. os.Environ returns the current environment as a slice of strings. You copy this slice, modify the copy, and pass it to exec.Command via the Env field. The child inherits the modified list. The parent remains untouched.

Here is how to isolate a variable for a subprocess.

package main

import (
	"fmt"
	"os"
	"os/exec"
)

func main() {
	// Environ returns the current environment as a slice of strings.
	// Each string has the format "KEY=VALUE".
	currentEnv := os.Environ()

	// Create a copy to avoid mutating the global state.
	// Slices share underlying arrays, so copy the content.
	childEnv := make([]string, len(currentEnv))
	copy(childEnv, currentEnv)

	// Append a custom variable for the child process only.
	// The parent process environment remains untouched.
	childEnv = append(childEnv, "CHILD_ONLY=secret")

	// Configure the command to use the modified environment.
	// The child inherits this exact list of variables.
	cmd := exec.Command("sh", "-c", "echo $CHILD_ONLY")
	cmd.Env = childEnv

	// Run the command and capture output.
	// The child sees CHILD_ONLY. The parent does not.
	output, err := cmd.CombinedOutput()
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Printf("Child output: %s", output)
}

The Exec package does not merge environments automatically. If you set cmd.Env, you must include all variables the child needs. If you omit cmd.Env, the child inherits the parent's environment exactly. Setting cmd.Env replaces the inheritance entirely. Copy the parent's environment first if you want to add variables without losing the rest.

Child processes inherit the environment. Copy and modify to keep the parent clean.

Runtime flags and GODEBUG

Some environment variables control runtime behavior. GODEBUG is the most famous. It toggles garbage collection tracing, scheduler debugging, and other internals. The runtime reads GODEBUG during initialization. It caches the value. If you call os.Setenv("GODEBUG", "gctrace=1") after the program starts, the runtime ignores the change. The flag is already baked in.

This catches many developers. You set the variable in your code, expect the runtime to react, and see nothing. The variable is set in the process map, but the runtime never checks the map again for that flag. You must set GODEBUG in the shell before running the binary, or use exec.Command with the variable pre-set in the child's environment.

# Set GODEBUG in the shell before running the binary.
# The runtime reads this during startup.
GODEBUG=gctrace=1 ./myapp

GODEBUG is a startup flag. Set it in the shell.

Pitfalls and errors

os.Setenv returns an error. The compiler allows you to ignore it, but go vet warns about the ignored return value. The function returns an error if the operating system rejects the variable. This happens rarely, but it can occur if the environment size exceeds system limits. On Linux, there is a limit called ARG_MAX. If your environment grows too large, exec calls fail. Setenv can return an error if the OS refuses the new variable due to size.

Check the error. The convention is if err != nil { return err }. It is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible.

Another pitfall is the parent process illusion. Users often expect os.Setenv to update the shell. It does not. The shell is a separate process. Your program cannot modify the shell's environment. If you need to set a variable for the user's shell, you must print a command for the shell to source, or use a wrapper script.

The worst environment bug is the one that silently inherits secrets. Be careful with os.Environ. It includes all variables, including passwords and tokens. If you log the environment or pass it to untrusted code, you leak secrets. Filter sensitive keys before exposing the environment.

Trust the return value of Setenv. Check the error.

Decision matrix

Use os.Setenv when you need to change a variable for the current process and all future child processes.

Use os.Unsetenv when you want to remove a variable from the process environment.

Use exec.Command with a custom Env slice when you need to pass variables to a subprocess without affecting the parent process.

Use os.LookupEnv when you need to distinguish between a missing variable and a variable set to an empty string.

Use os.Environ when you need to inspect or copy the full environment map.

Use shell export or wrapper scripts when you need to set a variable for the parent shell or the user's session.

Where to go next