Go en Ubuntu: instalando y escribiendo tu primer programa

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

Nota (2026): Esta entrada era, en 2017, la instalación de Go 1.8.3 con GOPATH, una forma de trabajar que el propio lenguaje abandonó. La reescribimos como un hello world de verdad en Ubuntu, con la versión actual y con módulos.

Este es el camino más corto entre un Ubuntu limpio y tu primer programa en Go funcionando. Lleva unos diez minutos, y al final sales con un binario único que corre en cualquier Linux: sin intérprete, sin máquina virtual, sin dependencias que instalar en el servidor.

Por qué Go, en dos frases

Go fue diseñado en Google en 2007 porque C++ tardaba demasiado en compilar y la concurrencia daba demasiado trabajo. El resultado: compila en segundos, tiene concurrencia incorporada en el lenguaje y ofrece un binario estático. Por eso Docker, Kubernetes, Terraform y Prometheus están escritos en él. La historia completa está en La historia del lenguaje Go.

Instalación en Ubuntu

Existen tres caminos. El tarball oficial es el recomendado — el paquete de apt suele quedarse una o dos versiones atrás, y en Go eso importa porque el lenguaje lanza una versión cada seis meses.

# 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"

Fíjate en el rm -rf /usr/local/go antes de extraer: es el paso que la documentación oficial exige y que casi todos los tutoriales olvidan. Extraer sobre una instalación antigua mezcla archivos de dos versiones y genera un error difícil de diagnosticar.

Ahora el PATH. Para que tenga efecto en todo el sistema y sobreviva a nuevos usuarios:

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

go version

Si ha aparecido algo como go version go1.27.1 linux/amd64, está listo. Si el comando no se ha encontrado, el source anterior lo aplica en la sesión actual; en las próximas se cargará solo.

En una máquina ARM, como una Raspberry Pi, cambia linux-amd64 por linux-arm64.

Las alternativas, y cuándo usarlas

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

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

El apt sirve si solo necesitas compilar algo de terceros y no te importa la versión. Para desarrollar, quédate con el tarball.

Hello, world

Antes de cualquier estructura de proyecto, mira el lenguaje puro. Crea ola.go:

package main

import "fmt"

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

El go run compila y ejecuta en memoria, sin dejar binario: es el modo de experimentar. Tres cosas a tener en cuenta: todo programa ejecutable empieza en package main; la función de entrada es func main(); y el import es obligatorio y no puede sobrar. Si importas un paquete y no lo usas, Go se niega a compilar. Es rígido a propósito.

Ahora como es debido: un módulo

En la práctica nadie trabaja con un archivo suelto. Go organiza los proyectos en módulos desde la versión 1.11, y ese es el cambio que jubiló al viejo GOPATH de esta entrada en 2017:

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

Esto crea el go.mod, que declara el nombre del módulo y la versión del lenguaje. Ahora un programa un poco más interesante — crea 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 .

Aquí aparece la marca registrada del lenguaje: el if err != nil. En Go no existe la excepción — toda función que puede fallar devuelve el error junto con el resultado, y estás obligado a decidir qué hacer con él. Es verboso, se critica, y es intencionado: el camino del error queda visible en el código, no escondido en un catch lejos.

Generando el binario

Esta es la parte que vende el lenguaje a quien administra servidores:

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

El archivo ola es un ejecutable completo. Cópialo a otro Linux y funciona — no hace falta instalar Go allí, ni runtime, ni bibliotecas. Y se puede compilar para otro sistema sin salir del tuyo:

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

Compilación cruzada en una variable de entorno, sin toolchain extra. Por eso tantas herramientas de línea de comandos modernas se distribuyen como un binario por plataforma.

Usando una dependencia

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 .

El go.sum, creado automáticamente, guarda el hash de cada dependencia: es lo que garantiza que la versión descargada mañana es byte a byte la misma de hoy. Versiona ambos archivos, go.mod y go.sum, junto con el código.

Un adelanto de concurrencia

No se puede presentar Go sin mostrar la goroutine, que es el motivo por el que el lenguaje ha conquistado la infraestructura:

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 .

Los tres sitios se consultan en paralelo, y el total tarda lo que el más lento, no la suma de los tres. Esto es una palabra — go — y un canal. Sin biblioteca, sin pool de threads, sin callback.

Las herramientas que ya vienen incluidas

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

El go fmt merece destaque: el lenguaje tiene un estilo oficial, aplicado por una herramienta. Esto zanjó, de raíz, la discusión sobre tabulación y llaves que consume tiempo en otras comunidades.

Varias versiones en la misma máquina

Cuando un proyecto está atascado en una versión antigua, el propio Go lo resuelve — sin gestor externo:

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

Y, desde la 1.21, existe algo aún mejor: si el go.mod del proyecto pide una versión que no tienes, el GOTOOLCHAIN descarga la correcta en el momento del build, por sí solo.

Por dónde seguir

Diez minutos, un binario estático y un gopher azul. Buen comienzo.