Paquetes, pruebas y herramientas en Go

Lección 8 de 8, y la que convierte lo aprendido en un proyecto de verdad: cómo se reparte el código en paquetes, cómo se prueba y qué más trae la caja de herramientas. Todo esto viene con Go: no hay que elegir un sistema de construcción ni una biblioteca de pruebas.

Paquetes

Cómo se organiza un proyecto de Go y qué se exportaLa forma de un proyecto de Gogo.model nombre del módulo y sus dependenciasmain.gopackage main: aquí arranca el programatienda/precio.gopackage tienda: la lógica, reutilizabletienda/precio_test.golas pruebas, al lado de lo que pruebanLo que empieza por MAYÚSCULA se ve desde fuera del paquete. Lo que empieza por minúscula, noConIVA() se puede llamar desde main; redondear() solo desde dentro de tiendaNo hay palabras public ni private: la decisión es la primera letra

Un paquete es una carpeta. Los archivos de dentro declaran el mismo package y se ven entre sí sin importar nada.

// tienda/precio.go
package tienda

// ConIVA devuelve el precio con el IVA aplicado.
func ConIVA(base, tipo float64) float64 {
	return redondear(base * (1 + tipo/100))
}

// redondear es privada: en minúscula, solo se ve dentro del paquete.
func redondear(v float64) float64 {
	return float64(int(v*100+0.5)) / 100
}

Para usarlo desde fuera se importa por su ruta completa, que empieza por el nombre del módulo:

// main.go
package main

import (
	"fmt"

	"ejemplo/caja/tienda"
)

func main() {
	fmt.Printf("%.2f\n", tienda.ConIVA(100, 21))
}
salida121.00
Mayúscula o minúscula, y nada más. No hay public ni private: lo que empieza por mayúscula se exporta, lo que empieza por minúscula no. ConIVA se puede llamar desde main; redondear, solo desde dentro de tienda. La misma regla vale para tipos, campos y constantes.

Pruebas

Un archivo que acabe en _test.go, funciones que empiecen por Test y reciban *testing.T. No hace falta instalar nada.

La forma habitual en Go es la prueba de tabla: los casos en un slice y un bucle que los recorre.

// tienda/precio_test.go
package tienda

import "testing"

func TestConIVA(t *testing.T) {
	casos := []struct {
		nombre string
		base   float64
		tipo   float64
		quiero float64
	}{
		{"iva del 21", 100, 21, 121},
		{"iva del 10", 50, 10, 55},
		{"sin iva", 30, 0, 30},
		{"redondeo", 19.99, 21, 24.19},
	}
	for _, c := range casos {
		t.Run(c.nombre, func(t *testing.T) {
			if got := ConIVA(c.base, c.tipo); got != c.quiero {
				t.Errorf("ConIVA(%v, %v) = %v, quería %v", c.base, c.tipo, got, c.quiero)
			}
		})
	}
}
go test ./...
salida?   	ejemplo/caja	[no test files]
ok  	ejemplo/caja/tienda	0.003s

Añadir un caso es añadir una línea. Y el t.Run con el nombre hace que, cuando algo falle, el mensaje diga exactamente cuál:

si falla--- FAIL: TestConIVA (0.00s)
    --- FAIL: TestConIVA/redondeo (0.00s)
        precio_test.go:20: ConIVA(19.99, 21) = 24.19, quería 24.2
FAIL

Dice el caso, el archivo, la línea, lo que salió y lo que esperabas. Por eso el mensaje de t.Errorf conviene escribirlo con ese formato: qué llamaste, qué obtuviste, qué querías.

Y ejecútalas con el detector. go test -race ./... es el hábito que de verdad paga, sobre todo si hay goroutines por medio. Lo vimos en la lección anterior.

go vet: lo que compila pero está mal

nombre := "Ana"
fmt.Printf("hola %d\n", nombre)   // %d para una cadena
go vet ./...
salida./vet.go:7:19: fmt.Printf format %d has arg nombre of wrong type string

Eso compila perfectamente y en ejecución imprime una cosa rara. go vet lo caza antes. Viene con Go y conviene pasarlo junto con las pruebas.

Dependencias

No hay que declarar nada a mano. Se importa y se pide a Go que ordene:

go mod tidy
salidago: finding module for package github.com/google/uuid
go: downloading github.com/google/uuid v1.6.0
go: found github.com/google/uuid in github.com/google/uuid v1.6.0
go.modmodule ejemplo/caja

go 1.25.14

require github.com/google/uuid v1.6.0

go mod tidy funciona en los dos sentidos: añade lo que falta y quita lo que ya no se usa. Al borrar el import y volver a ejecutarlo, la línea require desaparece sola. Aparece además un go.sum con las huellas de cada dependencia, y los dos archivos se guardan en el repositorio.

Compilar para otro sistema

Esto es de las cosas que más sorprenden al llegar de otros lenguajes. Dos variables de entorno y ya:

GOOS=windows GOARCH=amd64 go build -o caja.exe .
GOOS=darwin  GOARCH=arm64 go build -o caja-mac .
salida2.3M caja.exe
2.3M caja-mac

Hecho desde Linux, sin Windows ni un Mac a la vista y sin instalar nada. Es la misma propiedad de la que hablábamos en la guía: el binario se lleva todo dentro.

La caja de herramientas completa

Orden Para qué
go run . Compilar y ejecutar mientras desarrollas
go build -o nombre . Dejar el ejecutable
go test ./... Todas las pruebas del proyecto
go test -race ./... Lo mismo, cazando carreras
go test -v -run TestConIVA Una sola prueba, con detalle
go vet ./... Errores que compilan
gofmt -w . Formatear
go mod tidy Poner las dependencias al día
go doc fmt.Println Documentación sin salir de la terminal

Y hasta aquí

Con estas ocho lecciones tienes lo necesario para leer casi cualquier código de Go y escribir el tuyo: la forma del lenguaje, sus colecciones, sus interfaces implícitas, el trato que da a los errores, qué se copia y qué no, la concurrencia y las herramientas.

Lo que falta no es más sintaxis, es práctica. En la sección de ejemplos de Go hay casos resueltos —leer y escribir XML y JSON, montar un servidor web, conectar con una base de datos— que ya puedes seguir enteros. Y si quieres acompañarlo de las herramientas que rodean a cualquier proyecto, están las guías de Linux, Git y Docker.

Todo el código de esta lección se ejecutó con Go 1.25 antes de publicarla: el proyecto de ejemplo, las pruebas pasando y fallando, el aviso de go vet, la descarga real de la dependencia y los dos binarios cruzados, compilados desde Linux.