// 05 · de script a proyecto profesional

Proyecto, testing y calidad

Módulos, el layout estándar, tests de tabla, linters y CI: todo lo que convierte "código que funciona" en un repo que da gusto abrir.

// go.mod es tu pom.xml en 3 líneas

Módulos y estructura de proyecto

Un módulo es un árbol de paquetes con un archivo go.mod en la raíz que declara su nombre (una ruta tipo URL) y sus dependencias con . go.sum guarda los hashes criptográficos de cada dependencia — lockfile e integridad en uno.

go.mod — esto es TODO tu pom.xml
module github.com/daniel/mi-servicio

go 1.24

require (
	github.com/go-chi/chi/v5 v5.1.0
	github.com/jackc/pgx/v5 v5.7.1
)

El layout estándar de un servicio real

Go no impone estructura, pero la comunidad converge en este layout. Explóralo — haz clic en los archivos para ver qué va en cada uno:

i

internal/ es visibilidad forzada por el compiladorNada fuera de tu módulo puede importar lo que esté bajo internal/. Es el package-private de Java, pero a nivel de árbol de carpetas y sin posibilidad de saltárselo. Por defecto, mete todo ahí; expón solo lo que otros módulos deban consumir.

Reglas de imports, dependencias circulares y workspaces
  • Se importa por ruta completa: import "github.com/daniel/mi-servicio/internal/store".
  • Un import no usado es error de compilación, no warning. El editor los limpia solo (goimports).
  • Las dependencias circulares entre paquetes no compilan. Donde Spring te dejaba crear ciclos de beans y resolverlos con proxies, Go te obliga a arreglar el diseño. Frustrante el día uno, salvavidas el mes seis.
  • go mod tidy = ordenar dependencias: añade las que faltan, borra las que sobran. Córrelo antes de cada commit.
  • go work (workspaces, 1.18+): editar varios módulos a la vez sin publicar versiones — tu proyecto multi-módulo de Maven, sin ceremonias.

// sin JUnit, sin Mockito, sin nada que instalar

Testing integrado

El testing viene en el lenguaje: un archivo foo_test.go junto al código, funciones TestXxx(t *testing.T), y go test ./... corre todo. Sin runners, sin anotaciones, sin dependencias.

El idioma dominante es el test de tabla: defines casos como datos y un solo bucle los ejecuta. Cubre lo que en JUnit haces con @ParameterizedTest, pero sin framework.

descuento_test.go — test de tabla
package precios

import "testing"

func TestAplicarDescuento(t *testing.T) {
	casos := []struct {
		nombre   string
		precio   float64
		cupon    string
		esperado float64
	}{
		{"sin cupón", 100, "", 100},
		{"cupón 10%", 100, "DIEZ", 90},
		{"cupón inválido no descuenta", 100, "FALSO", 100},
		{"precio cero", 0, "DIEZ", 0},
	}

	for _, c := range casos {
		t.Run(c.nombre, func(t *testing.T) { // subtests con nombre, como @Nested
			got := AplicarDescuento(c.precio, c.cupon)
			if got != c.esperado {
				t.Errorf("AplicarDescuento(%v, %q) = %v; quería %v",
					c.precio, c.cupon, got, c.esperado)
			}
		})
	}
}

¿Y los mocks?

Aquí brillan las interfaces implícitas: como el consumidor define interfaces pequeñas, mockear es escribir un struct de 5 líneas — sin Mockito, sin proxies, sin reflexión.

handler_test.go — mock manual + httptest
// La interfaz que usa el handler (definida por el consumidor):
type BuscadorTareas interface {
	PorID(id int) (Tarea, error)
}

// El "mock": un struct que la cumple. Eso es todo.
type buscadorFalso struct {
	tarea Tarea
	err   error
}

func (b buscadorFalso) PorID(int) (Tarea, error) { return b.tarea, b.err }

func TestGetTarea(t *testing.T) {
	h := NuevoHandler(buscadorFalso{tarea: Tarea{ID: 1, Titulo: "estudiar Go"}})

	// httptest: servidor y request falsos de la stdlib (tu MockMvc)
	req := httptest.NewRequest("GET", "/tareas/1", nil)
	rec := httptest.NewRecorder()

	h.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d; quería 200", rec.Code)
	}
}

Benchmarks y fuzzing, también de serie

parse_test.go — benchmark + fuzz
// Benchmark: mide ns/op y allocs/op. Go 1.24 introdujo b.Loop().
func BenchmarkParsear(b *testing.B) {
	for b.Loop() {
		Parsear(entradaEjemplo)
	}
}
// $ go test -bench=. -benchmem
// BenchmarkParsear-8   1204818   983 ns/op   256 B/op   3 allocs/op

// Fuzzing: el motor genera entradas mutadas buscando panics y fallos.
// Así se encuentran los bugs de parseo que ningún humano imagina.
func FuzzParsear(f *testing.F) {
	f.Add("entrada semilla")            // corpus inicial
	f.Fuzz(func(t *testing.T, s string) {
		_, err := Parsear(s)
		if err == nil {
			return // entrada válida: ok
		}
		// si err != nil está bien; lo que NUNCA debe pasar es un panic
	})
}
// $ go test -fuzz=FuzzParsear -fuzztime=30s
✓

Si extrañas los assertsLa lib stretchr/testify te da assert.Equal(t, esperado, got) y require.NoError(t, err) — es la dependencia de test más usada del ecosistema y totalmente aceptada. Para mocks generados: mockery o gomock.

// nuevo · el linter que agrega 40 revisores a tu equipo

Linters y calidad de código

La base viene con el toolchain: gofmt (formato), go vet (errores probables). El estándar de la industria encima de eso es golangci-lint: un agregador que corre decenas de linters en paralelo con caché.

.golangci.yml — config inicial sensata
# golangci-lint run  ← corre esto en local y en CI
linters:
  enable:
    - govet          # el go vet de siempre
    - staticcheck    # el linter más querido: cientos de chequeos serios
    - errcheck       # ¿ignoraste un error sin querer? te delata
    - unused         # código muerto
    - ineffassign    # asignaciones que nadie lee
    - gocritic       # estilo y rendimiento
    - misspell       # typos en comentarios y strings

linters-settings:
  errcheck:
    check-type-assertions: true   # v, ok := x.(T) — exige el ok
i

Instalaciónwinget install GolangCI.golangci-lint (o go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest). En VS Code: settings → Go: Lint Tool → golangci-lint. Es tu SonarQube + Checkstyle + SpotBugs, corriendo en segundos y sin servidor.

// nuevo · CI en 20 líneas

CI con GitHub Actions

Como Go compila y testea tan rápido, un pipeline completo (vet + race + cobertura + build) tarda 1–2 minutos. Este workflow es todo lo que necesita un proyecto para verse profesional en tu GitHub:

.github/workflows/ci.yml
name: ci
on:
  push:
    branches: [main]
  pull_request:

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

      - uses: actions/setup-go@v5
        with:
          go-version: "1.24"
          cache: true            # cachea módulos y build: corre aún más rápido

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

      - name: Tests con detector de races
        run: go test -race -coverprofile=cover.out ./...

      - name: Build
        run: go build ./...

      - name: Lint
        uses: golangci/golangci-lint-action@v6
✓

Comparado con tu CI de JavaSin instalar Maven, sin descargar medio Internet de dependencias, sin 8 minutos de tests de contexto de Spring. El badge verde en un repo Go con -race y lint activados dice mucho de ti en una entrevista.

// nuevo · diseño idiomático

Patrones de diseño a la Go

Los patrones GoF que memorizaste siguen existiendo, pero muchos se disuelven: sin herencia ni contenedores DI, la mayoría se reduce a funciones y structs pequeños. Estos son los que sí verás a diario:

Functional options: el "builder" de Go

options.go
type Servidor struct {
	puerto  int
	timeout time.Duration
	log     *slog.Logger
}

// Una Opción es una función que modifica el servidor:
type Opcion func(*Servidor)

func ConPuerto(p int) Opcion {
	return func(s *Servidor) { s.puerto = p }
}

func ConTimeout(d time.Duration) Opcion {
	return func(s *Servidor) { s.timeout = d }
}

// El constructor pone defaults sensatos y aplica las opciones recibidas:
func NewServidor(opts ...Opcion) *Servidor {
	s := &Servidor{
		puerto:  8080,
		timeout: 5 * time.Second,
		log:     slog.Default(),
	}
	for _, aplicar := range opts {
		aplicar(s)
	}
	return s
}

// Uso: legible, extensible sin romper llamadas existentes, sin builder classes
srv := NewServidor(ConPuerto(9090), ConTimeout(10*time.Second))

Inyección de dependencias sin contenedor

di.go — el "contenedor" es main()
// Cada capa declara sus dependencias como interfaces en el constructor.
// main() las construye en orden y las conecta. Eso es TODO el DI de Go:
func main() {
	db := store.Abrir(os.Getenv("DATABASE_URL"))
	repo := store.NewUsuarioRepo(db)          // capa datos
	svc := service.NewUsuarios(repo)          // capa negocio (recibe interfaz)
	api := handler.New(svc)                   // capa HTTP (recibe interfaz)

	http.ListenAndServe(":8080", api.Router())
}
// Ventaja sobre Spring: el grafo de dependencias es código que se lee
// de arriba a abajo. Ctrl+click responde "¿de dónde sale esto?" siempre.
// ¿Proyecto enorme? Existe google/wire (genera este cableado), pero
// la mayoría de equipos simplemente escriben main() a mano.

Anti-patrones: Java que no debes traducir

Hábito JavaEn Go se hace…Por qué
Getters/setters para todoCampos exportados directosSin frameworks de beans que los exijan; añade métodos solo si hay lógica
AbstractBaseService<T>Funciones + composiciónSin herencia; lo común se extrae a funciones o structs embebidos
UtilsClass con estáticosPaquete pequeño con nombre clarostringsx.Truncar() se lee mejor que Utils.truncar()
Singleton con doble locksync.Once o variable de paqueteEl runtime garantiza la inicialización única
Interfaz por cada clase "por si acaso"Interfaz solo cuando hay 2+ implementaciones o un test que la necesitaLas interfaces se descubren, no se planifican
DTO + Entity + Mapper para todoA menudo el mismo struct con tagsSepara solo cuando los modelos de verdad divergen