// 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.
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:
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.
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.
// 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
// 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=30sSi 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-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 okInstalació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:
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@v6Comparado 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
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
// 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 Java | En Go se hace… | Por qué |
|---|---|---|
| Getters/setters para todo | Campos exportados directos | Sin frameworks de beans que los exijan; añade métodos solo si hay lógica |
AbstractBaseService<T> | Funciones + composición | Sin herencia; lo común se extrae a funciones o structs embebidos |
UtilsClass con estáticos | Paquete pequeño con nombre claro | stringsx.Truncar() se lee mejor que Utils.truncar() |
| Singleton con doble lock | sync.Once o variable de paquete | El runtime garantiza la inicialización única |
| Interfaz por cada clase "por si acaso" | Interfaz solo cuando hay 2+ implementaciones o un test que la necesita | Las interfaces se descubren, no se planifican |
| DTO + Entity + Mapper para todo | A menudo el mismo struct con tags | Separa solo cuando los modelos de verdad divergen |