El mito del “Standard Project Layout” en Go

Si has explorado repositorios de proyectos grandes en GitHub, habrás visto inevitablemente una estructura de carpetas con cmd, pkg, internal, api, web y api. Probablemente te hayas topado con el repositorio golang-standards/project-layout. Aunque es un recurso muy popular y ampliamente citado, es vital entender que no es un estándar oficial de Go. Es un esfuerzo de la comunidad, una convención social, pero no una regla del lenguaje ni una recomendación del equipo de Go.

De hecho, el equipo de Go ha sido explícito al declarar que no existe un “layout oficial” para los proyectos. En Go, la simplicidad y la facilidad de lectura suelen primar sobre la organización jerárquica profunda. El project-layout comunitario intenta resolver la organización de proyectos masivos, pero si intentas aplicarlo a un microservicio pequeño o a una herramienta CLI, estarás cometiendo un error de diseño.

Esta estructura es útil solo cuando tu proyecto escala hasta el punto de necesitar múltiples binarios (usando cmd/) o cuando quieres proteger lógica crítica de ser importada por otros proyectos (usando internal/). Sin embargo, el peligro de adoptarlo de forma prematura es la sobre-ingeniería: terminar con una estructura de directorios compleja para un código que todavía no existe o que no lo justifica. Si empiezas creando diez carpetas para un proyecto de tres archivos, solo estás añadiendo fricción cognitiva y dificultando la navegación para tus compañeros.

La recomendación pragmática es la siguiente: empieza con un diseño plano. Mantén tu lógica en el paquete main o en un par de archivos en la raíz. A medida que el proyecto crezca, cuando detectes que un archivo es demasiado grande o que una funcionalidad necesita ser compartida o protegida, extrae esa lógica a un paquete nuevo. Deja que el código dicte la arquitectura, no una plantilla de carpetas.

package main

import (
	"fmt"
	"net/http"
	"time"
)

// Server encapsula la lógica de nuestra aplicación. 
// Al mantenerlo en el mismo paquete, evitamos la complejidad de 
// exportar múltiples tipos y dependencias externas innecesarias.
type Server struct {
	addr         string
	requestCount int
}

func (s *Server) handlePing(w http.ResponseWriter, r *http.Request) {
	s.requestCount++
	fmt.Fprintf(w, "pong (total: %d)\n", s.requestCount)
}

func (s *Server) start() error {
	mux := http.NewServeMux()
	mux.HandleFunc("/ping", s.handlePing)

	srv := &http.Server{
		Addr:         s.addr,
		Handler:      mux,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	fmt.Printf("Iniciando servidor en %s...\n", s.addr)
	return srv.ListenAndServe()
}

func main() {
	// Empezamos con algo simple: un único punto de entrada.
	// No hay carpetas /cmd, /internal ni /pkg.
	// Solo código funcional y testable.
	app := &Server{
		addr: ":8080",
	}

	if err := app.start(); err != nil {
		fmt.Printf("Error crítico: %v\n", err)
		// En un caso real, usarías un logger estructurado aquí.
	}
}

En el ejemplo anterior, hemos implementado un servidor HTTP funcional que mantiene todo en el package main. Fíjate en Server: es una estructura que encapsula el estado (requestCount) y el comportamiento (handlePing). Al estar todo en el mismo paquete, no necesitamos crear un directorio pkg/server ni internal/app para que el código sea limpio, testeable y profesional.

Si este servidor empezara a crecer y tuvieras que añadir una lógica de base de datos compleja, ese día moverías la lógica de Server a un paquete internal/app. Si decidieras que quieres añadir una herramienta de migración de base de datos que sea un binario separado, ese sería el momento de crear una carpeta cmd/migrate. La estructura ha evolucionado como respuesta a una necesidad real, no como una predicción de un futuro inexistente. El Server sigue siendo la pieza central, pero ahora su ubicación refleja su importancia y su ámbito de uso.

El error frecuente

El error más común es el “Folder-Driven Development” (Desarrollo guiado por carpetas). Consiste en crear la estructura completa de golang-standards antes de escribir la primera función de lógica de negocio.

# ERROR: Estructura prematura para un proyecto que solo hace un "Hello World"
my-project/
├── cmd/
│   └── my-app/
│       └── main.go
├── internal/
│   ├── app/
│   │   └── app.go
│   ├── config/
│   │   └── config.go
│   └── domain/
│       └── models.go
├── pkg/
│   └── util/
│       └── helper.go
├── go.mod
└── Makefile

Si un desarrollador nuevo abre este repositorio, perderá tiempo navegando por cinco niveles de profundidad para entender qué hace el programa. Si el proyecto se cancela o se simplifica, te habrás quedado con una arquitectura de “código fantasma” que solo añade ruido al repositorio.

104

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio