CommunityProgramación y desarrollogithub.com

tenequm/go-dev

Configuración opinada de Go con golangci-lint v2, gofumpt, gotestsum, golang-migrate y just. Úsalo al iniciar un proyecto Go, configurar lint, formato, tests, cobertura o CI, escribir un Justfile, cablear migraciones o abandonar un flujo con Makefile.

¿Qué es go-dev?

Esta skill proporciona una configuración moderna y opinada para el desarrollo en Go, consolidando herramientas como golangci-lint v2, gofumpt, gotestsum, golang-migrate, just y lefthook. Su objetivo es eliminar la superposición de herramientas, reemplazando flujos fragmentados de gofmt/govet/staticcheck con un stack unificado. Es útil para iniciar nuevos proyectos Go, agregar infraestructura de linting/formateo/testing, configurar CI/CD, crear Justfiles en lugar de Makefiles, y añadir migraciones de bases de datos. La skill proporciona plantillas completas de configuración, incluyendo .golangci.yml, Justfile, lefthook.yml y una guía de inicio rápido, con recomendaciones de versiones mínimas y advertencias sobre footguns comunes como la deriva de dependencias y el comportamiento del toolchain de Go. Soporta las plataformas Claude Code, Cursor y Codex, y se integra con los comandos estándar del ecosistema Go.

Compatible con✓Claude Code✓Codex CLI✓Cursor
npx skills add https://github.com/tenequm/skills/tree/HEAD/skills/go-dev

Installed? Explore more Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Stack de Desarrollo Go

Configuración opinada y moderna para desarrollo Go. Una herramienta por preocupación, cero superposición.

Cuándo Usarlo

  • Iniciar un nuevo proyecto Go desde cero
  • Añadir infraestructura de linting, formateo o testing
  • Configurar CI/CD para un servicio o librería Go
  • Crear un Justfile para reemplazar un Makefile
  • Añadir herramientas de migración de base de datos
  • Migrar desde invocaciones dispersas de gofmt/govet/staticcheck a una configuración unificada

No usar para forks que se fusionan regularmente con upstream: reemplazar su Makefile y reformatear el árbol con gofumpt genera conflictos en cada fusión y entierra cambios reales en un diff de reformateo. Mantén el gofmt y las herramientas de compilación de upstream allí, y solo añade comprobaciones.

El Stack

HerramientaVersiónRolReemplaza
Go1.27+Lenguaje, toolchain, go mod, go fix-
golangci-lintv2.14+Meta-linter (100+ linters + formateadores + comando fmt)gofmt, govet, staticcheck, errcheck ejecutados por separado
gofumptv0.12+Formateador estricto (superconjunto de gofmt, 19 reglas por defecto)gofmt
gotestsumv1.13+Ejecutor de tests con salida legible, modo watch, XML JUnitgo test directo
just1.58+Ejecutor de tareasMakefile
golang-migratev4.20+Migraciones de BD (CLI + librería + embed.FS)Scripts SQL manuales
lefthookv2.1+Hooks de Git (binario único, paralelo)pre-commit (Python)

Los pisos de versión son importantes. golangci-lint "soporta versiones de Go menores o iguales a la versión de Go utilizada para compilarlo" - una versión fijada más antigua que tu toolchain de Go falla por completo. El soporte para Go 1.27 llegó en golangci-lint v2.13.0, por lo que v2.13 es el piso para un proyecto Go 1.27; esta skill fija v2.14.0, la primera versión que incluye gofumpt v0.12.0. Otros dos pisos se movieron recientemente: gofumpt v0.12.0 "está basado en el gofmt de Go 1.27 y requiere Go 1.26 o posterior", y la ruta go install de lefthook ahora requiere Go 1.26+.

Inicio Rápido: Nuevo Proyecto

# 1. Crear módulo
mkdir myapp && cd myapp
go mod init github.com/yourorg/myapp

# 2. Crear directorios
mkdir -p cmd/myapp internal migrations

# 3. Instalar golangci-lint como binario, no como herramienta de módulo (ver nota abajo)
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.14.0

# 4. Gestionar el resto en go.mod (directiva tool de Go 1.24+). Fijar versiones - nunca @latest,
#    que recompila la herramienta en cada ejecución de CI y deriva entre máquinas.
go get -tool mvdan.cc/[email protected]
go get -tool gotest.tools/[email protected]

# golang-migrate necesita un build tag, así que instálalo directamente
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/[email protected]

# 5. Crear archivos de configuración (plantillas abajo)
# 6. Ejecutar: just check

No instales golangci-lint mediante el patrón de herramientas. Upstream es explícito: "El uso de go install/go get, el "patrón de herramientas" y las directivas tool/comando no están garantizados para funcionar. Recomendamos la instalación binaria." La razón por la que esto importa en un repositorio compartido es la fuga de dependencias - "las dependencias de una herramienta pueden modificar las dependencias de otra herramienta o de tu proyecto". Si es necesario tenerlo en go.mod, aíslalo detrás de su propio -modfile - consulta la Referencia de golangci-lint.

go get -tool rastrea; go tool ejecuta. La directiva tool registra la dependencia en go.mod pero no pone nada en tu PATH. O invócala a través del toolchain - go tool gofumpt -l ., go tool gotestsum --format testname - o go install tool una vez para poblar $(go env GOPATH)/bin. El Justfile a continuación llama a los binarios directamente, por lo que asume la ruta go install tool (o una instalación del sistema mediante Homebrew - pero "Homebrew puede usar una versión inesperada de Go para construir el binario" para golangci-lint, y no puede fijar una versión). Ten en cuenta que go tool resuelve contra el módulo en el directorio actual - "se pueden definir herramientas adicionales en el go.mod del módulo actual" - por lo que en un monorepo falla con go: no such tool "..." a menos que la receta establezca [working-directory(...)].

Dos comportamientos del comando go que vale la pena conocer antes del primer commit:

  • go mod init bajo un toolchain 1.N escribe go 1.(N-1).0, no 1.N - "Ejecutar go mod init usando un toolchain de versión 1.N.X creará un archivo go.mod que especifica la versión de Go go 1.(N-1).0." Aumenta la directiva deliberadamente si quieres características del lenguaje 1.N.
  • Establece una línea toolchain go1.27.1 en go.mod, pero ten en cuenta que es un piso, no una fijación: bajo el GOTOOLCHAIN=auto por defecto, el comando go cambia solo "si ... <tname> es más nuevo que el toolchain de Go por defecto", por lo que un go local más nuevo gana. Para un toolchain exacto, establece GOTOOLCHAIN=go1.27.1 (CI o go env -w); GODEBUG=toolchaintrace=1 go version muestra cuál fue seleccionado. Mantén la línea en el parche actual, no en el .0: selecciona el go contra el cual govulncheck escanea los avisos de stdlib en cualquier máquina cuyo propio go sea más antiguo, por lo que un parche obsoleto activa la luz roja de CI por sí solo - ver Footguns abajo.

.golangci.yml

version: "2"

run:
  timeout: 5m
  build-tags:
    - integration   # de lo contrario, los archivos detrás del tag integration del Justfile nunca se lintean

linters:
  default: standard
  enable:
    - bodyclose
    - copyloopvar
    - dupl
    - durationcheck
    - err113
    - errname
    - errorlint
    - exhaustive
    - exptostd
    - fatcontext
    - goconst
    - gocritic
    - gosec
    - intrange
    - misspell
    - modernize
    - musttag
    - nakedret
    - nestif
    - nilerr
    - noctx
    - nolintlint
    - nonamedreturns
    - perfsprint
    - prealloc
    - revive
    - sqlclosecheck
    - testifylint
    - thelper
    - unconvert
    - unparam
    - usestdlibvars
    - usetesting
    - wastedassign
    - whitespace
    - wrapcheck
  settings:
    govet:
      enable:
        - shadow
    gocritic:
      enabled-checks:
        - nestingReduce
    revive:
      enable-all-rules: true
      rules:
        # enable-all-rules activa `unhandled-error`, que señala `fmt.Println` en main.
        # Bajo enable-all-rules, los `arguments` de una regla se ignoran (la regla se registra
        # dos veces), por lo que una lista blanca no funciona aquí - solo `disabled` tiene efecto.
        - name: unhandled-error
          disabled: true
    errcheck:
      check-type-assertions: true
  exclusions:
    generated: strict
    presets:
      - comments
      - std-error-handling
      - common-false-positives
    rules:
      - path: _test\.go
        linters:
          - errcheck
          - dupl
          - gosec
          - wrapcheck

formatters:
  enable:
    - gofumpt
    - goimports
  settings:
    gofumpt:
      # Seleccionar reglas individualmente. `extra-rules: true` está obsoleto y además
      # activa `balance_calls`, que gofumpt mismo degradó como controvertida.
      extra:
        group-params: true
        clothe-returns: true
        balance-calls: false
  exclusions:
    generated: strict
    paths:
      - vendor/

output:
  formats:
    text:
      path: stdout
      print-linter-name: true
      colors: true
  sort-order:
    - linter
    - file
  show-stats: true

Justfile

set shell := ["bash", "-euo", "pipefail", "-c"]
set dotenv-load

binary := "myapp"

[private]
default:
    @just --list --unsorted

# ── Calidad del Código ──────────────────────────────────────────

# Formatear todo el código Go
[group('quality')]
fmt:
    golangci-lint fmt ./...

# Comprobar formato sin modificar (seguro para CI)
[group('quality')]
fmt-check:
    golangci-lint fmt --diff ./...

# Ejecutar linter
[group('quality')]
lint:
    golangci-lint run ./...

# Ejecutar linter con corrección automática
[group('quality')]
lint-fix:
    golangci-lint run --fix ./...

# Ejecutar comprobación de vulnerabilidades
[group('quality')]
vuln:
    govulncheck ./...

# ── Testing ───────────────────────────────────────────────

# Ejecutar todos los tests con detección de race conditions
[group('test')]
test *args="./...":
    gotestsum --format testname -- -race {{ args }}

# Ejecutar tests con cobertura
[group('test')]
test-cov:
    gotestsum --format testname -- -race -coverprofile=coverage.out -covermode=atomic ./...
    go tool cover -func=coverage.out

# Abrir informe de cobertura en el navegador
[group('test')]
coverage: test-cov
    go tool cover -html=coverage.out

# Ejecutar tests de integración
[group('test')]
test-integration:
    gotestsum --format testname -- -race -tags=integration ./...

# Ver tests durante el desarrollo
[group('test')]
test-watch:
    gotestsum --watch --watch-clear --format testname

# Ejecutar benchmarks
[group('test')]
bench:
    go test -bench=. -benchmem ./...

# ── Compilación ─────────────────────────────────────────────────

# Compilar el binario
[group('build')]
build:
    go build -o {{ binary }} ./cmd/{{ binary }}

# Compilar binario optimizado para release
[group('build')]
build-release:
    CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o {{ binary }} ./cmd/{{ binary }}

# ── Dependencias ──────────────────────────────────────────

# Limpiar y verificar módulos
[group('deps')]
tidy:
    go mod tidy
    go mod verify

# Fallar si go.mod/go.sum no están limpios, sin modificarlos (seguro para CI)
[group('deps')]
tidy-check:
    go mod tidy -diff

# Ejecutar generadores de código
[group('deps')]
generate:
    go generate ./...

# ── Base de Datos ──────────────────────────────────────────────

# Aplicar todas las migraciones pendientes
[group('db')]
migrate-up:
    migrate -path migrations -database "$DATABASE_URL" up

# Revertir la última migración
[group('db')]
migrate-down:
    migrate -path migrations -database "$DATABASE_URL" down 1

# Crear una nueva migración
[group('db')]
migrate-create name:
    migrate create -ext sql -dir migrations -seq {{ name }}

# ── CI ────────────────────────────────────────────────────

# Puerta de CI completa (comprobación de formato + lint + test)
[group('ci')]
check: fmt-check lint test
    @echo "Todas las comprobaciones pasaron"

# Limpiar artefactos de compilación
[group('ci')]
clean:
    go clean
    rm -f {{ binary }} coverage.out

check omite deliberadamente vuln: govulncheck llama a la red, y una actualización de la base de datos de vulnerabilidades puede poner la puerta en rojo por un cambio que no tocó nada. Ejecútalo como un trabajo de CI separado. golangci-lint run también ejecuta los formateadores habilitados e informa sus problemas, por lo que fmt-check duplica esa comprobación - mantenlo de todos modos por el diff legible.

Configuración de Lefthook

Lefthook se prefiere sobre pre-commit para proyectos Go - es un único binario Go, ejecuta hooks en paralelo y no necesita Python.

go install github.com/evilmartians/lefthook/[email protected]   # necesita Go 1.26+
lefthook install
# lefthook.yml
assert_lefthook_installed: true   # fallar en voz alta en lugar de saltar cada regla

pre-commit:
  piped: true   # fail fast - detenerse en el primer trabajo que falle
  commands:
    fmt:
      glob: "*.go"
      run: golangci-lint fmt {staged_files}
      stage_fixed: true
    lint:
      glob: "*.go"
      # Nunca pasar una lista de archivos simple a `golangci-lint run`: una lista que abarca dos
      # directorios es rechazada directamente, y un archivo de un paquete de múltiples archivos
      # reporta errores fantasma de typecheck `undefined:`. Lintear los paquetes en su lugar.
      run: printf '%s\n' {staged_files} | xargs -n1 dirname | sort -u | xargs golangci-lint run --fix
      stage_fixed: true
    mod-tidy:
      glob: "*.{go,mod,sum}"
      run: go mod tidy

pre-push:
  commands:
    test:
      run: go test -race ./...

piped: true es fail-fast, no ordenación - lefthook "ejecuta comandos y scripts secuencialmente por defecto", y piped añade "Dejar de ejecutar comandos y scripts si uno de ellos falla." No se puede combinar con parallel: true.

jobs: (añadido en lefthook 1.10.0) es la primitiva más nueva junto con la división commands:/scripts: - "Jobs proporcionan una forma flexible de definir tareas, soportando tanto comandos como scripts. Los trabajos se pueden agrupar para un control de flujo avanzado." commands: no está obsoleto y permanece completamente documentado; recurre a jobs: cuando necesites agrupación, control de flujo anidado o una mezcla de comandos en línea y scripts en un solo hook.

Cuatro más que vale la pena cablear:

  • assert_lefthook_installed: true, arriba, es el antídoto contra el footgun de inactividad abajo: "fallar (con código de salida 1) si el ejecutable lefthook no se encuentra en $PATH".
  • lefthook validate en CI detecta un lefthook.yml mal formado antes de que deshabilite silenciosamente los hooks; lefthook dump imprime la configuración efectiva fusionada cuando un hook no se comporta como está escrito.
  • Un lefthook-local.yml ignorado por git permite que un desarrollador añada o salte trabajos sin imponérselo a los compañeros de equipo - "Esto es útil cuando quieres usar lefthook localmente sin imponérselo a tus compañeros de equipo."
  • En un monorepo, asigna a cada trabajo una root: que apunte a su directorio de módulo; sin ella, go mod tidy y go tool se ejecutan contra la raíz del repositorio y fallan.
  • skip: [merge, rebase] en los trabajos mod-tidy y lint los mantiene fuera de los commits realizados durante una fusión o rebase en curso, para que resolver conflictos no reetapifique una reescritura tidy o ediciones --fix.

El conjunto de pruebas pre-push repite lo que CI ya ejecuta. Una vez que toma minutos, elimínalo o delimítalo a paquetes modificados - de lo contrario, los desarrolladores aprenden a hacer push con LEFTHOOK=0. Los tests ejecutados desde un hook también heredan el entorno de hook de git (GIT_DIR, GIT_INDEX_FILE, GIT_WORK_TREE), por lo que un helper de test que ejecuta git init o git commit en t.TempDir() actúa sobre el repositorio real - limpia esas variables en cmd.Env.

Un git merge sin conflictos nunca ejecuta pre-commit: git invoca pre-merge-commit en su lugar, y ejecuta pre-commit solo cuando finalizas una fusión conflictiva con git commit. Refleja las comprobaciones bajo una clave pre-merge-commit:, o las fusiones llegarán sin lint.

Los comandos de hook resuelven herramientas del PATH del invocador, por lo que un binario faltante falla el commit con un simple exit 127. Ejecutarlos como go tool <name> (directiva tool) hace que el hook sea tan reproducible como la compilación.

Beta, pero vale la pena saberlo: ai: declara hooks de agentes LLM en el mismo archivo - "Durante lefthook install, lefthook genera el archivo de configuración específico del proveedor para que el agente llame a lefthook run <hook> cuando..."

Footguns / Dice / Pisos / Y tus CI Pasa Verde Pero...

gofumpt reescribe el árbol. golangci-lint fmt aplica gofumpt y goimports a cada archivo Go que toca. En un proyecto estable, golangci-lint fmt ./... cambia la firma de cada repositorio que migra. Espera en el PR inicial, o aplícalo con --diff en CI hasta que el equipo esté listo.

golang-migrate versiona las migraciones por timestamp, no por secuencia. El comando migrate create escribió archivos {timestamp}_{name}.{up,down}.sql hasta v4.15. -seq restaura la numeración {nnnnnn}_{name}.{up,down}.sql que el equipo probablemente espera. Los archivos legacy {timestamp}_ existentes funcionan junto con los nuevos -seq; no es necesaria una migración de migraciones.

golangci-lint no es un gestor de formateadores. Activarlo en .golangci.yml solo comprueba que los archivos ya formateados coinciden con las reglas; no los reformatea por sí mismo. golangci-lint fmt sí aplica formateo, pero debes ejecutarlo explícitamente. golangci-lint run --fix ejecuta formateo más arreglos de linters.

lefthook install no toca lefthook.yml. Crea lefthook.yml antes de instalar; de lo contrario, lefthook install crea hooks vacíos y sale 0 - los commits entran sin lint y un lefthook run pre-commit los ignora silenciosamente. Un miembro del equipo que hace checkout de un repositorio existente obtiene archivos de hook que ya saben buscar lefthook.yml, así que este problema solo afecta al desarrollador que inicializa los hooks. La guardia assert_lefthook_installed: true lo detecta en el primer hook que falla.

Homebrew y golangci-lint no se llevan bien. brew install golangci-lint instala la última versión, que puede ser más antigua que la que fijó tu configuración. La instalación binaria evita que CI y desarrolladores usen versiones diferentes.

go get -tool fija un mínimo, no un máximo. Si alguna vez ejecutas go get -u sin filtro, la directiva tool se actualiza a la versión más reciente disponible. Fíjala explícitamente en un PR separado o mediante un bot de dependencias.

golang-migrate traza su propia versión de Go. Su binario está compilado con una versión de Go que puede ser más antigua que la tuya - migrate --version muestra la versión de Go con la que fue compilado. Si tu CI ejecuta go install de golang-migrate, obtienes la versión de Go de CI, no la de tu proyecto.

Los archivos de hook de lefthook no se comparten entre IDEs. Los IDEs tienden a tener su propio PATH - Cursor, VS Code, GoLand - y lefthook install escribe hooks que apuntan al PATH del desarrollador en el momento de la instalación. En una máquina con varias instalaciones de shell, un hook puede resolverse contra un PATH diferente que el desarrollador usa en la terminal. Un hook pre-commit que necesita golangci-lint en el PATH falla silenciosamente si el binario está instalado en el PATH del shell pero no en el PATH del hook. Si un hook falla intermitentemente, comprueba que el binario esté en el PATH del hook: lefthook run pre-commit desde la terminal vs desde el IDE puede comportarse de manera diferente porque el IDE no carga los dotfiles del shell.

Sobrescritura del Go toolchain mediante go.mod. La línea toolchain go1.27.1 es un piso, no una fijación - si un desarrollador tiene go1.28.0 instalado, GOTOOLCHAIN=auto (el predeterminado) selecciona go1.28.0. Esto significa que go mod tidy -diff puede aprobar cambios que fallan bajo el toolchain mínimo del proyecto. Para una fijación exacta, establece GOTOOLCHAIN=go1.27.1 en CI y recomienda que los desarrolladores establezcan go env -w GOTOOLCHAIN=go1.27.1.

go mod tidy -go=1.27 vs go 1.27 en go.mod. La directiva go en go.mod controla qué características del lenguaje están disponibles, pero go mod tidy -go=1.27 establece un piso de toolchain diferente para las dependencias. No uses -go= a menos que estés reduciendo intencionalmente el piso del módulo; go mod tidy sin banderas escribe la versión del toolchain que lo ejecuta.

migrate create sin -seq en un proyecto existente crea archivos {timestamp}_... que no se ordenan junto a los archivos {nnnn}_... existentes. Al generar una migración de una en una, los timestamps siempre aumentan, por lo que la ordenación es correcta hasta que algún script necesite encontrar la migración más reciente por nombre de archivo. Los timestamps no son comparables en cadena para números de 9 dígitos sobre 2000 - el orden de clasificación ASCII se alinea con el orden cronológico para años de 4 dígitos y marcas de tiempo de dos dígitos. Este footgun es cosmético hasta que necesites ejecutar migraciones ascendentes y descendentes en orden lexicográfico por nombre de archivo (sort contra find -name).

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

Skills relacionados