Por qué tu proyecto Go pide CI/CD como un perro pide salir a pasear
Escribiste un microservicio increíble en Go. Vuela, las pruebas pasan, el código es hermoso. Pero cada mañana ejecutas manualmente go build, subes el binario al servidor por SSH y rezas para que nada falle. ¿Te suena familiar? Si es así, tengo noticias para ti: estás gastando el 15% de tu vida en lo que un robot puede hacer.
CI/CD (Integración Continua / Despliegue Continuo) no es solo una palabra de moda en las ofertas de DevOps. Es tu asistente personal que:
- ejecuta pruebas en cada push a Git;
- compila el binario para Linux/amd64, incluso si tienes Mac;
- envía notificaciones a Telegram si la compilación falla;
- despliega automáticamente en el servidor cuando fusionas un PR en main.
En este artículo configuraremos un pipeline CI/CD para Go desde cero. Sin rodeos, sin "instala Jenkins en 20 servidores". Solo GitHub Actions, Docker y un poco de magia. ¡Vamos allá!
Qué necesitaremos
Para la configuración necesitaremos:
- Un repositorio en GitHub (puedes crear uno de prueba ahora mismo);
- Un proyecto Go con módulos (escribiremos un servidor HTTP simple);
- Una cuenta en Docker Hub (o cualquier otro registry);
- 5 minutos de atención y ganas de automatizar la rutina.
Todo el código que usaremos está disponible públicamente. Puedes copiarlo y adaptarlo a tu proyecto. Lo importante es que no olvides reemplazar los tokens y nombres de repositorios por los tuyos.
Paso 1: Escribimos una aplicación Go simple
Para la demostración, tomaremos un servidor HTTP mínimo. Crea el archivo main.go:
package main
import ( "fmt" "net/http")
func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello from Go CI/CD pipeline!") })
http.ListenAndServe(":8080", nil)}Y el archivo main_test.go para verificar el funcionamiento:
package main
import ( "net/http" "net/http/httptest" "testing")
func TestHelloHandler(t *testing.T) { req := httptest.NewRequest("GET", "/", nil) w := httptest.NewRecorder()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello from Go CI/CD pipeline!") })
http.DefaultServeMux.ServeHTTP(w, req)
if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) }}Inicializamos el módulo:
go mod init github.com/tu-usuario/go-cicd-examplego mod tidyVerificamos que todo funciona:
go test ./... -vgo run main.goSi ves ok y el saludo en localhost:8080 — eres increíble. Sigamos adelante.
Paso 2: Creamos un Dockerfile para la aplicación Go
La contenerización es una etapa obligatoria del despliegue moderno. Compilaremos un binario estático (sin CGO) y lo empaquetaremos en una imagen mínima basada en scratch o alpine. Esto nos dará una imagen de aproximadamente 10–15 MB en lugar de 300+ MB.
Crea el archivo Dockerfile en la raíz del proyecto:
FROM golang:1.22-alpine AS builder
WORKDIR /appCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 GOOS=linux go build -o main .
FROM alpine:latestRUN apk --no-cache add ca-certificatesWORKDIR /root/COPY --from=builder /app/main .EXPOSE 8080CMD ["./main"]Nota: usamos compilación multi-etapa. La primera etapa es la compilación en un entorno completo de Go. La segunda es una imagen mínima de Alpine, donde solo se copia el binario. Este es el estándar de la industria para aplicaciones Go.
Compilamos la imagen localmente y verificamos:
docker build -t go-cicd-example .docker run -p 8080:8080 go-cicd-exampleSi todo está bien — pasamos a