# Makefiles en proyectos Go: lo justo y necesario

Go tiene `go build`, `go test`, `go run`. No necesita un sistema de build. Pero en cuanto tu proyecto crece un poco, acabas con comandos que no son triviales de recordar: flags de compilación, variables de entorno para tests, linters, generación de código, deploys. Un Makefile es la forma más simple de documentar y ejecutar esos comandos.

## El Makefile mínimo

![Makefiles en proyectos Go: lo justo y necesario](fig-01.webp)

```makefile
.PHONY: build test lint run clean

build:
	go build -o bin/server ./cmd/server

test:
	go test ./...

lint:
	golangci-lint run

run:
	API_TOKEN=dev go run ./cmd/server

clean:
	rm -rf bin/ tmp/
```

Cinco targets. Cinco comandos. Nada más. Con esto puedes hacer `make build`, `make test`, `make run` y `make lint` sin recordar nada. El `.PHONY` le dice a Make que estos targets no son ficheros sino acciones.

## Variables

Si repites valores, usa variables:

```makefile
BINARY := bin/server
CMD := ./cmd/server
GO := go

.PHONY: build test lint run

build:
	$(GO) build -o $(BINARY) $(CMD)

test:
	$(GO) test ./...

lint:
	golangci-lint run

run: build
	API_TOKEN=dev $(BINARY)
```

Nota que `run` depende de `build`. Cuando haces `make run`, primero compila y luego ejecuta. Make solo recompila si los fuentes han cambiado.

## Información del build

![Makefiles en proyectos Go: lo justo y necesario](fig-02.webp)

Puedes inyectar información en el binario en tiempo de compilación con `-ldflags`:

```makefile
VERSION := $(shell git describe --tags --always --dirty)
COMMIT := $(shell git rev-parse --short HEAD)
BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.buildDate=$(BUILD_DATE)

build:
	go build -ldflags "$(LDFLAGS)" -o bin/server ./cmd/server
```

En tu `main.go`:

```go
var (
    version   = "dev"
    commit    = "none"
    buildDate = "unknown"
)

func main() {
    slog.Info("starting", "version", version, "commit", commit, "build_date", buildDate)
    // ...
}
```

Cada binario compilado sabe exactamente de qué commit viene y cuándo se compiló. Invaluable para debugging en producción.

## Tests con cobertura

```makefile
test:
	go test ./... -count=1

cover:
	go test ./... -coverprofile=coverage.out
	go tool cover -html=coverage.out -o coverage.html
	@echo "Abierto coverage.html en el navegador"

test-race:
	go test ./... -race -count=1
```

`-count=1` desactiva la cache de tests para que siempre se ejecuten. `-race` activa el detector de race conditions, que es lento pero encuentra bugs de concurrencia que de otra forma son invisibles.

## Deploy

![Makefiles en proyectos Go: lo justo y necesario](fig-03.webp)

```makefile
SERVER := root@web.javiervalencia.net
DEPLOY_PATH := /opt/blog

deploy-dry:
	rsync -avzn --delete \
		--exclude='.git' --exclude='*.go' --exclude='go.*' \
		--exclude='cmd' --exclude='internal' --exclude='deploy' \
		--exclude='.env' --exclude='content/views.json' \
		./ $(SERVER):$(DEPLOY_PATH)/

deploy: deploy-dry
	@read -p "¿Continuar con el deploy? (s/n) " ans; \
	if [ "$$ans" = "s" ]; then \
		rsync -avz --delete \
			--exclude='.git' --exclude='*.go' --exclude='go.*' \
			--exclude='cmd' --exclude='internal' --exclude='deploy' \
			--exclude='.env' --exclude='content/views.json' \
			./ $(SERVER):$(DEPLOY_PATH)/; \
		ssh $(SERVER) 'systemctl restart blog'; \
		echo "Deploy completado."; \
	fi

reload:
	curl -s -X POST -H "Authorization: Bearer $${API_TOKEN}" \
		https://javiervalencia.net/api/v1/reload | jq .
```

`make deploy` primero hace un dry-run, luego pide confirmación. Si dices sí, sincroniza y reinicia el servicio. `make reload` recarga el contenido sin reiniciar, útil cuando solo has subido un post nuevo.

## Docker (si lo necesitas)

```makefile
DOCKER_IMAGE := blog
DOCKER_TAG := $(VERSION)

docker-build:
	docker build -t $(DOCKER_IMAGE):$(DOCKER_TAG) .
	docker tag $(DOCKER_IMAGE):$(DOCKER_TAG) $(DOCKER_IMAGE):latest

docker-run:
	docker run --rm -p 8080:8080 --env-file .env $(DOCKER_IMAGE):latest
```

No uso Docker para este blog, pero si tu proyecto lo necesita, tener los comandos en el Makefile evita recordar las flags de `docker build` cada vez.

## Help automático

Un truco útil: generar la ayuda automáticamente a partir de comentarios:

```makefile
.DEFAULT_GOAL := help

help: ## Mostrar esta ayuda
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
		awk 'BEGIN {FS = ":.*?## "}; {printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2}'

build: ## Compilar el binario
	go build -o bin/server ./cmd/server

test: ## Ejecutar tests
	go test ./...

lint: ## Ejecutar linter
	golangci-lint run

run: ## Arrancar en desarrollo
	API_TOKEN=dev go run ./cmd/server

deploy: ## Desplegar a producción
	# ...
```

Ahora `make` sin argumentos muestra la lista de targets con su descripción. Es documentación que se mantiene sola.

## Lo que NO debe hacer un Makefile

He visto Makefiles de 500 líneas con lógica condicional, funciones de shell anidadas, variables computadas con tres niveles de indirección y targets que nadie recuerda para qué sirven. Eso no es un Makefile: es un script de shell disfrazado.

Un buen Makefile para un proyecto Go debería:

- Caber en una pantalla (máximo 50-60 líneas).
- Tener targets que un humano pueda recordar.
- No duplicar lo que Go ya hace bien (`go generate`, `go vet`, `go mod tidy`).
- No tener lógica compleja. Si necesitas un `if` dentro de un target, probablemente necesitas un script de shell separado.

El Makefile es un índice de comandos, no un sistema de build. Go ya tiene un sistema de build excelente. El Makefile solo le pone nombres fáciles de recordar a los comandos que usas todos los días.

## Conclusión

Un Makefile de 30 líneas cubre el 90% de lo que necesitas en un proyecto Go. Build, test, lint, run, deploy. Lo justo. Ni más ni menos. Cada target es un comando que puedes leer y entender en un segundo. Eso es lo que debería ser un Makefile: obvio, predecible y útil.
