Versionado y compatibilidad en librerías de Go

Mantener la compatibilidad hacia atrás (backward compatibility) no es solo una buena práctica; en Go, es el mecanismo que mantiene la estabilidad de todo el ecosistema de módulos. Cuando publicas una librería en la versión v1.0.0, estás firmando un contrato con tus usuarios: sus proyectos deben seguir compilando y funcionando mientras se mantengan dentro de la misma rama mayor.

Esto funciona gracias a cómo el comando go gestiona los directorios de módulos en la caché local. Si mantienes la compatibilidad en las versiones v1.x.x —añadiendo nuevas funciones, tipos o métodos—, los usuarios pueden actualizar sus dependencias de forma segura usando go get -u. Sin embargo, si decides cambiar la firma de una función (por ejemplo, añadiendo un parámetro), eliminar una función exportada o cambiar un tipo de dato en la firma, estarás rompiendo el contrato. En Go, la solución técnica para estos cambios disruptivos es el salto de versión mayor (major bump), lo que implica cambiar la ruta de importación del módulo (ej. de github.com/usuario/proyecto a github.com/usuario/proyecto/v2). Esto permite que los usuarios de la v1 sigan trabajando sin interrupciones mientras otros migran gradualmente a la v2.

Debes aplicar este rigor siempre que seas el autor de un módulo destinado a ser consumido por otros. Si estás trabajando en un paquete puramente internal de tu propio proyecto, tienes la libertad de cambiar lo que quieras sin miedo. Pero en librerías públicas, la previsibilidad es lo que define a un desarrollador profesional.

Si ignoras estas reglas, causarás fallos de compilación en los proyectos de tus usuarios y, lo que es peor, fallos lógicos silenciosos. Cambiar el comportamiento observable de una función (que antes devolvía un error nil y ahora devuelve un error con valor) es una ruptura que el compilador no detectará, pero que tumbará sistemas en producción.

package main

import (
	"errors"
	"fmt"
)

// --- SIMULACIÓN DE UNA LIBRERÍA PÚBLICA (github.com/acme/payments) ---

// PaymentProcessor define la interfaz para procesar pagos de forma moderna.
type PaymentProcessor interface {
	Pay(amount float64) error
}

// LegacyProcessor es una estructura antigua para procesar pagos con enteros.
// Deprecated: Usa NewStripeProcessor en su lugar. La lógica de enteros está obsoleta.
type LegacyProcessor struct{}

// PayLegacy procesa un monto entero. Es el método que se está retirando.
func (lp *LegacyProcessor) PayLegacy(amount int) error {
	fmt.Printf("[Legacy] Procesando monto: %d\n", amount)
	return nil
}

// RealProcessor implementa la interfaz PaymentProcessor con lógica moderna.
type RealProcessor struct {
	APIKey string
}

// Pay realiza el proceso de pago con precisión de punto flotante.
func (rp *RealProcessor) Pay(amount float64) error {
	if amount <= 0 {
		return errors.New("el monto debe ser mayor a cero")
	}
	fmt.Printf("[Modern] Procesando monto: %.2f con API Key: %s\n", amount, rp.APIKey)
	return nil
}

// internalHelper es una función que no es exportada (empieza con minúscula).
// Aunque estuviera en un paquete 'internal/', no podría ser usada por el consumidor final.
func internalHelper() string {
	return "secreto_de_implementacion"
}

// --- SIMULACIÓN DEL CONSUMIDOR (Tu aplicación principal) ---

func main() {
	// 1. Uso de la API antigua (v1.0.0 - Compatible)
	// El usuario todavía puede usar esto para evitar romper su código hoy.
	oldProc := &LegacyProcessor{}
	_ = oldProc.PayLegacy(100)

	// 2. Uso de la API nueva (v1.1.0 - Evolución compatible)
	// El desarrollador migra gradualmente a la nueva forma recomendada.
	newProc := &RealProcessor{APIKey: "sk_live_51N..."}
	err := newProc.Pay(150.75)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	// 3. Ejemplo de cómo el uso de 'internal' protege al autor
	// Si cambiamos 'internalHelper' internamente, el usuario nunca se entera.
	fmt.Printf("Sistema listo. Uso de: %s\n", internalHelper())
}

Desglose del ejemplo

En el código anterior, hemos simulado la evolución de una API profesional.

Primero, observa LegacyProcessor y su método PayLegacy. He utilizado el comentario // Deprecated: .... Esto es crucial: no es solo un comentario informativo; las herramientas como go doc y los IDEs (como VS Code o GoLand) lo detectan para tachar el nombre de la función o mostrar una advertencia visual al programador. Esto permite que el usuario sepa que debe migrar a RealProcessor, pero sin que su código deje de compilar hoy mismo.

La estructura RealProcessor representa la nueva versión de la API. Nota que hemos cambiado la precisión de int a float64. Si hubiéramos eliminado PayLegacy y solo nos hubiéramos quedado con Pay en la misma versión, habríamos roto el código de todos nuestros usuarios. Al mantener ambos, permitimos una transición suave.

Finalmente, la función internalHelper ilustra el concepto de encapsulamiento. Al no empezar con mayúscula, es privada al paquete. En un entorno real, si colocas lógica en un directorio llamado internal/, el sistema de módulos de Go garantiza que ningún código fuera de tu módulo pueda importarlo, dándote la libertad de refactorizar esa lógica tan a menudo como quieras sin romper la compatibilidad de la API pública.

El error frecuente

Un error crítico es realizar cambios de comportamiento en funciones existentes sin avisar. Imagina que tienes esta función:

// Versión v1.0.0
func (p *Processor) Validate(data string) bool {
    return data != ""
}

// Versión v1.1.0 (ERROR: Cambio de comportamiento observable)
func (p *Processor) Validate(data string) bool {
    // Ahora también valida que no sea un espacio en blanco
    return data != "" && !strings.TrimSpace(data) == ""
}

Aunque la firma de la función no ha cambiado (sigue siendo string -> bool), el comportamiento sí. Un usuario que enviaba " " esperaba que Validate devolviera true, pero ahora recibirá false. Esto puede causar que una validación que antes pasaba, ahora falle, rompiendo la lógica de negocio del cliente sin que el código falle al compilar. Si necesitas cambiar la semántica de una función, es mejor introducir una nueva función o, si el cambio es estructural, saltar a una v2.

227

Dejar un comentario

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

Scroll al inicio