Blog

Hola, Go: herramientas, módulos y tu primer programa

Un primer programa en Go necesita una herramienta de línea de comandos, un archivo go.mod y un paquete main. Qué hacen go run, go build, gofmt y go vet, y por qué Go no compila código con un import sin usar.

Go viene como una sola herramienta llamada go. Compila tu código, lo ejecuta, lo formatea, busca errores comunes y maneja sus dependencias. No eliges un sistema de compilación ni un formateador, porque ya vienen incluidos.

Este post instala esa herramienta, escribe un primer programa y recorre los comandos que vas a escribir todos los días. Cada programa de abajo se ejecutó con Go 1.26, y su salida está copiada de esa ejecución.

Instalar Go y comprobar que funciona

Go se instala desde la página de descargas en go.dev/dl, o con el gestor de paquetes de tu sistema. De cualquier forma, terminas con un comando go en tu path. Compruébalo:

$ go version
go version go1.26.2 linux/amd64

La última parte indica tu sistema operativo y tu procesador, así que el tuyo puede decir darwin/arm64 o windows/amd64. Lo que importa es que diga go1.26 o posterior.

Esa es toda la instalación. Los tutoriales viejos te piden crear una carpeta GOPATH y guardar todo tu código ahí. Ya no hace falta. Desde que llegaron los módulos, un proyecto de Go puede vivir en cualquier carpeta de tu disco.

GOPATH todavía existe, pero es un lugar que Go usa para sí mismo. Guarda una caché de las dependencias descargadas, y es donde go install pone los programas que compila. Puedes pasar mucho tiempo sin mirar adentro.

Tu primer programa

Un proyecto de Go empieza con una carpeta y un comando go mod init, que la convierte en un módulo:

$ mkdir hello
$ cd hello
$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello

Ahora crea un archivo llamado main.go en esa carpeta:

package main

import "fmt"

func main() {
	fmt.Println("Hello, Go")
}

Imprime:

Hello, Go

Ejecútalo con go run ., donde el punto significa “el paquete de esta carpeta”. Tres líneas hacen todo el trabajo.

package main dice a qué paquete pertenece este archivo. Todo archivo de Go empieza con una línea de paquete. Un paquete llamado main es especial: es el que se convierte en un programa que puedes ejecutar. Cualquier otro nombre crea una biblioteca que otro código importa.

import "fmt" trae el paquete de formato de la biblioteca estándar. fmt.Println imprime sus argumentos con espacios entre ellos y un salto de línea al final.

func main() es donde empieza el programa. No recibe argumentos y no devuelve nada. Cuando main termina, el programa termina.

Las dos mitades son obligatorias. Un paquete main sin una función main no puede convertirse en programa, y Go te lo dice:

package main

import "fmt"

func Main() {
	fmt.Println("Hello, Go")
}

La compilación falla con:

function main is undeclared in the main package

La M mayúscula la convirtió en otra función. Los nombres en Go distinguen mayúsculas de minúsculas y, como vas a ver en un momento, la primera letra significa algo.

go run y go build

Go te da dos formas de ejecutar un programa: go run para probarlo ahora, y go build para crear algo que puedes pasarle a otra persona.

$ go run .
Hello, Go
$ go build
$ ls
go.mod  hello  main.go
$ ./hello
Hello, Go

go run compila el programa en un archivo temporal, lo ejecuta y tira el archivo. go build lo compila y guarda el resultado, un archivo con el nombre del último elemento de la ruta del módulo, hello.

Ese archivo es el programa entero. Cópialo a otra máquina Linux del mismo tipo y se ejecuta ahí, aunque nunca se haya instalado Go en ella. No hay un runtime que instalar primero ni una carpeta de bibliotecas que enviar junto a él.

También puedes compilar para un sistema operativo distinto del que usas, con dos variables de entorno:

$ GOOS=windows GOARCH=amd64 go build
$ ls
go.mod  hello  hello.exe  main.go

hello.exe es un programa de Windows, compilado en Linux, sin instalar nada extra.

Explicado como si tuvieras diez años

Piensa en una receta y un pastel.

Un script, como un programa de Python, es una receta. Para tener pastel, la persona a quien se la das necesita una cocina, un horno y alguien que lea recetas. Si su horno es de otro modelo, el pastel puede salir mal.

Un programa de Go compilado es el pastel terminado dentro de una caja. La cocción ya pasó en tu computadora. La persona a quien se lo das solo abre la caja. No necesita cocina.

La versión precisa

El compilador de Go convierte tu código fuente, y cada paquete que importa, en código máquina para un sistema operativo y un procesador. Enlaza todo eso en un solo archivo ejecutable. El runtime de Go, que ejecuta el recolector de basura y planifica las goroutines, se compila dentro de ese mismo archivo.

Un programa como este, que usa solo paquetes de Go puro, está enlazado estáticamente: no carga bibliotecas compartidas al arrancar. Por eso el archivo pesa un par de megabytes para un programa que imprime una línea, y también por eso puedes copiarlo a cualquier lugar que coincida con su GOOS y su GOARCH.

Un programa de Python o JavaScript funciona al revés. Envías el código fuente, y la máquina necesita tener instalado el intérprete correcto para ejecutarlo.

Dónde falla la analogía: un pastel en caja se puede comer en cualquier lado, pero un binario de Go solo se ejecuta en el sistema para el que se compiló. Un binario de Linux no arranca en Windows. Horneas un pastel distinto para cada tipo de cocina, y Go lo hace barato con GOOS y GOARCH.

Qué guarda go.mod

El archivo go.mod que creó go mod init es corto. Aquí está completo:

$ cat go.mod
module example.com/hello

go 1.26.2

La línea module es la ruta del módulo, y es el prefijo de cada ruta de import dentro de él. Parece una dirección web porque los módulos publicados suelen encontrarse en una. Para un proyecto que nunca sale de tu máquina, cualquier nombre sirve, pero el estilo de dominio te evita problemas si algún día lo publicas.

La línea go dice para qué versión del lenguaje Go está escrito este módulo. go mod init puso la versión exacta que lo ejecutó, con número de parche incluido, así que la tuya va a coincidir con tus herramientas de Go.

Esa línea hace más que documentar. Activa y desactiva funciones del lenguaje. Cambia main.go por un bucle que use range sobre un entero, for i := range 3, algo que se agregó en Go 1.22. Después pon la línea go en una versión más vieja y ejecútalo:

$ go mod edit -go=1.21
$ go run .
# example.com/hello
./main.go:6:17: cannot range over 3 (untyped int constant): requires go1.22 or later (-lang was set to go1.21; check go.mod)

El compilador fue Go 1.26 todo el tiempo. Rechazó el bucle porque go.mod decía que el código apunta a 1.21. Así los módulos viejos siguen compilando igual mientras Go agrega funciones, y el error hasta te dice qué archivo revisar.

Cuando tu módulo empieza a usar código de otras personas, go.mod también lista esas dependencias y sus versiones. Eso vuelve en la parte sobre paquetes y módulos.

Imports y nombres exportados

La biblioteca estándar es un conjunto de paquetes que importas por su ruta, como fmt y strings. Una vez importados, llamas a lo que tienen adentro con el nombre del paquete, un punto y el nombre:

package main

import (
	"fmt"
	"strings"
)

func main() {
	title := "hello, go"
	fmt.Println(strings.ToUpper(title))
	fmt.Println(strings.Fields("  one   two three "))
	fmt.Println(strings.Contains(title, "go"))
}

Imprime:

HELLO, GO
[one two three]
true

Dos o más imports van en un bloque entre paréntesis, uno por línea. strings.ToUpper devuelve una copia en mayúsculas, strings.Fields separa donde hay espacios seguidos, y strings.Contains indica si una cadena aparece dentro de otra.

Cada nombre que llamaste ahí empieza con mayúscula: Println, ToUpper, Fields, Contains. No es una cuestión de estilo. En Go, un nombre que empieza con mayúscula está exportado, lo que significa que el código de fuera del paquete puede usarlo. Un nombre que empieza con minúscula es privado de su paquete.

No hay palabra clave public ni private. La primera letra es toda la regla. Si te equivocas, el programa no compila:

package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.toUpper("quiet"))
}

La compilación falla con:

./main.go:9:22: undefined: strings.toUpper (but have ToUpper)

El compilador no se limitó a decir que el nombre no existe. Encontró el nombre exportado que solo cambia en mayúsculas y lo sugirió. Vale la pena leer despacio mensajes de error como ese, porque muchas veces traen la solución.

La regla también aplica a tu propio código. Dentro del paquete main, una función llamada greeting y otra llamada Greeting funcionan las dos, porque nada fuera de main lo importa. Empieza a importar cuando divides el código en paquetes propios.

gofmt: un formato para todos

Go tiene un formato oficial para el código fuente, y una herramienta llamada gofmt lo aplica. Aquí hay un programa escrito sin ningún cuidado por el formato:

package main
import "fmt"
func main()  {
    x:=[]int{1,2,3}
  for _,v:=range x {fmt.Println( v )}
}

Compila y se ejecuta. Ahora deja que gofmt lo arregle:

$ gofmt -l .
main.go
$ gofmt -w main.go

gofmt -l lista los archivos con formato incorrecto, y gofmt -w los reescribe en el lugar. Después de reescribirlo, el archivo queda así:

package main

import "fmt"

func main() {
	x := []int{1, 2, 3}
	for _, v := range x {
		fmt.Println(v)
	}
}

Imprime:

1
2
3

Tabs para la indentación, espacios alrededor de := y después de las comas, una línea en blanco entre secciones, y el cuerpo del bucle en sus propias líneas. No hay opciones para cambiar nada de eso.

Esa es la idea. Con un solo formato, nadie en el equipo discute dónde van las llaves, y los diffs de la revisión de código muestran solo cambios reales. Cada proyecto de Go que abres se ve familiar. La mayoría de los editores ejecutan gofmt al guardar, así que casi nunca lo llamas a mano. El comando go fmt ./... hace lo mismo para cada paquete de un módulo.

go vet: un bug que compila

Algunos errores son Go válido pero casi seguro están mal, y go vet los busca. Aquí hay uno común, con los argumentos de Printf en el orden equivocado:

package main

import "fmt"

func main() {
	name := "Ada"
	age := 36
	fmt.Printf("%s is %d years old\n", age, name)
}

%s espera una cadena y %d espera un número entero. Este programa los pasa al revés, y aun así compila y se ejecuta:

$ go run .
%!s(int=36) is %!d(string=Ada) years old
$ go vet
main.go:8:14: fmt.Printf format %s has arg age of wrong type int

El compilador no puede detectarlo, porque Printf acepta cualquier valor después de la cadena de formato. En tiempo de ejecución, fmt tampoco falla. Imprime %!s(int=36), su forma de decir “me pediste una cadena y me diste un int”. Una línea así puede quedarse meses en un archivo de log antes de que alguien la note.

go vet lee la cadena de formato, empareja cada verbo con su argumento, y te dice la línea y el problema. Intercambia los argumentos y el programa queda bien:

package main

import "fmt"

func main() {
	name := "Ada"
	age := 36
	fmt.Printf("%s is %d years old\n", name, age)
}

Imprime:

Ada is 36 years old

go test ejecuta automáticamente una parte de estas revisiones de vet, pero vale la pena que ejecutes go vet tú antes de hacer commit. Todos los programas de esta serie lo pasan.

Las variables y los imports sin usar no compilan

Go trata una variable local sin usar o un import sin usar como un error de compilación, no como una advertencia. Este programa declara una variable que nunca lee e importa un paquete que nunca llama:

package main

import (
	"fmt"
	"os"
)

func main() {
	count := 3
	fmt.Println("hello")
}

La compilación falla con:

./main.go:5:2: "os" imported and not used
./main.go:9:2: declared and not used: count

Las dos líneas señalan la línea y la columna exactas. El programa no compila hasta que quites count y el import de os, o los uses.

Se siente estricto la primera vez que pasa, normalmente a mitad de un cambio. La razón es práctica. Una variable sin usar muchas veces es un bug: calculaste algo y después usaste el nombre equivocado. Un import sin usar hace más lenta cada compilación y oculta de qué depende realmente el archivo. Go decidió que valía la pena detener las dos cosas en la puerta.

Cuando de verdad necesitas ignorar un valor, el identificador en blanco _ lo dice a propósito. Lo viste en for _, v := range x, donde el índice no hace falta.

Leer argumentos de la línea de comandos

Un programa de Go lee las palabras escritas después de su nombre desde os.Args, un slice de cadenas. os.Args[0] es la ruta del propio programa, y los argumentos empiezan en el índice 1.

Poner la lógica en su propia función hace fácil probarla sin escribir nada:

package main

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

func greeting(names []string) string {
	if len(names) == 0 {
		return "Hello, whoever you are"
	}
	return "Hello, " + strings.Join(names, " and ")
}

func main() {
	fmt.Println(len(os.Args))
	fmt.Println(greeting(os.Args[1:]))
	fmt.Println(greeting([]string{"Ada", "Grace"}))
}

Ejecutado sin argumentos, imprime:

1
Hello, whoever you are
Hello, Ada and Grace

len(os.Args) es 1, porque la única entrada es la ruta del programa. os.Args[1:] es entonces un slice vacío, así que greeting toma su primera rama. La última línea llama a greeting directamente con dos nombres, que es como la llamaría una prueba.

Compílalo y pásale argumentos reales, y main tiene a quién saludar:

$ go build
$ ./greet Ada Grace Linus
4
Hello, Ada and Grace and Linus
Hello, Ada and Grace

Ahora os.Args tiene cuatro cadenas: la ruta del programa y tres nombres. La shell separó las palabras por los espacios antes de que Go las viera.

go run también pasa los argumentos. Todo lo que va después del paquete llega a tu programa:

$ go run . Ada Grace
3
Hello, Ada and Grace
Hello, Ada and Grace

Para algo más que un puñado de palabras sueltas, el paquete flag de la biblioteca estándar interpreta opciones como -port 8080 por ti.

Qué recordar

  • Un solo comando go compila, ejecuta, formatea, revisa con vet y maneja dependencias. No hace falta un workspace GOPATH, y un módulo puede vivir en cualquier carpeta.
  • Un programa ejecutable es package main con una func main(). Cualquier otro nombre de paquete crea una biblioteca.
  • go run compila y ejecuta en un paso. go build deja un solo binario que se ejecuta sin Go instalado, y GOOS/GOARCH lo compilan para otros sistemas.
  • go.mod guarda la ruta del módulo y la línea go, y la línea go decide qué funciones del lenguaje permite el compilador.
  • Un nombre que empieza con mayúscula está exportado. Esa es la única regla de visibilidad.
  • gofmt le da a todo el código Go un solo formato, y go vet detecta código válido que casi seguro está mal, como un verbo de Printf que no coincide.
  • Las variables y los imports sin usar son errores de compilación. Usa _ cuando quieras ignorar un valor.

Go junta la compilación, el formato y la primera ronda de revisiones en una sola herramienta, así que todos los proyectos empiezan desde el mismo lugar.

¿Qué tan útil te resultó este post?

¡Haz clic en un corazón para calificar!

Calificación promedio 0 / 5. Total de votos: 0

Todavía no hay votos. Sé el primero en calificar este post.