Skip to main content

Environment Management

IMO, Keeping all language versions in a dedicated ~/language-versions folder is the cleanest and most explicit way to manage multiple versions without cluttering system paths. It gives you:

Full control over versions (install, remove, switch at will).
No clutter in /usr/local/bin, /usr/bin, or $HOME/bin.
No conflicts with system-wide installations.
Explicit switching via PATH, aliases, or symlinks(what I actually use).


🚀 A Universal Multi-Language Versioning Setup

If you want to apply this approach to Go, Python, Node.js, Rust, etc., you can structure it like this:

~/language-versions/
├── go/
│ ├── go1.22.9/
│ ├── go1.21.6/
│ ├── go1.18.10/
│ ├── current/ # (symlink to active version)

├── python/
│ ├── python3.12/
│ ├── python3.11/
│ ├── python3.10/
│ ├── current/ # (symlink to active version)

├── node/
│ ├── node18/
│ ├── node16/
│ ├── node14/
│ ├── current/ # (symlink to active version)

1️⃣ Installing Languages in ~/language-versions

🔹 Go (Explicit Versioning)

mkdir -p ~/language-versions/go
cd ~/language-versions/go

# Download and extract Go versions
curl -LO https://go.dev/dl/go1.22.9.linux-amd64.tar.gz
tar -xzf go1.22.9.linux-amd64.tar.gz
# This creates a folder named go --- so its ~/language-versions/go/go
# rename the inner go (the output of our tar) to the version number
mv go 1.22.9

Do the same for go1.21.6, go1.18.10, etc.


🔹 Python (Manual Install)

mkdir -p ~/language-versions/python
cd ~/language-versions/python

# Install Python versions
sudo apt install python3.12 python3.11 python3.10 # Ubuntu

If using source:

curl -LO https://www.python.org/ftp/python/3.12.0/Python-3.12.0.tgz
tar -xzf Python-3.12.0.tgz
mv Python-3.12.0 python3.12

🔹 Node.js (Manual or n Tool)

mkdir -p ~/language-versions/node
cd ~/language-versions/node

# Install Node.js versions manually
curl -LO https://nodejs.org/dist/v18.17.0/node-v18.17.0-linux-x64.tar.gz
tar -xzf node-v18.17.0-linux-x64.tar.gz
mv node-v18.17.0-linux-x64 node18

2️⃣ Switching Versions Explicitly

You can switch versions explicitly by modifying PATH:

Go Example

export PATH=~/language-versions/go/1.22.9/bin:$PATH
go version # Shows Go 1.22.9

Python Example

export PATH=~/language-versions/python/python3.12/bin:$PATH
python --version # Shows Python 3.12

Node.js Example

export PATH=~/language-versions/node/node18/bin:$PATH
node -v # Shows Node.js v18

Instead of manually changing PATH, create a current symlink:

cd ~/language-versions/go
ln -sfn 1.22.9 current # Switch Go version

Then, just add one-time setup to .bashrc or .zshrc:

export PATH=~/language-versions/go/current/bin:$PATH

Now, every time you change current, it instantly switches to the new version.

Same idea for Python:

cd ~/language-versions/python
ln -sfn python3.12 current # Switch Python version

And Node.js:

cd ~/language-versions/node
ln -sfn node18 current # Switch Node.js version

Now you can switch versions just by updating the symlink!

you can even drop this in a function inside ~/.bashrc:

use-lang ()
{
lang=$1;
version=$2;
ln -sfn "$HOME/language-versions/$lang/$version" "$HOME/language-versions/$lang/current";
echo "✅ Switched $lang to $version"
}

🚀 Why This Method Rocks

No system-wide clutter (/usr/bin, /usr/local/bin stay clean).
Explicit version control (no magic happening behind the scenes). ✔ Easy switching with ln -sfn or export PATH.
Works across all languages (not tied to a tool like nvm, pyenv, or gvm).


🎯 Basic Summary

This method is basically "nvm/pyenv/gvm but explicit and self-contained."

  • Works on any machine, with any language.
  • No system-wide modifications.
  • You always know exactly where everything lives.

My Actual Setup

Some languages need additional environment variables set, therefore you may have some language specific logic. ChatGPT also gave me a slightly more robust version as I was setting up this current box.

# in ~/.bashrc
# Multi-language Version Setup
export PATH="$HOME/language-versions/go/current/bin:$PATH"

use-lang () {
local lang="$1"
local version="$2"
local root="$HOME/language-versions/$lang"
local target="$root/$version"
local current="$root/current"

if [[ -z "$lang" || -z "$version" ]]; then
echo "usage: use-lang <lang> <version>" >&2
return 2
fi
if [[ ! -d "$target" ]]; then
echo "✗ $target not found" >&2
return 1
fi

ln -sfn "$target" "$current"

# Language-specific env (Go)
if [[ "$lang" == "go" ]]; then
export GOROOT="$current"
fi

# Flush shell's command path cache so 'go' resolves to new bin
hash -r 2>/dev/null || true

# Show what we actually switched to
if [[ -x "$current/bin/go" ]]; then
local v; v="$("$current/bin/go" version 2>/dev/null)"
echo "✅ Switched $lang to $version → $v"
else
echo "✅ Switched $lang to $version"
fi
}

Use good ol ls -alh to ensure your symlink is pointing at the right dir:

symlink pointed at correct dir

Go specific Note

After unzipping you will see that the archive has a top level directory of go, so just mv (move/rename it) after untarring.

Note: unfortunately since the go folder is within the archive, there isn't a clean way to rename that with tar... but cmon its only a few more keystrokes.

# given as 1.22.9 as an example...
wget https://go.dev/dl/go1.22.9.linux-amd64.tar.gz

# list files | retain only first line of output (noticve that /go is tld)
tar -tf go1.22.9.linux-amd64.tar.gz | head -1

# do
tar -xzf go1.22.9.linux-amd64.tar.gz
mv go 1.22.9

Environment Variable Management

Additionally, I maintain a structured environment setup:

~/env_setup/<project>/local_backend.sh  # or 'prod', 'staging', etc.

I always source the appropriate file when opening a new shell, ensuring environment consistency without persisting changes beyond the current session:

source ~/env_setup/my_project/local_backend.sh