Como probablemente los más habituales ya sabéis, somos muy fans de las series de artículos, y, con el paso del tiempo, solemos embarcarnos en varias de ellas. Y, si bien es cierto que aún tenemos algunas pendientes de continuar (cómo crear un videojuego en Go), también hemos cerrado ya algunas de ellas (orientación a objetos en Go, patrones de concurrencia). Así que, no hemos podido tener una mejor idea que empezar con una nueva serie: patrones de diseño en Go. Si bien es cierto que, como ya vimos en su día, Go podría considerarse un lenguaje de programación orientada a objetos, esto sería con algunas particularidades, que, en este caso, también habrá que considerar a la hora de implementar los patrones de diseño más comunes. Para ello, vamos a empezar con un patron de diseño creacional que es algo tabú: el patrón singleton.
¿Qué es el patrón singleton?
Bien, el patrón singleton es un patrón de diseño creacional, es decir, que nos va a ayudar en la inicialización de nuestros objetos, en particular cuándo necesitemos tener una instancia única global en nuestra aplicación, cuya inicialización pueda ser idealmente retardada (lazy initialization).
Entonces, lo que vamos a querar lograr es:
- Asegurar que la instancia de nuestro objeto (en este caso struct) es única y que puede ser accedida globalmente.
- Que la inicialización de dicha instancia esté encapsulada y pueda ser retardada (es decir, instanciada cuándo vaya a ser usada).
Además, es probable que algunos de vosotros estéis pensando que este patrón suele estar desaconsejado por las contraprestaciones asociadas al mismo (como la dificultad a la hora de testear en algunos casos o el uso de variables globales). Sin embargo, el propósito de este artículo no es otro que el de ver como lograr los objetivos que nos hemos fijado, en Go, dejando la decisión de usarlo o no al gusto y a criterio del consumidor.
Implementando el patrón singleton
Después de ver la parte teórica, es hora de pasar a la acción. Y para ello, vamos a suponer que en nuestro caso de uso
tenemos un struct de conexión (type Connection struct
) cuya instancia debe ser única (es decir, vamos a suponer que
queremos reaprovechar la misma conexión para todos los componentes de nuestra aplicación).
El primer paso pues, sería declarar la instancia de dicha conexión, pues como ya dijimos, dicha instancia debía ser global.
package connection
type Connection struct { /* details are out of scope */ }
var Instance *Connection = &Connection{ /* initialization here */ }
Sin embargo, si lo hacemos de este modo, lo que vamos a tener será una instacia global manipulable por cualquiera. Por esa razón, lo que vamos a hacer es encapsular dicha instancia en una variable privada (recordad que la visibilidad en Go es a nivel de paquete). Además, aprovecharemos dicho método para hacer que la inicialización de la instancia se produzca en tiempo de uso / consumo:
package connection
type Connection struct{ /* details are out of scope */ }
var instance *Connection
func Instance() *Connection {
if instance == nil {
instance = &Connection{ /* initialization here */ }
}
return instance
}
Como podemos ver, ahora nuestro código ha quedado algo más manejable que el anterior, pues nuestra instancia será privada
y el método connection.Instance()
no solo nos permitirá obtener dicha instancia globalmente, sino que además hará la
inicialización cuándo dicha instancia sea usada por primera vez (permitiéndonos ahorrar dicha inicialización cuando no sea
necesaria).
Soporte para la concurrencia
Bien, ahora ya tenemos nuestra propia implementación funcional del patrón singleton. Sin embargo, debemos recordar que
Go es un lenguaje multi-hilo (además especialmente usado por su soporte para la concurrencia). Por lo tanto, debemos
esperar que múltiples rutinas llamen al método Instance
de forma concurrente, lo que nos podría llevar a race conditions.
Una opción para proporcionar a nuestro singleton de dicho soporte para concurrencia es mediante locks. Veamos un ejemplo:
package connection
import "sync"
type Connection struct{ /* Details are out of scope */ }
var (
lock = &sync.Mutex{}
instance *Connection
)
func Instance() *Connection {
lock.Lock()
defer lock.Unlock()
if instance == nil {
instance = &Connection{ /* initialization here */ }
}
return instance
}
De este modo, como podéis ver, mediante la estructura sync.Mutex
(lock) y la sentencia defer
,
vamos a ser capaces de soportar llamadas concurrentes a la función connection.Instance()
, pues lo que va a suceder es
que, la primera llamada que llegue, bloqueará la ejecución de las posteriores (adquirirá el lock), haciendo que la
línea de instanciación de la conexión sea ejecutada por una única rutina al mismo tiempo, a diferencia del caso anterior,
y evitando así race conditions.
Consideraciones idiomáticas
Ahora sí que nuestro singleton ya estaría listo para ser usado. Sin embargo, aún quedan algunas consideraciones idiomáticas
a tener en cuenta. La primera de todas (y probablemente en la que ya estáis pensando) es la de usar la estructura sync.Once
en lugar del lock para dotar a nuestra implementación de soporte para la concurrencia. Veamos un ejemplo:
package connection
import "sync"
type Connection struct{ /* details are out of scope */ }
var (
once sync.Once
instance *Connection
)
func Instance() *Connection {
once.Do(func() {
instance = &Connection{ /* initialization here */ }
})
return instance
}
Como podéis imaginar, en este caso la estructura sync.Once nos permitirá conseguir el mismo resultado que antes (que la instanciación solo se produzca una vez) pero de una forma mucho más idiomática.
Por otro lado, también deberíamos tener en cuenta las consideraciones idiomáticas asociadas al propio hecho de que el lenguaje no es exactamente un lenguaje de programación orientada a objetos como lo concebemos habitualmente, pues, en ese sentido, no tenemos constructoras o métodos mágicos de clonado que sobreescribir como sí haríamos en otros lenguajes como PHP o Python. Eso sí, como contraprestación, cualquiera podría inicializar o clonar nuestra estructura (struct) por su cuenta, así que será necesario también un mínimo nivel de disciplina.
NOTA: Esto último podría ser resuelto mediante la definición de una interfaz, permitiéndonos así no publicar el tipo
Connection
de tal modo que ya no se podría instanciar desde fuera de nuestro paquete.
Sin embargo, esto podría tener otras contraprestaciones asociadas.
Y vosotros, ¿ya habíais implementado alguna vez el patrón singleton en Go? ¿Tenéis alguna consideración o recomendación que queráis compartir con nosotros? Como siempre, estaremos encantados de recibir vuestro feedback en los comentarios del blog o vía Twitter.