En Go, el ciclo de desarrollo es distinto al de lenguajes como Python o JavaScript. No puedes simplemente inyectar cambios en un proceso en ejecución; necesitas compilar, enlazar y ejecutar un nuevo binario cada vez que tocas una línea de código. Esta fricción, aunque mínima en Go comparado con C++, se vuelve un lastre en aplicaciones web o microservicios que tardan segundos en arrancar. Las herramientas de hot reload automatizan este proceso monitoreando el sistema de archivos a través de syscalls del kernel como inotify (en Linux) o fsevents (en macOS).
Existen tres jugadores principales para resolver esto. air es el estándar de facto en la industria por su facilidad de uso y su capacidad para manejar el ciclo de vida de la aplicación de forma limpia. reflex es una herramienta más genérica y potente; no solo vigila archivos, sino que puede ejecutar cualquier comando (como migraciones de base de datos) cuando detecta cambios. Por su parte, modd ofrece un control extremadamente granular mediante archivos de configuración, permitiendo ejecutar acciones distintas según el tipo de archivo que haya cambiado.
En la práctica, usarás estas herramientas durante el desarrollo activo para eliminar el comando manual de go run main.go cada vez que edites un handler. Si no configuras correctamente el alcance del monitoreo, lo más probable es que provoques un bucle infinito de compilación: el compilador genera un binario, el binario se guarda en el disco, el watcher detecta ese nuevo archivo, dispara una nueva compilación, y el ciclo se repite sin fin consumiendo toda la CPU.
// main.go
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// Un servidor simple para observar el reinicio
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hora del servidor: %s\n", time.Now().Format(time.RFC1123))
fmt.Fprintln(w, "Cambia el código para ver el reinicio automático.")
})
port := ":8080"
log.Printf("Servidor iniciando en %s...", port)
// Si el proceso muere (por el hot reload), el servidor se detiene aquí
if err := http.ListenAndServe(port, nil); err != nil {
log.Fatal(err)
}
}
/*
// Para usar esto con `air`, necesitas un archivo .air.toml en la raíz.
// Aquí te dejo la configuración optimizada para evitar el bucle infinito.
[build]
# Comando para compilar el binario
cmd = "go build -o ./tmp/main main.go"
# Comando para ejecutar el binario resultante
bin = "./tmp/main"
# Extensiones que disparan la recarga
include_ext = ["go", "html", "tmpl", "toml"]
# Directorios que NO debemos monitorear para evitar bucles
exclude_dir = ["assets", "tmp", "vendor"]
# Si el binario falla, air lo reiniciará automáticamente
stop_on_error = true
*/
Para que el ejemplo anterior funcione, air necesita saber exactamente qué archivos vigilar y qué comandos ejecutar. En la configuración mostrada arriba, cmd se encarga de la fase de compilación. Es fundamental que el resultado de esa compilación se guarde en un directorio como ./tmp/, que esté explícitamente incluido en exclude_dir. Si intentaras compilar directamente en la raíz del proyecto sin excluir la carpeta de salida, air detectaría el nuevo binario como un cambio de código y lanzaría una nueva compilación infinitamente.
El parámetro bin le dice a air que, una vez que la compilación ha terminado con éxito, debe ejecutar el archivo ./tmp/main. Si tu aplicación es un servidor web, air enviará una señal de terminación al proceso actual antes de arrancar el nuevo, asegurando que el puerto :8080 se libere correctamente y no obtengas el error de address already in use. Finalmente, include_ext actúa como un filtro para que la recarga no se dispare por cambios en archivos de documentación o logs, manteniendo el ciclo de desarrollo enfocado solo en lo que importa.
El error frecuente
El error más sutil y destructivo es la captura de eventos de escritura incompleta. Algunos sistemas de archivos o herramientas de edición (como algunos plugins de VS Code) realizan múltiples operaciones de escritura al guardar un archivo. Si la herramienta de hot reload reacciona al primer evento de escritura antes de que el compilador de Go haya terminado de leer el archivo, la compilación fallará con un error de “syntax error” o “unexpected EOF” porque el archivo estaba “bloqueado” o incompleto.
Si usas air, esto se mitiga con el parámetro delay, que añade una pequeña pausa (por ejemplo, 100ms) después de detectar el cambio antes de disparar la recarga:
# Solución en .air.toml delay = 100 # milisegundos
Sin este pequeño margen, tu flujo de trabajo se verá interrumpido por fallos de compilación aleatorios que desaparecerían si volvieras a ejecutar manualmente.
N° 254