Dev

Install Go (Golang) on Ubuntu 26.04 LTS

The Go you install on Ubuntu 26.04 is not always the Go that builds your code. Ubuntu’s archive ships the 1.26 line, but when a go.mod asks for a newer release, the go command downloads that toolchain into your home directory and switches to it without asking. On our test VM that silent download added 325 MB under ~/go the first time a module asked for the current release.

Original content from computingforgeeks.com - post 166022

This guide installs Go on Ubuntu 26.04 from the official go.dev tarball with a checksum check, then compares it with the apt and snap packages you could use instead. You’ll set up PATH, build and test a small module, cross-compile it for arm64, install a real tool with go install, upgrade, and remove everything cleanly. Everything below was run on Ubuntu 26.04 in October 2026 with Go 1.27.2 from go.dev, 1.26.8 from the Ubuntu archive and 1.27.2 from the snap.

Three sources ship three versions

You can get Go onto Ubuntu 26.04 from the go.dev tarball, from Ubuntu’s golang-go package or from the go snap, and they don’t hand you the same release. We installed each one on the same fresh VM and recorded what it gave us:

SourceVersion we gotDownloadInstall timeGOROOT
go.dev tarballgo1.27.267.3 MiB tarball5.9 s download, then extract/usr/local/go
Ubuntu archive (golang-go)go1.26.8117 MB, 33 packages25.7 s/usr/lib/go-1.26
Snap (go --classic)go1.27.266.2 MB snap32.8 s/snap/go/11327

The archive is a full minor release behind, and it pulls 33 packages because apt installs its recommended GCC, G++ and pkgconf for cgo. That costs 478 MB of disk against 283 MB for the extracted tarball, so the apt route is also the heaviest one. Which one should you pick? It depends on who you want to own the updates:

  1. Tarball: current release, upgraded by you.
  2. Snap: current release, refreshed by snapd on its schedule.
  3. apt: the 1.26 line, with Ubuntu updates and a C toolchain.

We use the tarball for the rest of this guide, because it is the only method where the version you see is the version you chose. All three set GOTOOLCHAIN=auto, though, so none of them stops a module from fetching a newer toolchain, and that changes what “the installed version” means in practice.

Prerequisites

You need very little for the tarball method, and everything on this list was already on the Ubuntu 26.04 cloud image we started from:

  • Ubuntu 26.04 LTS on amd64 or arm64
  • a user with sudo
  • curl and python3
  • about 300 MB of disk for the toolchain

Your CPU architecture matters for one reason: dpkg --print-architecture returns the same names go.dev uses for amd64 and arm64, so the commands below can read it directly. Caches need space too, because after one go install our module cache held 28 MB and the build cache in ~/.cache/go-build held 184 MB. If you’re on Ubuntu 24.04 or Debian 13, the Go install guide for those releases covers their archive versions. On a brand-new server, run through the initial server setup for Ubuntu 26.04 first, so the sudo user and updates are in place before you add a toolchain.

Install Go on Ubuntu 26.04 from the go.dev tarball

The go.dev tarball is a self-contained toolchain: you unpack it into /usr/local/go, and nothing else on the system changes. You don’t need to hardcode a version, because go.dev publishes the current release name at a stable URL. Read it, along with your CPU architecture:

GO_VERSION=$(curl -fsSL 'https://go.dev/VERSION?m=text' | head -1)
ARCH=$(dpkg --print-architecture)
echo "${GO_VERSION} ${ARCH}"

On our VM the first line of that URL gave the release name, and the architecture is the one dpkg reports:

go1.27.2 amd64

Download the tarball for that release. On our VM the 70,590,635-byte file took 5.9 seconds, so a slow mirror is easy to spot:

curl -fLO "https://go.dev/dl/${GO_VERSION}.linux-${ARCH}.tar.gz"

Verify it before you extract it. The checksum comes from the JSON listing behind the go.dev download page, not from the server that delivered the file, so a bad mirror or a truncated download cannot vouch for itself. We query the full listing (include=all), because the default one only carries the newest patch of each supported release line and fails for anything older. Python is only used here to pick the right entry out of that JSON:

GO_SHA256=$(curl -fsSL 'https://go.dev/dl/?mode=json&include=all' | python3 -c 'import json,sys; f=sys.argv[1]; print(next(x["sha256"] for r in json.load(sys.stdin) for x in r["files"] if x["filename"] == f))' "${GO_VERSION}.linux-${ARCH}.tar.gz")

Now check the file and install it in one line. Go’s install notes warn that untarring over an existing tree is known to produce broken installations, which is why the old /usr/local/go is removed first. The && chain means nothing is deleted or extracted unless the checksum matches:

echo "${GO_SHA256}  ${GO_VERSION}.linux-${ARCH}.tar.gz" | sha256sum -c - && sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf "${GO_VERSION}.linux-${ARCH}.tar.gz"

You want OK as the only output. When we fed sha256sum a wrong hash on purpose, it printed FAILED, exited with status 1 and the rm and tar never ran, and that is your signal to delete the file and download it again:

go1.27.2.linux-amd64.tar.gz: OK

Delete the tarball once the line succeeds, since everything you need is now under /usr/local/go:

rm "${GO_VERSION}.linux-${ARCH}.tar.gz"

Your toolchain now sits under /usr/local/go, 283 MB unpacked. Your shell doesn’t know where it is yet, so go still returns “command not found” until the next step.

PATH needs two directories, not GOROOT

Older guides, including the April version of this one, tell you to export GOROOT and GOPATH. You need neither. The go binary works out its own GOROOT from where it lives, and GOPATH defaults to ~/go. What you do need is PATH, with two entries in it:

  1. /usr/local/go/bin for go and gofmt
  2. $HOME/go/bin for tools from go install

Setting GOROOT by hand also causes trouble later, because an exported GOROOT overrides the location each go binary would find on its own. If you ever switch to the apt or snap package, a leftover GOROOT points that binary at the tarball’s standard library. Write one line to /etc/profile.d/go.sh so every login shell gets both directories. The single quotes keep $HOME unexpanded, so each user gets their own ~/go/bin:

echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' | sudo tee /etc/profile.d/go.sh
source /etc/profile.d/go.sh
go version

New logins read the file on their own, and source only loads it into the shell you’re already in. The version should match the tarball you downloaded:

go version go1.27.2 linux/amd64

Ask go env where things live. These are the defaults the toolchain picked without any variables from you, and they show where your downloads will go:

go env GOROOT GOPATH GOBIN GOMODCACHE

GOROOT is the tarball, and everything you download or install goes under your home directory:

/usr/local/go
/home/kenneth/go
/home/kenneth/go/bin
/home/kenneth/go/pkg/mod

If go version says “command not found”, your shell did not read the profile file. Run the source line again, or log out and back in, and check that /etc/profile.d/go.sh contains the line above.

A module starts with go mod init

Every Go program now lives in a module, and go mod init creates the go.mod file that names it. The module path doesn’t have to exist anywhere, so example.com/hello is fine for local code. Create the project and its module:

mkdir -p ~/hello && cd ~/hello
go mod init example.com/hello

Open the program file in vim. It is a normal file in your home directory, so no sudo is needed:

vim main.go

Paste the program we built. It prints a greeting for the name you pass with -name:

package main

import (
	"flag"
	"fmt"
)

func greet(name string) string {
	return fmt.Sprintf("Hello, %s", name)
}

func main() {
	name := flag.String("name", "gopher", "who to greet")
	flag.Parse()
	fmt.Println(greet(*name))
}

Tests sit next to the code in files ending in _test.go, and the test runner ships with the toolchain, so there is nothing else to install. Open the test file:

vim main_test.go

Paste the test, which checks the one function the program has:

package main

import "testing"

func TestGreet(t *testing.T) {
	if got := greet("Ubuntu"); got != "Hello, Ubuntu" {
		t.Fatalf("greet() = %q", got)
	}
}

Run the program straight from source, then run the tests. go run compiles to a temporary binary and runs it, so nothing is left in the directory:

go run . -name Ubuntu
go test -v ./...

Both work, and the test passes. After this step the project holds three files:

  • go.mod, the module definition
  • main.go, the program
  • main_test.go, its test

Both commands printed this on our VM:

Hello, Ubuntu
=== RUN   TestGreet
--- PASS: TestGreet (0.00s)
PASS
ok  	example.com/hello	0.003s

go mod init also wrote go 1.27.2 into go.mod, the version of the toolchain that created it. Keep that line in mind, because it decides which toolchain runs your builds later.

Build a binary and check its size

go build writes a single executable, and you can strip its debug information to make it smaller. Build both versions and compare them:

go build -o hello .
ls -l hello && file hello
go build -ldflags="-s -w" -o hello-small .
ls -l hello-small

The default build is 2,463,409 bytes and statically linked, so it runs on any amd64 Linux box without Go installed. Stripping the symbol table and DWARF data brought it down to 1,593,504 bytes, 35% smaller. Debuggers such as Delve need that data, so we’d strip release builds only:

-rwxr-xr-x 1 kenneth kenneth 2463409 Oct 18 06:40 hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=FuYWITFjyQjfnYIFuKkA/sD1VQ9JS9Jr14ZitcO8A/FrPcjbUHTC4yUxTc51RG/GjgF8SQK_OLXiudnwcQ2, BuildID[sha1]=d4f8ca017e53d7e82728b852d0caf816fbcfda6f, with debug_info, not stripped
-rwxr-xr-x 1 kenneth kenneth 1593504 Oct 18 06:40 hello-small

Build speed depends almost entirely on the build cache. After go clean -cache, the first build of this program took 6.1 seconds on our 2 vCPU VM, because it had to compile the standard library packages the program imports. With the cache warm, a rebuild after touching main.go took 0.07 seconds, so the slow first build is a one-off cost.

Cross-compiling needs two variables

Go cross-compiles without an extra toolchain. You set the target OS and CPU in two environment variables, and the compiler does the rest, so for a pure Go program like this one there is nothing to install. Build for 64-bit ARM, which Graviton instances and a Raspberry Pi 4 or 5 on a 64-bit OS both use:

GOOS=linux GOARCH=arm64 go build -o hello-arm64 .
file hello-arm64

file confirms the target. The arm64 binary was 2,380,191 bytes, and its first build took 6.2 seconds, because the standard library had to be compiled once for arm64:

hello-arm64: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, Go BuildID=vAnK9nfSWiwpTx2EhVhV/OdxysVOQYL4mKRFp_wXy/2zxJvGvmIwhR-7KIUjHM/ESf1KO2lsagHaPi-v8-m, BuildID[sha1]=6afae71612a6d1d06c0eda5e24c633f7f89a797b, with debug_info, not stripped

What happens when you run it on the build host? It fails with cannot execute binary file: Exec format error and exit status 126, which is the expected result on amd64. Copy it to an arm64 machine to run it. Three things decide whether a cross build works:

  • GOOS, the target operating system
  • GOARCH, the target CPU
  • cgo, which needs a C cross-compiler

This program does not use cgo, so the first two were enough. go tool dist list prints every GOOS/GOARCH pair the toolchain supports. The capture below is the same session on the lab VM, from go version through the arm64 build:

Go version, go env paths, go test and arm64 cross-compiled binary on Ubuntu 26.04

The arm64 file at the bottom is the one you copy to a Graviton instance or a Pi, while hello and hello-small stay with amd64 hosts.

go install puts tools in ~/go/bin

go install builds a program from its module path and places the binary in GOBIN, which is ~/go/bin here. That is the second PATH entry from earlier doing its job. We installed govulncheck, the Go team’s vulnerability scanner, and ran it against the project:

go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...

Installing it downloaded five modules and took 22.5 seconds, and the binary landed at /home/kenneth/go/bin/govulncheck (15.9 MB). The scan came back clean, because the program only uses the standard library of a current release:

go: downloading golang.org/x/vuln v1.9.0
go: downloading golang.org/x/telemetry v0.0.0-20261010193011-f71dc92da45e
go: downloading golang.org/x/mod v0.42.0
go: downloading golang.org/x/tools v0.52.0
go: downloading golang.org/x/sync v0.24.0
No vulnerabilities found.

@latest resolves to the newest tagged release, so running the same command later upgrades the tool. In CI you want the same scanner on every run, so pin a version there instead, such as @v1.9.0.

go.mod can switch the toolchain

The go line in go.mod is a minimum. With the default GOTOOLCHAIN=auto, an older go command meets it by downloading the newer toolchain and running that instead, as Go’s toolchain documentation describes. To see it on a real system, we installed the previous release, go1.26.9, from its tarball and ran go version inside ~/hello, whose go.mod says go 1.27.2. In order:

  1. Outside the module, go version reported go1.26.9.
  2. Inside ~/hello, it downloaded go1.27.2 and reported it.
  3. With GOTOOLCHAIN=local, the build failed.

You would see the failure from step 3 anywhere GOTOOLCHAIN=local is set, such as a locked-down CI image:

go: go.mod requires go >= 1.27.2 (running go 1.26.9; GOTOOLCHAIN=local)

The downloaded toolchain is stored as a module under ~/go/pkg/mod/golang.org/[email protected]. Ubuntu’s golang-go behaves the same way, because its /usr/lib/go-1.26/go.env sets GOTOOLCHAIN=auto, and on our apt install a go.mod asking for 1.27.2 pulled the newer toolchain just the same. So the version apt shows you is a floor, not a guarantee. All three results are in this capture from the lab VM:

go.mod requiring a newer Go downloads the toolchain, GOTOOLCHAIN=local fails on Ubuntu 26.04

Don’t rely on spotting the go: downloading line. It only appears the first time a toolchain lands in your module cache, and when we repeated the test after a rollback to go1.27.1, the second go version in the same module switched silently. Ask Go directly instead. Run this inside the module directory, because the answer depends on its go.mod.

go env GOVERSION

On a current install both answers are the same. We ran it after the rollback described in the next section, with go1.27.1 installed: inside ~/hello it printed go1.27.2, while GOTOOLCHAIN=local go env GOVERSION printed go1.27.1, the toolchain actually installed. If you want the installed toolchain to be the only one that ever runs, set it for your user with go env -w, which writes to ~/.config/go/env and survives new shells:

go env -w GOTOOLCHAIN=local

With that set, the build in ~/hello on go1.27.1 failed with the same “go.mod requires” error. go env -u GOTOOLCHAIN removes the setting again and brings back auto, which we confirmed with go env GOTOOLCHAIN.

Upgrades replace /usr/local/go

A tarball install has no package manager behind it, so upgrading means repeating the install with the new release. Re-run the commands from the start of this guide in order:

  1. Read GO_VERSION and ARCH from go.dev.
  2. Download the tarball and look up its checksum.
  3. Run the verify, rm -rf and tar line.

We tested exactly that on top of go1.26.9 and again on top of go1.27.1. Each time go version reported go1.27.2 afterwards, and ~/hello rebuilt with GOTOOLCHAIN=local. Nothing under ~/go needs touching, and your installed tools keep working, although rerunning go install for them rebuilds them with the new release.

Rolling back works the same way, except that you set the release yourself instead of reading it from go.dev. Pick the version from the go.dev download page, then run step 2 and step 3 unchanged:

GO_VERSION=go1.27.1 #https://go.dev/dl/
ARCH=$(dpkg --print-architecture)

That is what we ran for the rollback to the previous patch. The include=all listing returned its checksum, the line printed OK, and go version outside a module reported go1.27.1. The default listing without include=all failed for the same file with a Python StopIteration, because it carries only the newest patch of each line.

apt and snap trade freshness for convenience

Both alternatives worked on Ubuntu 26.04, with the version differences from the table at the top. If you want a package manager to own Go, refresh the index and install the archive package:

sudo apt update
sudo apt install -y golang-go

It puts go in /usr/bin as a link to /usr/lib/go-1.26/bin/go, so it is on PATH without a profile file. The version is the archive’s 1.26 build:

go version go1.26.8 linux/amd64

The 33 packages come from recommends, not hard dependencies: golang-1.26-go depends only on its source package and recommends gcc, g++, libc6-dev and pkgconf, which cgo needs. Add --no-install-recommends if you only build pure Go code and don’t want a C toolchain.

The snap uses classic confinement, so it can read and write your whole home directory like a normal binary. Install it from the default channel:

sudo snap install go --classic

On our VM it installed go1.27.2 from latest/stable into /snap/bin/go. The 32.8 seconds in the table include snapd refreshing itself first, which happens once on a fresh image. The snap also offers per-release channels such as 1.27/stable, and --channel=1.27/stable holds you on one minor release while still taking its patch updates.

Pick one method per machine. With the tarball’s profile line in place, the shell searched for go in this order on our VM:

  1. /usr/bin, the apt link
  2. /snap/bin, the snap
  3. /usr/local/go/bin, the tarball

An apt or snap go would therefore win over the tarball, and go version would report a different release than the one you just extracted. Run command -v go whenever the version looks wrong, because it prints the path the shell actually picked.

Removing Go leaves a read-only cache

The Go module cache is read-only by design. That protects downloaded source from accidental edits, but it trips up anyone who removes Go with a plain rm -rf. Clean the cache with Go’s own command first, while go is still installed. The last line also deletes anything else under ~/go, including old GOPATH-style code in ~/go/src and the tools in ~/go/bin, so move anything you want to keep first:

go clean -modcache
go env -u GOTOOLCHAIN
sudo rm -rf /usr/local/go
sudo rm /etc/profile.d/go.sh
rm -rf ~/go ~/.cache/go-build

In our run the cache clean and the deletes exited 0, and a new login no longer found go. The go env -u line matters only if you set GOTOOLCHAIN=local earlier: that setting lives in ~/.config/go/env, outside ~/go, and a later reinstall would silently keep it. The order is the same for the apt and snap packages: run go clean -modcache while their go still exists, then remove the package. We did that on the apt install:

go clean -modcache
sudo apt purge -y golang-go
sudo apt autoremove --purge
rm -rf ~/go ~/.cache/go-build

We ran autoremove with -y; the block leaves it off so apt shows the list and asks before it removes anything. On our VM that list held the rest of Go (golang-1.26-go, golang-1.26-src, golang-src) plus gcc, g++, pkgconf and the rest of the C toolchain, 478 MB together, and removing the compiler breaks anything else on the box that builds C code. If you want to keep the compiler, answer no and purge only the Go packages instead. We didn’t run this variant, but the names are the ones apt installed on our VM:

sudo apt purge golang-go golang-1.26-go golang-1.26-src golang-src

It leaves gcc, g++, pkgconf and their libraries installed and marked as automatic, so a later autoremove would still offer to remove them. For the snap, sudo snap remove --purge go takes the place of the two apt lines.

Error: “rm: cannot remove ‘/home/kenneth/go/pkg/mod/…’: Permission denied”

This is what happens when you skip go clean -modcache. On our VM, rm -rf ~/go exited with status 1 and printed 14,339 “Permission denied” lines. Module directories are mode dr-xr-xr-x, and rm cannot delete files inside a directory you cannot write to:

rm: cannot remove '/home/kenneth/go/pkg/mod/golang.org/x/[email protected]/LICENSE': Permission denied

If you already removed the toolchain and have no go left to clean with, make the tree writable first and then delete it:

chmod -R u+w ~/go
rm -rf ~/go

When we ran those two commands on a cache that had just refused rm -rf, both exited 0 and ~/go was gone.

Where each install method puts Go

Each method puts Go in a different place, which matters when you upgrade, remove it or wonder which go your shell is running. This is what we saw on Ubuntu 26.04:

go.dev tarballapt golang-gosnap go
go binary/usr/local/go/bin/go/usr/bin/go/snap/bin/go
GOROOT/usr/local/go/usr/lib/go-1.26/snap/go/<revision>
PATH setup/etc/profile.d/go.shnonenone
GOTOOLCHAIN defaultautoautoauto
Upgradesrerun the installapt upgrade, 1.26 line onlysnapd refresh
Removalrm -rf /usr/local/goapt purge golang-gosnap remove --purge go

GOPATH, the module cache and the build cache sit under your home directory with all three, so go clean -modcache is part of every removal. The same tarball method is covered in our guides for Rocky Linux and AlmaLinux, Fedora and Linux Mint. If you plan to ship these binaries in containers, set up Docker CE on Ubuntu 26.04 next, because a static Go binary needs very little underneath it.

Keep reading

Upgrade Ubuntu 24.04 to Ubuntu 26.04 LTS (Step by Step) Ubuntu Upgrade Ubuntu 24.04 to Ubuntu 26.04 LTS (Step by Step) UFW Firewall Commands with Examples on Ubuntu 26.04 / 24.04 Security UFW Firewall Commands with Examples on Ubuntu 26.04 / 24.04 Ubuntu 26.04 vs 24.04 LTS: Should You Upgrade Now? Ubuntu Ubuntu 26.04 vs 24.04 LTS: Should You Upgrade Now? Upgrade a Single Package on Ubuntu / Debian with apt Debian Upgrade a Single Package on Ubuntu / Debian with apt APT Cheat Sheet: apt and apt-get Commands for Ubuntu/Debian Cheat Sheets APT Cheat Sheet: apt and apt-get Commands for Ubuntu/Debian Build Your Own Music Streamer with Koel Ubuntu Build Your Own Music Streamer with Koel

Leave a Comment

Press ESC to close