Cuando un proyecto en Go crece, dejar todo el código en la raíz del módulo se vuelve insostenible. Necesitas una estructura que separe la lógica de negocio de los puntos de entrada y, lo más importante, que proteja tu API de cambios accidentales. Esta organización consiste en definir tres tipos de directorios: cmd/ para los binarios, internal/ para el código privado y pkg/ (opcional) para el código público.
Esta estructura no es una imposición del compilador en su totalidad, sino una convención de diseño para gestionar la visibilidad. Usamos cmd/ para que el usuario final no importe el main de tu aplicación, internal/ para aprovechar una restricción real del compilador que impide que otros módulos importen tu lógica privada, y pkg/ para ofrecer utilidades de forma clara. Debes usar este patrón cuando tu proyecto deja de ser un script de una sola función y se convierte en una aplicación con múltiples binarios (como un servidor API y una herramienta CLI) o una librería que requiere una distinción clara entre su implementación interna y su interfaz pública. Si aplicas mal esta división, expondrás detalles de implementación que, una vez importados por terceros, te obligarán a mantenerlos para siempre para no romper la compatibilidad, limitando tu capacidad de refactorizar.
package main
import (
"errors"
"fmt"
"strings"
)
// --- SIMULACIÓN DE ESTRUCTURA DE DIRECTORIOS ---
// En un proyecto real, este código no estaría en un solo archivo.
// El siguiente ejemplo integra las tres capas en un único archivo
// para que sea ejecutable, pero representa la lógica de cada carpeta.
// --- [pkg/validator/validator.go] ---
// Un paquete público. Cualquier módulo externo puede importar "mi-proyecto/pkg/validator".
// Se usa para utilidades genéricas y estables.
func IsEmpty(s string) bool {
return len(strings.TrimSpace(s)) == 0
}
// --- [internal/auth/auth.go] ---
// Un paquete privado. El compilador de Go prohíbe que otros módulos
// importen cualquier cosa que esté dentro de una carpeta llamada 'internal'.
// Es ideal para lógica que puede cambiar sin previo aviso.
type authenticator struct {
secretKey string
}
func newAuthenticator(key string) *authenticator {
return &authenticator{secretKey: key}
}
func (a *authenticator) verify(token string) error {
if token != a.secretKey {
return errors.New("token inválido")
}
return nil
}
// --- [cmd/api/main.go] ---
// El punto de entrada. Solo debe contener código de orquestación:
// leer flags, configurar variables de entorno y arrancar el proceso.
func main() {
// Configuración simulada
apiKey := "prod-secret-key-789"
userToken := "prod-secret-key-789"
fmt.Println("--- Iniciando Servidor API ---")
// 1. Uso de pkg (público y seguro para compartir)
if IsEmpty(userToken) {
fmt.Println("Error: El token proporcionado está vacío.")
return
}
// 2. Uso de internal (lógica privada del módulo)
// En un proyecto real, esto sería: auth.NewAuthenticator(apiKey)
auth := newAuthenticator(apiKey)
err := auth.verify(userToken)
if err != nil {
fmt.Printf("Acceso denegado: %v\n", err)
return
}
fmt.Println("Autenticación exitosa. Bienvenido al sistema.")
}
En el ejemplo anterior, la función main actúa como el controlador de cmd/api/main.go. Nota que su única misión es coordinar el flujo: llamar al validador y luego al autenticador.
La función IsEmpty representa lo que viviría en pkg/. Es una utilidad pura, no tiene estado y es altamente reutilizable; por tanto, no nos importa si un tercero la importa. En cambio, el tipo authenticator y su método verify simulan el contenido de internal/auth. Al estar en un paquete internal, garantizamos que si mañana decidimos cambiar secretKey por un sistema de firmas JWT, los usuarios de nuestro módulo no se verán afectados porque, técnicamente, nunca pudieron acceder a esa estructura de todos modos. Finalmente, el constructor newAuthenticator (que en un entorno real sería NewAuthenticator para ser exportado dentro de su propio paquete) es la única forma en que el resto del módulo interactúa con esa lógica privada, manteniendo el encapsulamiento.
El dilema de pkg/ es real: muchos desarrolladores prefieren poner los paquetes directamente en la raíz del módulo para evitar la profundidad de carpetas. La regla de oro es: si el paquete es una utilidad genérica que no depende de la lógica específica de tu aplicación, considera la raíz o pkg/. Si es lógica de negocio core, ve directo a internal/.
El error frecuente ocurre cuando confundes la intención de pkg/ y colocas lógica de implementación en ella. Si mueves internal/auth a pkg/auth para “facilitar” la importación, estás cometiendo un error de diseño grave. Al hacerlo, estás declarando formalmente que esa API es pública. Si en la siguiente versión decides que la autenticación debe ser mediante un servicio externo y cambias la firma de los métodos, habrás roto el código de todos tus clientes. Usa internal/ para proteger tu capacidad de evolucionar el código sin pedir permiso a nadie.
N° 105