El comando go explicado: run, build, test, mod, fmt y vet

Guía práctica de los comandos esenciales de Go: go run, build, test, fmt, vet y mod. Todo lo que necesitas para trabajar en local y CI.

Cover for El comando go explicado: run, build, test, mod, fmt y vet

Cuando empiezas con Go viniendo de Java, Python o Node, una de las primeras cosas que notas es que no necesitas Maven, Gradle, pytest, black ni ESLint. El binario go que se instala con el lenguaje ya incluye compilador, gestor de dependencias, runner de tests, formateador, linter y generador de código. Todo en un solo comando.

Esto no es un accidente. Es una decisión de diseño deliberada. El equipo de Go apostó desde el principio por herramientas estándar integradas en el propio toolchain. Menos configuración, menos debates sobre qué herramienta usar, menos fricción al incorporar a alguien nuevo al proyecto. Si sabes Go, ya sabes cómo compilar, testear y formatear cualquier proyecto Go.

Este artículo cubre los subcomandos que vas a usar a diario: go run, go build, go test, go fmt, go vet, go mod, go generate, go install y go env. Con ejemplos prácticos, flags útiles y cómo combinarlos en un pipeline de CI.


go run: compilar y ejecutar en un paso

go run compila y ejecuta un programa Go sin generar un binario persistente. Es el equivalente a python script.py o node app.js: útil para desarrollo rápido y scripts.

go run main.go

Si tu main importa otros archivos del mismo paquete, puedes pasar varios archivos o usar el patrón de directorio:

go run .
go run ./cmd/server

Pasar argumentos al programa

Los argumentos después del archivo o paquete se pasan directamente al programa:

go run main.go --port 8080 --env production

Flags útiles de go run

# Mostrar los comandos que ejecuta internamente
go run -x main.go

# Compilar sin optimizaciones (útil para debugging con Delve)
go run -gcflags="-N -l" main.go

Cuándo NO usar go run

go run compila cada vez que lo ejecutas. No hay caché implícita del binario resultante. Para un servidor que reinicias 50 veces al día, eso es aceptable. Para un binario que vas a distribuir o desplegar, necesitas go build.

go run es para desarrollo local. Nunca lo uses en producción ni en un Dockerfile final.


go build: crear binarios

go build compila tu código y genera un binario ejecutable. Si no especificas nombre de salida, usa el nombre del módulo o del directorio.

go build -o server ./cmd/server

El binario resultante es estático por defecto (en la mayoría de casos): no necesita runtime, no necesita que Go esté instalado en la máquina destino. Copias el binario y funciona.

Cross-compilation

Una de las mejores características de Go es la compilación cruzada. Puedes generar binarios para cualquier plataforma desde tu máquina:

# Linux AMD64 (lo típico para servidores y contenedores Docker)
GOOS=linux GOARCH=amd64 go build -o server-linux ./cmd/server

# macOS ARM (Apple Silicon)
GOOS=darwin GOARCH=arm64 go build -o server-mac ./cmd/server

# Windows
GOOS=windows GOARCH=amd64 go build -o server.exe ./cmd/server

No necesitas una máquina virtual, no necesitas Docker, no necesitas un CI especial. Solo dos variables de entorno. Esto es algo que en Java o Python directamente no existe de forma nativa.

Flags de build comunes

# Reducir tamaño del binario eliminando información de debug
go build -ldflags="-s -w" -o server ./cmd/server

# Inyectar variables en tiempo de compilación (versión, commit, fecha)
go build -ldflags="-X main.version=1.2.3 -X main.commit=$(git rev-parse HEAD)" -o server ./cmd/server

# Build completamente estático (sin dependencias CGO)
CGO_ENABLED=0 go build -o server ./cmd/server

# Ver qué comandos ejecuta internamente
go build -x -o server ./cmd/server

Inyectar versión en tiempo de compilación

Un patrón muy común es definir variables en tu main.go y rellenarlas con -ldflags:

package main

import "fmt"

var (
    version = "dev"
    commit  = "none"
    date    = "unknown"
)

func main() {
    fmt.Printf("server %s (commit: %s, built: %s)\n", version, commit, date)
}
go build -ldflags="-X main.version=1.2.3 -X main.commit=$(git rev-parse --short HEAD) -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -o server .

Esto te da un binario que sabe exactamente qué versión es y cuándo se compiló. Muy útil para logs y para diagnóstico en producción.


go test: tests, cobertura y benchmarks

go test es el runner de tests integrado. Lee archivos *_test.go, ejecuta funciones que empiezan por Test, Benchmark o Example, y reporta resultados. Sin JUnit, sin pytest, sin configuración.

# Ejecutar tests del paquete actual
go test

# Ejecutar tests de todo el proyecto
go test ./...

# Con output detallado
go test -v ./...

# Ejecutar solo tests que coincidan con un patrón
go test -run TestCreateUser ./internal/user/

Si quieres profundizar en testing, tengo un artículo dedicado a testing en Go con más detalle sobre table-driven tests, mocks y organización.

Cobertura

# Ver porcentaje de cobertura
go test -cover ./...

# Generar archivo de cobertura para inspección detallada
go test -coverprofile=coverage.out ./...

# Ver cobertura línea por línea en el navegador
go tool cover -html=coverage.out

# Ver cobertura por función
go tool cover -func=coverage.out

El output de -cover te da un porcentaje por paquete:

ok  	github.com/user/project/internal/user	0.012s	coverage: 87.3% of statements
ok  	github.com/user/project/internal/auth	0.008s	coverage: 92.1% of statements

Benchmarks

Go tiene soporte nativo para benchmarks. Defines funciones Benchmark* en tus archivos de test:

func BenchmarkParseConfig(b *testing.B) {
    data := loadTestConfig()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        ParseConfig(data)
    }
}

Y los ejecutas con:

# Ejecutar benchmarks
go test -bench=. ./...

# Benchmarks con información de memoria
go test -bench=. -benchmem ./...

# Solo benchmarks, sin tests normales
go test -bench=. -run=^$ ./...

# Ejecutar un benchmark concreto
go test -bench=BenchmarkParseConfig ./internal/config/

Output típico:

BenchmarkParseConfig-8    1000000    1052 ns/op    256 B/op    4 allocs/op

Eso te dice: se ejecutó un millón de veces, cada ejecución tardó 1052 nanosegundos, asignó 256 bytes y realizó 4 allocations. Información concreta para optimizar.

Race detector

Go tiene un detector de race conditions integrado. Actívalo con -race:

go test -race ./...

Esto instrumenta el código para detectar accesos concurrentes a memoria compartida. Es más lento, así que no lo uses en benchmarks, pero debería ser obligatorio en CI.

Flags útiles de go test

# Timeout global (por defecto 10 minutos)
go test -timeout 30s ./...

# Ejecutar tests en paralelo (por defecto GOMAXPROCS)
go test -parallel 4 ./...

# Desactivar caché de tests (fuerza re-ejecución)
go test -count=1 ./...

# Tests cortos (skip de tests pesados marcados con testing.Short())
go test -short ./...

go fmt: el fin de los debates de formato

go fmt formatea tu código según el estilo oficial de Go. No hay configuración, no hay opciones, no hay .editorconfig ni .prettierrc. Un formato. Para todos.

# Formatear un archivo
go fmt main.go

# Formatear todo el proyecto
go fmt ./...

En la práctica, la mayoría de proyectos usan gofmt (el formateador subyacente) o goimports (que además organiza los imports):

# goimports: formatea + organiza imports + añade imports faltantes
goimports -w .

Por qué esto importa

En Java tienes Checkstyle, SpotBugs, Google Java Format, IntelliJ formatter, cada uno con su config. En Python tienes Black, YAPF, autopep8, isort. En JavaScript tienes Prettier, ESLint, Standard. Cada proyecto elige uno, lo configura, y siempre hay alguien que tiene el IDE configurado diferente y mete cambios de formato en los diffs.

En Go ese problema no existe. go fmt es el estándar. Punto. No hay debate sobre tabs vs spaces (tabs), no hay debate sobre dónde va la llave de apertura (misma línea), no hay debate sobre ancho máximo de línea (no hay límite forzado). El formato es parte del lenguaje.

Si tu código no está formateado con go fmt, la comunidad lo considera incorrecto. Así de simple.


go vet: análisis estático integrado

go vet examina tu código buscando errores comunes que el compilador no detecta: argumentos mal pasados a fmt.Printf, variables de loop capturadas en goroutines, condiciones imposibles, código inalcanzable y más.

# Analizar el paquete actual
go vet

# Analizar todo el proyecto
go vet ./...

Qué detecta go vet

Algunos ejemplos de lo que encuentra:

// Printf con argumentos incorrectos
fmt.Printf("user: %d", username) // vet: wrong type for %d

// Copiar un sync.Mutex (error grave de concurrencia)
var mu sync.Mutex
mu2 := mu // vet: assignment copies lock value

// Comparación imposible
if x != x { // vet: suspicious comparison
}

// Unreachable code
func foo() int {
    return 1
    fmt.Println("never") // vet: unreachable code
}

go vet no es un linter completo como golangci-lint, pero cubre los errores más peligrosos y es rápido. En CI debería ejecutarse siempre.

go vet vs golangci-lint

go vet es un subconjunto. golangci-lint agrupa docenas de linters (incluyendo vet) y permite configurar reglas. Para un proyecto serio, usa ambos:

# En CI: primero lo rápido y estándar
go vet ./...

# Después el análisis completo
golangci-lint run

go mod: gestión de dependencias

go mod es el sistema de módulos de Go. Gestiona las dependencias de tu proyecto a través del archivo go.mod. Si vienes de otros lenguajes: go.mod es tu pom.xml, package.json o requirements.txt.

Para una guía completa sobre módulos, mira el artículo de módulos en Go. Aquí cubro los subcomandos que usarás a diario.

go mod init

Inicializa un nuevo módulo:

go mod init github.com/usuario/proyecto

Esto crea un go.mod con el nombre del módulo y la versión de Go:

module github.com/usuario/proyecto

go 1.22

go mod tidy

El comando que más vas a usar. Analiza tus imports, añade las dependencias que faltan al go.mod, y elimina las que ya no se usan:

go mod tidy

Ejecútalo después de añadir o quitar imports. En CI, una técnica común es verificar que el go.mod y go.sum estén actualizados:

go mod tidy
git diff --exit-code go.mod go.sum

Si hay diferencias, alguien se olvidó de ejecutar go mod tidy antes de hacer push.

go mod download

Descarga todas las dependencias al cache local sin compilar nada:

go mod download

Útil en Dockerfiles para aprovechar el cache de capas:

FROM golang:1.22-alpine AS builder
WORKDIR /app

# Primero copiamos solo go.mod y go.sum
COPY go.mod go.sum ./
RUN go mod download

# Después el código (esta capa se invalida más a menudo)
COPY . .
RUN go build -o server ./cmd/server

go mod vendor

Copia todas las dependencias a un directorio vendor/ dentro del proyecto:

go mod vendor

Esto permite builds sin acceso a internet y garantiza reproducibilidad. Para compilar usando el vendor:

go build -mod=vendor -o server ./cmd/server

go mod graph y go mod why

Para depurar dependencias:

# Ver el grafo completo de dependencias
go mod graph

# Saber por qué una dependencia está en tu go.mod
go mod why github.com/lib/pq

go mod why es especialmente útil cuando ves una dependencia en go.sum y no sabes quién la trajo.


go generate: generación de código

go generate ejecuta comandos definidos en comentarios especiales dentro de tu código Go. No es un sistema de build ni un preprocesador: es un mecanismo para ejecutar herramientas que generan código Go.

//go:generate stringer -type=Status
//go:generate mockgen -source=repository.go -destination=mock_repository.go
//go:generate protoc --go_out=. --go-grpc_out=. api.proto

Para ejecutar todos los generadores del proyecto:

go generate ./...

Casos de uso comunes

  • Enums con stringer: Genera métodos String() para tipos basados en iota.
  • Mocks con mockgen: Genera implementaciones mock de interfaces para tests.
  • Protocol Buffers: Genera código Go desde archivos .proto.
  • Embeds SQL: Herramientas como sqlc generan código Go type-safe desde queries SQL.

Buenas prácticas con go generate

  1. Commitea el código generado. Quien clone tu repo no debería necesitar tener protoc, mockgen o stringer instalados para compilar.
  2. Verifica en CI que el código generado está actualizado:
go generate ./...
git diff --exit-code

Si hay diferencias, alguien modificó el código fuente sin regenerar.


go install: instalar herramientas

go install compila e instala un binario en $GOPATH/bin (o $GOBIN si lo tienes definido). Es la forma estándar de instalar herramientas escritas en Go.

# Instalar una herramienta específica con versión
go install golang.org/x/tools/cmd/goimports@latest
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.59.0

# Instalar desde el proyecto actual
go install ./cmd/server

Diferencia entre go install y go build

  • go build genera el binario en el directorio actual (o donde indiques con -o).
  • go install genera el binario en $GOPATH/bin.

Para herramientas CLI que quieres tener disponibles globalmente, usa go install. Para tu proyecto, usa go build.

Gestionar versiones de herramientas en el proyecto

Un patrón habitual es tener un archivo tools.go con un build tag que nunca se compila, solo para que go mod tidy registre las dependencias de herramientas:

//go:build tools

package tools

import (
    _ "github.com/golangci/golangci-lint/cmd/golangci-lint"
    _ "go.uber.org/mock/mockgen"
    _ "golang.org/x/tools/cmd/goimports"
)

Así las versiones quedan fijadas en go.mod y todo el equipo usa las mismas.


go env: entender tu entorno

go env muestra todas las variables de entorno que afectan al toolchain de Go. Es el primer comando que ejecutar cuando algo no funciona como esperas.

# Ver todas las variables
go env

# Ver una variable concreta
go env GOPATH
go env GOROOT
go env GOOS
go env GOARCH

# Ver en formato JSON
go env -json

Variables importantes

VariableQué hace
GOPATHDirectorio base para dependencias y binarios instalados
GOROOTDirectorio de instalación de Go
GOBINDónde se instalan los binarios con go install
GOOSSistema operativo objetivo para compilación
GOARCHArquitectura objetivo para compilación
GOPROXYProxy para descargar módulos (por defecto https://proxy.golang.org)
GONOSUMCHECKMódulos que no se verifican en el sumdb
CGO_ENABLEDSi se permite compilar código C (0 o 1)
GOFLAGSFlags que se aplican a todos los comandos go

Modificar variables de forma persistente

# Cambiar el proxy (útil en entornos corporativos)
go env -w GOPROXY=https://goproxy.io,direct

# Desactivar CGO por defecto
go env -w CGO_ENABLED=0

Estas configuraciones se guardan en $GOPATH/env y persisten entre sesiones.


Combinando comandos en CI/CD

Un pipeline de CI para un proyecto Go típico se ve así:

#!/bin/bash
set -euo pipefail

echo "=== Verificando formato ==="
gofmt -l . | tee /tmp/fmt-check
if [ -s /tmp/fmt-check ]; then
    echo "ERROR: archivos sin formatear"
    exit 1
fi

echo "=== Análisis estático ==="
go vet ./...

echo "=== Verificando go.mod ==="
go mod tidy
git diff --exit-code go.mod go.sum

echo "=== Verificando código generado ==="
go generate ./...
git diff --exit-code

echo "=== Tests con race detector ==="
go test -race -cover -coverprofile=coverage.out ./...

echo "=== Cobertura ==="
go tool cover -func=coverage.out

echo "=== Build ==="
CGO_ENABLED=0 go build -ldflags="-s -w" -o /tmp/app ./cmd/server

Ejemplo con GitHub Actions

name: CI
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version: '1.22'

      - name: Verify formatting
        run: |
          if [ -n "$(gofmt -l .)" ]; then
            echo "Code not formatted:"
            gofmt -l .
            exit 1
          fi

      - name: Vet
        run: go vet ./...

      - name: Test
        run: go test -race -coverprofile=coverage.out ./...

      - name: Build
        run: CGO_ENABLED=0 go build -ldflags="-s -w" -o app ./cmd/server

Fíjate en el orden: formato primero (es instantáneo y falla rápido), después vet, después tests, después build. No hay Maven que tarda 30 segundos en arrancar, no hay npm install de 200 MB. Un proyecto Go mediano pasa CI en menos de un minuto.


Tabla comparativa: Go tooling vs otros ecosistemas

TareaGoJavaPythonNode.js
Compilargo buildmvn package / gradle buildN/A (interpretado)N/A (interpretado)
Ejecutargo runjava -jar / mvn exec:javapython script.pynode app.js
Testsgo testJUnit + Maven/Gradlepytest / unittestJest / Vitest
Formateargo fmtgoogle-java-format / SpotlessBlack / YAPFPrettier
Lintergo vet + golangci-lintCheckstyle / SpotBugsRuff / Flake8 / PylintESLint
Dependenciasgo modMaven / Gradlepip / Poetry / uvnpm / pnpm / yarn
Coberturago test -coverJaCoCocoverage.py / pytest-covc8 / istanbul
Benchmarksgo test -benchJMHpytest-benchmarkBenchmark.js
Cross-compileGOOS=x GOARCH=y go buildGraalVM native-image (limitado)No nativoNo nativo (pkg)
Generar códigogo generateAnnotation processorsNo estándarNo estándar

La diferencia principal: en Go todo es un único binario con una interfaz consistente. En otros ecosistemas necesitas instalar, configurar y mantener herramientas separadas para cada tarea.


Lo que hace diferente al tooling de Go

El go command no es solo un compilador. Es un statement sobre cómo debería funcionar el tooling de un lenguaje. Formato sin configuración. Tests sin framework externo. Compilación cruzada con dos variables de entorno. Gestión de dependencias sin archivo de lock separado (el go.sum se autogenera).

Hay cosas que no cubre: linting avanzado (necesitas golangci-lint), hot reload (necesitas air o similar), y gestión de releases (necesitas goreleaser o scripts). Pero la base que ofrece el toolchain estándar es más completa que la de cualquier otro lenguaje que haya usado.

Si estás empezando con Go, dedica una hora a explorar go help y los subcomandos que hemos visto. Esa hora te va a ahorrar días de configuración de herramientas que en otros ecosistemas das por sentado que necesitas. Si quieres una guía para dar los primeros pasos, empieza por cómo empezar con Go.

OshyTech

Ingeniería backend y de datos orientada a sistemas escalables, automatización e IA.

Navegación

Copyright 2026 OshyTech. Todos los derechos reservados