Skip to content

Instantly share code, notes, and snippets.

@alexjoedt
Last active December 14, 2025 18:15
Show Gist options
  • Select an option

  • Save alexjoedt/9c61f9cc4ce211430257b2febd68be9f to your computer and use it in GitHub Desktop.

Select an option

Save alexjoedt/9c61f9cc4ce211430257b2febd68be9f to your computer and use it in GitHub Desktop.
#!/bin/bash
# shellcheck disable=SC2016
set -euo pipefail
#==============================================================================
# Go Installation Script
#==============================================================================
# This script installs Go to $HOME/.local/go by default (user-local).
# Use --system-install for traditional /usr/local/go installation.
#
# Quick Install (from GitHub Gist):
# curl -fsSL https://gist.githubusercontent.com/alexjoedt/9c61f9cc4ce211430257b2febd68be9f/raw/a565fb60eae2b5a4c255890ef1cf4ce579d33b29/install_go.sh | bash
# curl -fsSL https://gist.githubusercontent.com/alexjoedt/9c61f9cc4ce211430257b2febd68be9f/raw/a565fb60eae2b5a4c255890ef1cf4ce579d33b29/install_go.sh | bash -s -- --with-tools
# curl -fsSL https://gist.githubusercontent.com/alexjoedt/9c61f9cc4ce211430257b2febd68be9f/raw/a565fb60eae2b5a4c255890ef1cf4ce579d33b29/install_go.sh | bash -s -- --system-install
# curl -fsSL https://gist.githubusercontent.com/alexjoedt/9c61f9cc4ce211430257b2febd68be9f/raw/a565fb60eae2b5a4c255890ef1cf4ce579d33b29/install_go.sh | bash -s -- --system-install --with-tools
#
# Flags:
# --with-tools Install Go development tools after installing Go
# --only-tools Only install tools (skip Go installation)
# --system-install Install to /usr/local/go (requires sudo)
# -y, --yes Skip all interactive prompts (auto-yes)
# -f, --force Force reinstall even if latest version exists
# -h, --help Show help message
#==============================================================================
#------------------------------------------------------------------------------
# Helper Functions
#------------------------------------------------------------------------------
# Print error message and exit
error_exit() {
echo "ERROR: $1" >&2
exit 1
}
# Print warning message
warn() {
echo "WARNING: $1" >&2
}
# Print info message
info() {
echo "INFO: $1"
}
# Show usage information
show_help() {
cat << EOF
Usage: $(basename "$0") [OPTIONS]
Install or update Go programming language.
OPTIONS:
--with-tools Install Go development tools after installing Go
--only-tools Only install tools (requires Go to be installed)
--system-install Install to /usr/local/go instead of ~/.local/go
-y, --yes Skip all interactive prompts (auto-accept)
-f, --force Force reinstall even if latest version is installed
-h, --help Show this help message
EXAMPLES:
$(basename "$0") # Install Go to ~/.local/go
$(basename "$0") --with-tools # Install Go and development tools
$(basename "$0") --system-install # Install to /usr/local/go (requires sudo)
$(basename "$0") --only-tools # Only install development tools
EOF
exit 0
}
# Check if required dependencies are available
check_dependencies() {
local missing_deps=()
# Check for curl or wget
if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
missing_deps+=("curl or wget")
fi
# Check for jq (required for parsing Go version JSON)
if ! command -v jq >/dev/null 2>&1; then
case "$(uname -s)" in
Linux)
info "Installing jq..."
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get -qq install jq -y >/dev/null 2>&1 || missing_deps+=("jq")
elif command -v pacman >/dev/null 2>&1; then
sudo pacman -S --noconfirm --quiet jq >/dev/null 2>&1 || missing_deps+=("jq")
elif command -v yum >/dev/null 2>&1; then
sudo yum install -q -y jq >/dev/null 2>&1 || missing_deps+=("jq")
elif command -v dnf >/dev/null 2>&1; then
sudo dnf install -q -y jq >/dev/null 2>&1 || missing_deps+=("jq")
else
missing_deps+=("jq")
fi
;;
Darwin)
if command -v brew >/dev/null 2>&1; then
info "Installing jq via Homebrew..."
brew install jq >/dev/null 2>&1 || missing_deps+=("jq")
else
missing_deps+=("jq (and Homebrew)")
fi
;;
*)
missing_deps+=("jq")
;;
esac
fi
if [[ ${#missing_deps[@]} -gt 0 ]]; then
error_exit "Missing required dependencies: ${missing_deps[*]}"
fi
}
# Detect operating system
detect_platform() {
local os
os="$(uname -s)"
case "$os" in
Linux)
echo "linux"
;;
Darwin)
echo "darwin"
;;
*)
error_exit "Unsupported operating system: $os"
;;
esac
}
# Detect system architecture and convert to Go naming
detect_architecture() {
local arch
arch="$(uname -m)"
case "$arch" in
x86_64)
echo "amd64"
;;
aarch64|arm64)
echo "arm64"
;;
armv6*|armv7*)
echo "armv6l"
;;
armv8*)
echo "arm64"
;;
i386|i686)
echo "386"
;;
*)
error_exit "Unsupported architecture: $arch"
;;
esac
}
# Check if system Go exists and warn user
check_system_go() {
local auto_yes="${1:-false}"
if [[ -d "/usr/local/go" ]]; then
warn "Go is already installed in /usr/local/go"
echo ""
echo "This script installs to $HOME/.local/go by default."
echo "Both installations can coexist, but may cause confusion."
echo ""
echo "To remove system Go first:"
echo " sudo rm -rf /usr/local/go"
echo ""
# Skip prompt if auto-yes or non-interactive
if [[ "$auto_yes" == true ]] || [[ ! -t 0 ]]; then
info "Auto-accepting: Proceeding with local installation..."
return 0
fi
read -r -p "Continue with local installation anyway? (y/N): " response
case "$response" in
[yY]|[yY][eE][sS])
info "Proceeding with local installation..."
;;
*)
info "Installation cancelled."
exit 0
;;
esac
fi
}
# Check if $HOME/.local/go/bin is in PATH
check_local_bin_in_path() {
if [[ ":$PATH:" != *":$HOME/.local/go/bin:"* ]]; then
warn "\$HOME/.local/go/bin is not in your \$PATH"
echo ""
echo "Go will be installed to $HOME/.local/go, but you won't be able to use it"
echo "until you add the Go bin directory to your shell configuration file."
echo "This will be done automatically at the end of installation."
echo ""
fi
}
# Get currently installed Go version
get_installed_version() {
if command -v go >/dev/null 2>&1; then
go version 2>/dev/null | awk '{print $3}' | sed 's/go//' || echo ""
else
echo ""
fi
}
# Get latest Go version and checksum from official API
# Outputs: version|sha256
get_latest_version_and_checksum() {
local platform="$1"
local arch="$2"
local api_response
local version
local sha256
local filename
# Fetch API data
api_response=$(curl -s "https://go.dev/dl/?mode=json" 2>/dev/null)
if [[ -z "$api_response" ]]; then
error_exit "Failed to fetch Go version information from API"
fi
# Get the first stable version and its checksum
version=$(echo "$api_response" | jq -r '.[0].version' 2>/dev/null)
if [[ -z "$version" || "$version" == "null" ]]; then
error_exit "Failed to parse Go version from API response"
fi
# Find the matching file for our platform/arch
filename="${version}.${platform}-${arch}.tar.gz"
sha256=$(echo "$api_response" | jq -r \
".[0].files[] | select(.filename==\"$filename\") | .sha256" 2>/dev/null)
# Fallback: try other stable versions if checksum not found
if [[ -z "$sha256" || "$sha256" == "null" ]]; then
warn "Latest version ${version} not available for ${platform}-${arch}, trying fallback..."
local versions_data
versions_data=$(echo "$api_response" | jq -r \
'.[] | select(.stable==true) | "\(.version)|\(.files[] | select(.os=="'"$platform"'" and .arch=="'"$arch"'" and .kind=="archive") | .sha256)"' 2>/dev/null)
while IFS='|' read -r ver chksum; do
if [[ -n "$chksum" && "$chksum" != "null" ]]; then
version="$ver"
sha256="$chksum"
break
fi
done <<< "$versions_data"
if [[ -z "$sha256" || "$sha256" == "null" ]]; then
error_exit "Could not find available Go version with checksum for ${platform}-${arch}"
fi
fi
echo "${version}|${sha256}"
}
# Download Go package
download_go() {
local url="$1"
local dest="$2"
info "Downloading $(basename "$dest")..."
if command -v wget >/dev/null 2>&1; then
wget -q --show-progress "$url" -O "$dest" || return 1
elif command -v curl >/dev/null 2>&1; then
curl -# -L -o "$dest" "$url" || return 1
else
error_exit "Neither wget nor curl is available"
fi
info "Download complete"
return 0
}
# Verify SHA256 checksum of downloaded file
verify_checksum() {
local file="$1"
local expected_sha256="$2"
local actual_sha256
info "Verifying checksum..."
# Use appropriate SHA256 tool based on OS
if command -v sha256sum >/dev/null 2>&1; then
actual_sha256=$(sha256sum "$file" | awk '{print $1}')
elif command -v shasum >/dev/null 2>&1; then
actual_sha256=$(shasum -a 256 "$file" | awk '{print $1}')
else
warn "No SHA256 tool found (sha256sum/shasum), skipping checksum verification"
return 0
fi
if [[ "$actual_sha256" != "$expected_sha256" ]]; then
error_exit "Checksum verification failed!
Expected: $expected_sha256
Actual: $actual_sha256"
fi
info "Checksum verified successfully"
return 0
}
# Detect shell and return RC file path
get_shell_rc() {
# Check current shell
if [[ -n "${ZSH_VERSION:-}" ]] || [[ "$SHELL" == *"zsh"* ]]; then
echo "$HOME/.zshrc"
elif [[ -n "${BASH_VERSION:-}" ]] || [[ "$SHELL" == *"bash"* ]]; then
echo "$HOME/.bashrc"
else
warn "Unknown shell: $SHELL"
echo "$HOME/.profile"
fi
}
# Print shell configuration instructions (does not modify files)
print_shell_config_instructions() {
local go_root="$1"
local shell_rc
shell_rc="$(get_shell_rc)"
# Check if already configured
if grep -q "$go_root" "$shell_rc" 2>/dev/null; then
info "Go appears to already be configured in $shell_rc"
return 0
fi
echo ""
info "Shell configuration required"
echo ""
echo "Add the following to your $shell_rc:"
echo ""
echo "# Go programming language"
echo "export GOROOT=\"$go_root\""
echo "export GOPATH=\"\${GOPATH:-\$HOME/go}\""
echo "export PATH=\"\$GOROOT/bin:\$GOPATH/bin:\$PATH\""
echo ""
echo "Then reload your shell:"
echo " source $shell_rc"
echo ""
}
# Install Go development tools
install_go_tools() {
info "Installing Go development tools..."
if ! command -v go >/dev/null 2>&1; then
error_exit "Go is not installed or not in PATH. Install Go first."
fi
local tools_url="https://gist.githubusercontent.com/alexjoedt/9c61f9cc4ce211430257b2febd68be9f/raw/f45048da7b29d9037579bbf726db1ff39cfaeef3/tools.go"
local temp_file="/tmp/go-tools.go"
if curl -fLo "$temp_file" "$tools_url" 2>/dev/null; then
go run "$temp_file" || warn "Some tools may have failed to install"
rm -f "$temp_file"
info "Go tools installation complete"
else
error_exit "Failed to download tools installer"
fi
}
#------------------------------------------------------------------------------
# Main Installation Function
#------------------------------------------------------------------------------
install_go() {
local force=false
local system_install=false
local with_tools=false
local only_tools=false
local auto_yes=false
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
show_help
;;
-f|--force)
force=true
shift
;;
-y|--yes)
auto_yes=true
shift
;;
--system-install)
system_install=true
shift
;;
--with-tools)
with_tools=true
shift
;;
--only-tools)
only_tools=true
shift
;;
*)
error_exit "Unknown option: $1. Use --help for usage information."
;;
esac
done
# Handle --only-tools flag
if [[ "$only_tools" == true ]]; then
install_go_tools
exit 0
fi
# Check dependencies
check_dependencies
# Determine installation directory
local goroot
local use_sudo=false
if [[ "$system_install" == true ]]; then
goroot="/usr/local/go"
use_sudo=true
info "Installing to system directory: $goroot"
else
goroot="${GOROOT:-$HOME/.local/go}"
info "Installing to user directory: $goroot"
# Check if system Go exists (only for local install)
check_system_go "$auto_yes"
# Verify ~/.local/go/bin is in PATH
check_local_bin_in_path
fi
# Set GOPATH if not already set
export GOPATH="${GOPATH:-$HOME/go}"
# Detect platform and architecture
local platform
local arch
platform="$(detect_platform)"
arch="$(detect_architecture)"
local platform_arch="${platform}-${arch}"
info "Detected platform: $platform_arch"
# Get version information
local installed_version
local version_data
local latest_version
local expected_sha256
installed_version="$(get_installed_version)"
info "Fetching latest Go version..."
version_data="$(get_latest_version_and_checksum "$platform" "$arch")"
latest_version="${version_data%%|*}"
expected_sha256="${version_data##*|}"
info "Latest Go version: ${latest_version#go}"
if [[ -n "$installed_version" ]]; then
info "Installed Go version: $installed_version"
fi
# Check if update is needed
if [[ "$force" == false ]] && [[ -n "$installed_version" ]] && [[ "$installed_version" == "${latest_version#go}" ]]; then
info "Latest Go version ($installed_version) is already installed"
if [[ "$with_tools" == true ]]; then
install_go_tools
fi
exit 0
fi
# Prepare download
local go_package="${latest_version}.${platform_arch}.tar.gz"
local download_url="https://dl.google.com/go/$go_package"
local temp_dir
temp_dir="$(mktemp -d)"
local temp_file="$temp_dir/$go_package"
# Set up cleanup trap
trap "rm -rf \"$temp_dir\"" EXIT
# Download Go
if ! download_go "$download_url" "$temp_file"; then
error_exit "Failed to download Go package"
fi
# Verify checksum
if ! verify_checksum "$temp_file" "$expected_sha256"; then
error_exit "Checksum verification failed"
fi
# Prepare installation directory
info "Preparing installation directory: $goroot"
if [[ -d "$goroot" ]]; then
if [[ "$use_sudo" == true ]]; then
sudo rm -rf "$goroot" || error_exit "Failed to remove old installation"
else
rm -rf "$goroot" || error_exit "Failed to remove old installation"
fi
fi
# Create parent directory if needed
local parent_dir
parent_dir="$(dirname "$goroot")"
if [[ ! -d "$parent_dir" ]]; then
if [[ "$use_sudo" == true ]]; then
sudo mkdir -p "$parent_dir" || error_exit "Failed to create directory: $parent_dir"
else
mkdir -p "$parent_dir" || error_exit "Failed to create directory: $parent_dir"
fi
fi
# Extract Go
info "Extracting Go to $goroot..."
if [[ "$use_sudo" == true ]]; then
if ! sudo tar -C "$parent_dir" -xzf "$temp_file"; then
error_exit "Failed to extract Go"
fi
else
if ! tar -C "$parent_dir" -xzf "$temp_file"; then
error_exit "Failed to extract Go"
fi
# For user install, handle the extracted 'go' directory
if [[ "$goroot" != "$parent_dir/go" ]] && [[ -d "$parent_dir/go" ]]; then
mv "$parent_dir/go" "$goroot" || error_exit "Failed to move Go to final location"
fi
fi
info "Go installation complete!"
# Print shell configuration instructions
print_shell_config_instructions "$goroot"
# Verify installation
if [[ -x "$goroot/bin/go" ]]; then
local new_version
new_version="$("$goroot/bin/go" version | awk '{print $3}' | sed 's/go//')"
info "Installed Go version: $new_version"
fi
# Install tools if requested
if [[ "$with_tools" == true ]]; then
# Temporarily add to PATH for tools installation
export PATH="$goroot/bin:$PATH"
install_go_tools
fi
echo ""
info "Installation successful!"
echo ""
}
#------------------------------------------------------------------------------
# Script Entry Point
#------------------------------------------------------------------------------
install_go "$@"
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Binary installtools is a helper that installs Go tools extension tests depend on.
package main
import (
"bytes"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
)
// finalVersion encodes the fact that the specified tool version
// is the known last version that can be buildable with goMinorVersion.
type finalVersion struct {
goMinorVersion int
version string
}
var tools = []struct {
path string
dest string
preferPreview bool
// versions is a list of supportedVersions sorted by
// goMinorVersion. If we want to pin a tool's version
// add a fake entry with a large goMinorVersion
// value and the pinned tool version as the last entry.
// Nil of empty list indicates we can use the `latest` version.
versions []finalVersion
}{
// TODO: auto-generate based on allTools.ts.in.
{"golang.org/x/tools/gopls", "", true, nil},
{"github.com/acroca/go-symbols", "", false, nil},
{"github.com/cweill/gotests/gotests", "", false, nil},
{"github.com/davidrjenni/reftools/cmd/fillstruct", "", false, nil},
{"github.com/haya14busa/goplay/cmd/goplay", "", false, nil},
{"github.com/stamblerre/gocode", "gocode-gomod", false, nil},
{"github.com/mdempsky/gocode", "", false, nil},
{"github.com/ramya-rao-a/go-outline", "", false, nil},
{"github.com/rogpeppe/godef", "", false, nil},
{"github.com/sqs/goreturns", "", false, nil},
{"github.com/uudashr/gopkgs/v2/cmd/gopkgs", "", false, nil},
{"github.com/zmb3/gogetdoc", "", false, nil},
{"honnef.co/go/tools/cmd/staticcheck", "", false, []finalVersion{{16, "v0.2.2"}, {18, "v0.3.3"}}},
{"golang.org/x/tools/cmd/gorename", "", false, nil},
{"github.com/go-delve/delve/cmd/dlv", "", false, []finalVersion{{16, "v1.8.3"}, {17, "v1.9.1"}}},
}
// pickVersion returns the version to install based on the supported
// version list.
func pickVersion(goMinorVersion int, versions []finalVersion, defaultVersion string) string {
for _, v := range versions {
if goMinorVersion <= v.goMinorVersion {
return v.version
}
}
return defaultVersion
}
func main() {
ver, err := goVersion()
if err != nil {
exitf("failed to find go version: %v", err)
}
if ver < 1 {
exitf("unsupported go version: 1.%v", ver)
}
bin, err := goBin()
if err != nil {
exitf("failed to determine go tool installation directory: %v", err)
}
err = installTools(bin, ver)
if err != nil {
exitf("failed to install tools: %v", err)
}
}
func exitf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format, args...)
os.Exit(1)
}
// goVersion returns an integer N if go's version is 1.N.
func goVersion() (int, error) {
cmd := exec.Command("go", "list", "-e", "-f", `{{context.ReleaseTags}}`, "--", "unsafe")
cmd.Env = append(os.Environ(), "GO111MODULE=off")
out, err := cmd.Output()
if err != nil {
return 0, fmt.Errorf("go list error: %v", err)
}
result := string(out)
if len(result) < 3 {
return 0, fmt.Errorf("bad ReleaseTagsOutput: %q", result)
}
// Split up "[go1.1 go1.15]"
tags := strings.Fields(result[1 : len(result)-2])
for i := len(tags) - 1; i >= 0; i-- {
var version int
if _, err := fmt.Sscanf(tags[i], "go1.%d", &version); err != nil {
continue
}
return version, nil
}
return 0, fmt.Errorf("no parseable ReleaseTags in %v", tags)
}
// goBin returns the directory where the go command will install binaries.
func goBin() (string, error) {
if gobin := os.Getenv("GOBIN"); gobin != "" {
return gobin, nil
}
out, err := exec.Command("go", "env", "GOPATH").Output()
if err != nil {
return "", err
}
gopaths := filepath.SplitList(strings.TrimSpace(string(out)))
if len(gopaths) == 0 {
return "", fmt.Errorf("invalid GOPATH: %s", out)
}
return filepath.Join(gopaths[0], "bin"), nil
}
func installTools(binDir string, goMinorVersion int) error {
installCmd := "install"
if goMinorVersion < 16 {
installCmd = "get"
}
dir := ""
if installCmd == "get" { // run `go get` command from an empty directory.
dir = os.TempDir()
}
env := append(os.Environ(), "GO111MODULE=on")
for _, tool := range tools {
ver := pickVersion(goMinorVersion, tool.versions, pickLatest(tool.path, tool.preferPreview))
path := tool.path + "@" + ver
cmd := exec.Command("go", installCmd, path)
cmd.Env = env
cmd.Dir = dir
fmt.Println("go", installCmd, path)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("installing %v: %s\n%v", path, out, err)
}
loc := filepath.Join(binDir, binName(tool.path))
if tool.dest != "" {
newLoc := filepath.Join(binDir, binName(tool.dest))
if err := os.Rename(loc, newLoc); err != nil {
return fmt.Errorf("copying %v to %v: %v", loc, newLoc, err)
}
loc = newLoc
}
fmt.Println("\tinstalled", loc)
}
return nil
}
func binName(toolPath string) string {
b := path.Base(toolPath)
if runtime.GOOS == "windows" {
return b + ".exe"
}
return b
}
func pickLatest(toolPath string, preferPreview bool) string {
if !preferPreview {
return "latest" // should we pick the pinned version in allTools.ts.in?
}
out, err := exec.Command("go", "list", "-m", "--versions", toolPath).Output()
if err != nil {
exitf("failed to find a suitable version for %q: %v", toolPath, err)
}
versions := bytes.Split(out, []byte(" "))
if len(versions) == 0 {
exitf("failed to find a suitable version for %q: %s", toolPath, out)
}
return string(bytes.TrimSpace(versions[len(versions)-1]))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment