Visibilidad de paquetes mediante directorios internal

En Go, la visibilidad de un símbolo (como una función o una estructura) se controla habitualmente con la capitalización de su primera letra: si es mayúscula, es pública para el paquete; si es minúscula, es privada para ese paquete (unexported). Sin embargo, existe un nivel de encapsulación superior que no depende de los identificadores, sino de la estructura de directorios: los paquetes dentro de un directorio llamado internal.

Un paquete ubicado en foo/internal/bar tiene una visibilidad restringida a nivel de módulo. El compilador de Go impone una regla estricta: cualquier paquete que intente importar algo desde un directorio internal debe estar dentro del mismo árbol de directorios que contiene a ese internal. Esto permite crear una “API privada” para tu módulo: puedes tener varios paquetes en tu proyecto que compartan lógica compleja sin que esa lógica sea visible para usuarios externos que descarguen tu módulo.

Si necesitas que una lógica sea compartida entre varios paquetes de tu propia librería pero no quieres que sea parte de la interfaz pública que tus clientes usarán, internal es la herramienta correcta. Si intentas importar un paquete internal desde un módulo distinto, el compilador lanzará un error de compilación, impidiendo que tus implementaciones internas se conviertan en dependencias accidentales para terceros.

Para entender esto, imagina la estructura de un módulo llamado github.com/acme/processor. En este ejemplo, simulamos su funcionamiento en un único archivo para que puedas ejecutarlo directamente, pero explicaremos cómo se comportaría en una arquitectura real de producción.

// go.mod
// module github.com/acme/processor

package main

import (
	"fmt"
)

// --- SIMULACIÓN DE ESTRUCTURA DE DIRECTORIOS ---
// En un proyecto real, los siguientes bloques estarían en archivos separados:
// 
// 1. Archivo: internal/engine/logic.go (package engine)
//    Este paquete contiene la lógica crítica que NO queremos exponer.
//
// 2. Archivo: pkg/api/handler.go (package api)
//    Este paquete es público y es lo que los usuarios de nuestro módulo usarán.

// Simulamos el paquete 'engine' que estaría en /internal/engine
type Engine struct {
	secretKey string
}

func NewEngine(key string) *Engine {
	return &Engine{secretKey: key}
}

// Execute simula una función interna que realiza cálculos complejos
func (e *Engine) Execute() string {
	return fmt.Sprintf("Procesando con clave: %s", e.secretKey)
}

// Simulamos el paquete 'api' que estaría en /pkg/api
// Este paquete es público y depende de 'internal/engine'
type API struct {
	engine *Engine
}

func NewAPI(e *Engine) *API {
	return &API{engine: e}
}

func (a *API) Serve() {
	// El paquete 'api' puede importar 'internal/engine' porque
	// ambos pertenecen al mismo módulo (github.com/acme/processor)
	result := a.engine.Execute()
	fmt.Println("API Response:", result)
}

// --- PUNTO DE ENTRADA ---

func main() {
	// El comando 'cmd/app/main.go' es el punto de entrada.
	// Puede usar tanto paquetes públicos (pkg/) como internos (internal/).
	
	engine := NewEngine("super-secret-token-123")
	api := NewAPI(engine)

	fmt.Println("Iniciando aplicación...")
	api.Serve()
}

Desglose del funcionamiento

En el código anterior, aunque todo reside en package main para permitirte la ejecución inmediata, la arquitectura lógica es la siguiente:

  1. El componente Engine representa el código que reside en internal/engine. Aunque sus métodos como Execute comiencen con mayúscula (siendo públicos dentro de su propio paquete), al estar dentro de un directorio internal, su visibilidad queda limitada.
  2. El componente API representa un paquete en pkg/api. En una estructura de producción, api importaría a engine mediante import "github.com/acme/processor/internal/engine". Esto es legal porque api y engine comparten el mismo módulo raíz.
  3. El main funciona como el orquestador en cmd/app/. Este tiene acceso total a la jerarquía de directorios del módulo, permitiéndole instanciar el motor interno para configurar la API.

Lo que ocurre bajo el capó es que el compilador de Go no busca solo el nombre del paquete, sino la ruta completa en el sistema de archivos. Cuando encuentra la secuencia internal en una ruta de importación, activa una validación de seguridad: “Si el importador no es descendiente del padre del directorio internal, abortar compilación”.

El error frecuente

El error más común ocurre cuando intentas mover una lógica de un paquete pkg/ (público) a un paquete internal/ (privado) porque querías “limpiar” tu API, pero olvidaste que un usuario externo de tu módulo también podría estar intentando usarlo.

Si un desarrollador externo crea su propio proyecto y hace esto:

package main

import (
    // INTENTO ILEGAL: Intentar usar algo privado de tu módulo
    "github.com/acme/processor/internal/engine" 
)

func main() {
    e := engine.NewEngine("hack")
}

El compilador no permitirá la creación del binario y lanzará un error similar a este:
use of internal package github.com/acme/processor/internal/engine prohibited

Este mecanismo es vital para la evolución de la librería: te permite cambiar, renombrar o eliminar el código dentro de internal en cualquier momento sin miedo a romper el código de tus usuarios, ya que el compilador garantiza que nadie fuera de tu módulo puede depender de él.

Diseña tu API pública de forma minimalista y deja la complejidad pesada dentro de directorios internal.

101

Dejar un comentario

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

Scroll al inicio