Patrón Singleton
Parte de la sección Programación Orientada a Objetos del Journey de GO de Coddy — lección 86 de 107.
El patrón Singleton garantiza que un tipo tenga una sola instancia en todo el programa y proporciona un punto de acceso global a ella. Esto es útil para recursos compartidos como gestores de configuración, conexiones a bases de datos o registradores (loggers).
En Go, implementamos el patrón Singleton utilizando una variable a nivel de paquete combinada con sync.Once para garantizar una inicialización segura para hilos (thread-safe):
package config
import "sync"
type Config struct {
DatabaseURL string
MaxRetries int
}
var (
instance *Config
once sync.Once
)
func GetInstance() *Config {
once.Do(func() {
instance = &Config{
DatabaseURL: "localhost:5432",
MaxRetries: 3,
}
})
return instance
}El sync.Once garantiza que la función de inicialización se ejecute exactamente una vez, incluso cuando múltiples goroutines llaman a GetInstance() simultáneamente. Cada llamada posterior devuelve la misma instancia sin volver a ejecutar el código de inicialización.
cfg1 := config.GetInstance()
cfg2 := config.GetInstance()
// cfg1 y cfg2 apuntan a la misma instanciaUsa Singleton con moderación. Aunque es conveniente, introduce un estado global que puede dificultar las pruebas y ocultar dependencias. Considera si la inyección de dependencias podría ser una mejor alternativa para tu caso de uso.
Desafío
Fácil¡Vamos a construir un singleton Logger que asegure que solo exista una instancia de logger en toda tu aplicación! Este es un caso de uso clásico para el patrón Singleton: quieres que todas las partes de tu programa compartan la misma configuración y estado del logger.
Organizarás tu código en dos archivos:
logger.go: Implementa tu logger Singleton seguro para hilos (thread-safe).Crea un struct
Loggercon dos campos:Prefix(string) para los prefijos de los mensajes de log, yMessageCount(int) para rastrear cuántos mensajes se han registrado.Usa variables a nivel de paquete con
sync.Oncepara asegurar que solo se cree una instancia. Implementa una funciónGetLogger()que inicialice el logger con un prefijo por defecto de"[LOG]"y devuelva la instancia singleton.Añade estos métodos a tu Logger:
SetPrefix(prefix string)- cambia el prefijo del loggerLog(message string) string- incrementa el contador de mensajes y devuelve una cadena formateada:[prefix] #[count]: [message]GetCount() int- devuelve el número total de mensajes registrados
main.go: Demuestra que múltiples llamadas aGetLogger()devuelven la misma instancia.Lee un nuevo valor de prefijo, luego lee un recuento de mensajes seguido de cada mensaje a registrar.
Obtén la instancia del logger, establece el prefijo personalizado, luego registra cada mensaje e imprime el resultado. Después de registrar todos los mensajes, obtén la instancia del logger de nuevo (para demostrar que es la misma) e imprime el recuento total de mensajes.
Se proporcionarán las siguientes entradas:
- Línea 1: Prefijo personalizado a establecer
- Línea 2: Número de mensajes
- Líneas siguientes: Cada mensaje a registrar
Por ejemplo, dado:
[APP]
3
Server started
User connected
Request processedTu salida debería ser:
[APP] #1: Server started
[APP] #2: User connected
[APP] #3: Request processed
Total messages logged: 3Y dado:
[DEBUG]
2
Initializing cache
Cache readyTu salida debería ser:
[DEBUG] #1: Initializing cache
[DEBUG] #2: Cache ready
Total messages logged: 2Y dado:
[ERROR]
1
Connection failedTu salida debería ser:
[ERROR] #1: Connection failed
Total messages logged: 1La idea clave es que no importa cuántas veces llames a GetLogger(), siempre obtendrás la misma instancia con su estado compartido. ¡El recuento de mensajes persiste porque solo existe un Logger!
Pruébalo tú mismo
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Lee el prefijo personalizado
prefix, _ := reader.ReadString('\n')
prefix = strings.TrimSpace(prefix)
// Lee el número de mensajes
countStr, _ := reader.ReadString('\n')
countStr = strings.TrimSpace(countStr)
count, _ := strconv.Atoi(countStr)
// Lee cada mensaje
messages := make([]string, count)
for i := 0; i < count; i++ {
msg, _ := reader.ReadString('\n')
messages[i] = strings.TrimSpace(msg)
}
// TODO: Obtén la instancia del logger usando GetLogger()
// TODO: Establece el prefijo personalizado usando SetPrefix()
// TODO: Registra cada mensaje e imprime el resultado
// TODO: Obtén la instancia del logger de nuevo (para demostrar que es la misma)
// y muestra el recuento total de mensajes en el formato: "Total messages logged: X"
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Programación Orientada a Objetos
1Fundamentos de POO en Go
Archivos externosEspacio de trabajo y módulos de GoPaquetes e importacionesNombres exportados vs. no exportadosIntroducción a la POO en GoStructs como clasesDefinición de métodos en structsReceptores de puntero vs. de valorInicialización de structsFunciones constructorasResumen - Calculadora simple4Interfaces
Introducción a las interfacesImplementación implícitaLa interfaz como contratoInterfaz vacía (any)Type AssertionType SwitchComposición de interfacesInterfaces Stringer y ErrorResumen: Calculadora de figuras7Encapsulamiento
Campos exportados vs. no exportadosEncapsulamiento a nivel de paqueteMétodos Getter y SetterOcultamiento de información en GoResumen - Registros de estudiantes10Genéricos (Go 1.18+)
Introducción a los GenéricosParámetros de tipoRestricciones de tipoStructs genéricosSolución alternativa para métodos genéricosResumen - Colección genérica13Patrones de diseño - Parte 1
Introducción a los patrones de diseñoPatrón SingletonPatrón FactoryPatrón Abstract FactoryPatrón ObserverPatrón Strategy2Inmersión profunda en Tipos y Structs
Tipos básicos y compuestosDefiniciones de tipos personalizadosEtiquetas de StructsStructs anónimosStructs anidadosValores cero y por defectoRepaso - Libreta de contactos5Composición sobre herencia
Por qué Go no tiene herenciaConceptos básicos de incrustación de structsPromoción de métodosIncrustación de múltiples structsIncrustación vs AgregaciónShadowing de métodos incrustadosResumen - Jerarquía de empleados8Manejo de Errores y OOP
La interfaz errorTipos de errores personalizadosEnvoltura de errores (fmt.Errorf)Errores centinelaerrors.Is() y errors.As()Panic, Defer y RecoverResumen - Analizador de archivos3Punteros y Memoria
Fundamentos de punteros en GoPunteros a StructsPaso por valor vs referenciaLa función new()Garbage Collection en GoResumen - Constructor de Linked List6Polimorfismo en Go
Polimorfismo mediante interfacesDuck Typing en GoReglas de satisfacción de interfacesColecciones polimórficasInyección de dependenciasResumen - Procesador de pagos9Concurrencia y POO
Fundamentos de GoroutinesChannels y comunicaciónChannels con buffer vs. sin bufferSentencia Selectsync.Mutex y sync.RWMutexsync.WaitGroupDiseño de estructuras Thread-SafeResumen - Worker Pool