Go on Ubuntu: installing and writing your first program

Mascote do LinuxPro digitando no laptop com o primeiro programa compilando no terminal, ao lado do gopher do Go

Note (2026): In 2017, this post covered installing Go 1.8.3 with GOPATH — a way of working that the language itself abandoned. We rewrote it as a hello world that actually runs on Ubuntu, with the current version and modules.

This is the shortest path from a clean Ubuntu installation to your first Go program running. It takes about ten minutes, and you end up with a single binary that runs on any Linux system — no interpreter, no virtual machine, no dependencies to install on the server.

Why Go, in two sentences

Go was designed at Google in 2007 because C++ took too long to compile and concurrency was too much work. The result: it compiles in seconds, has concurrency built into the language, and produces a static binary. That is why Docker, Kubernetes, Terraform, and Prometheus are written in it. The full story is in The history of the Go language.

Installing on Ubuntu

There are three options. The official tarball is recommended — the package from apt usually lags one or two versions behind, and that matters in Go because the language releases every six months.

# descobre a versão atual sem abrir o navegador
VER=$(curl -fsSL 'https://go.dev/VERSION?m=text' | head -n1)
echo "$VER"

# baixa e instala no lugar canônico
cd /tmp
curl -fLO "https://go.dev/dl/${VER}.linux-amd64.tar.gz"
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf "${VER}.linux-amd64.tar.gz"

Notice rm -rf /usr/local/go before extracting: it is the step the official documentation requires and almost every tutorial forgets. Extracting over an old installation mixes files from two versions and produces errors that are hard to diagnose.

Now for PATH. To apply it system-wide and make it work for new users:

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

go version

If you see something like go version go1.27.1 linux/amd64, you are ready. If the command was not found, the source above fixes it for the current session — in future sessions it loads automatically.

On an ARM machine, such as a Raspberry Pi, replace linux-amd64 with linux-arm64.

The alternatives, and when to use them

# snap — versão recente, atualiza sozinho
sudo snap install go --classic

# apt — mais simples, versão mais velha
sudo apt install golang-go

apt works if you only need to compile someone else's software and do not care about the version. For development, stick with the tarball.

Hello, world

Before setting up a project structure, look at the bare language. Create ola.go:

package main

import "fmt"

func main() {
    fmt.Println("Olá, mundo!")
}
go run ola.go

go run compiles and runs in memory without leaving a binary — it is the way to experiment. Three things to notice: every executable program starts with package main; the entry-point function is func main(); and an import is required and cannot be unused. If you import a package without using it, Go refuses to compile. It is deliberately strict.

Now the right way: a module

In practice, nobody works with standalone files. Go has organized projects into modules since version 1.11, and this is the change that retired the old GOPATH from this post in 2017:

mkdir ~/ola && cd ~/ola
go mod init exemplo.com/ola

This creates go.mod, which declares the module name and language version. Now for a slightly more interesting program — create main.go:

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    fmt.Print("Qual o seu nome? ")

    leitor := bufio.NewReader(os.Stdin)
    nome, err := leitor.ReadString('n')
    if err != nil {
        fmt.Fprintln(os.Stderr, "erro ao ler a entrada:", err)
        os.Exit(1)
    }

    nome = strings.TrimSpace(nome)
    if nome == "" {
        fmt.Println("Você não digitou nada.")
        return
    }

    fmt.Printf("Olá, %s! Bem-vindo ao Go.n", nome)
}
go run .

Here is the language's signature feature: if err != nil. Go has no exceptions — every function that can fail returns an error along with its result, and you must decide what to do with it. It is verbose, it is criticized, and it is intentional: the error path is visible in the code, not hidden in a catch far away.

Building the binary

This is the part that sells the language to server administrators:

go build -o ola
ls -lh ola
./ola

The file ola is a complete executable. Copy it to another Linux system and it runs — no need to install Go there, or a runtime, or a library. You can also compile for another system without leaving your own:

GOOS=linux   GOARCH=arm64 go build -o ola-arm      # Raspberry Pi
GOOS=windows GOARCH=amd64 go build -o ola.exe      # Windows
GOOS=darwin  GOARCH=arm64 go build -o ola-mac      # Mac com Apple Silicon

Cross-compilation with one environment variable, no extra toolchain. That is why so many modern command-line tools are distributed as one binary per platform.

Using a dependency

go get github.com/fatih/color
package main

import "github.com/fatih/color"

func main() {
    color.Green("Olá, mundo!")
    color.New(color.FgYellow, color.Bold).Println("agora em amarelo e negrito")
}
go mod tidy    # ajusta o go.mod: adiciona o que falta, remove o que sobra
go run .

The go.sum file, created automatically, stores the hash of each dependency — ensuring that the version downloaded tomorrow is byte-for-byte identical to today's. Keep both files under version control, go.mod and go.sum, along with the code.

A taste of concurrency

You cannot introduce Go without showing the goroutine, which is why the language won over infrastructure:

package main

import (
    "fmt"
    "net/http"
    "time"
)

func checar(url string, resultado chan<- string) {
    inicio := time.Now()
    resp, err := http.Get(url)
    if err != nil {
        resultado <- fmt.Sprintf("%-28s ERRO: %v", url, err)
        return
    }
    defer resp.Body.Close()
    resultado <- fmt.Sprintf("%-28s %s em %v", url, resp.Status, time.Since(inicio).Round(time.Millisecond))
}

func main() {
    sites := []string{
        "https://www.linuxpro.com.br",
        "https://go.dev",
        "https://kernel.org",
    }

    resultado := make(chan string)
    for _, s := range sites {
        go checar(s, resultado)     // dispara os três ao mesmo tempo
    }

    for range sites {
        fmt.Println(<-resultado)    // recebe conforme forem chegando
    }
}
go run .

The three sites are queried in parallel, and the total takes as long as the slowest, not the sum of all three. That takes one word — go — and one channel. No library, no thread pool, no callback.

The tools that come included

go fmt ./...      # formata no padrão oficial; não há debate de estilo em Go
go vet ./...      # aponta erro provável que compila mesmo assim
go test ./...     # roda os testes
go doc fmt.Println   # documentação sem sair do terminal
go clean -cache   # limpa o cache de build quando algo fica estranho

go fmt deserves special mention: the language has one official style, enforced by a tool. From the outset, this ended the debate over tabs and braces that consumes time in other communities.

Multiple versions on the same machine

When a project is stuck on an older version, Go itself handles it — no external manager:

go install golang.org/dl/go1.26@latest
go1.26 download
go1.26 version

And since 1.21, there is something even better: if the go.mod for the project requires a version you do not have, GOTOOLCHAIN downloads the right one at build time, automatically.

Where to go next

Ten minutes, a static binary, and a blue gopher. A good start.