Sempre me deparo com necessidade de implementar o Pattern Singleton em meus projetos, mas em Golang existem algumas particularidades que temos que tomar cuidado. Neste artigo, vou apresentar duas formas de implementar o Singleton usando Golang, a forma “Not Thread Safe” e a forma “Thread Safe”. O objetivo é apresentar de forma prática e técnica as formas de implementação e quando temos que implementar o patterns singleton.
Problema a ser resolvido
Temos que abrir uma conexão com o banco de dados e mantê-la em uma única instância para não sobrecarregar o banco e termos problemas ou erros com o limite de conexões simultâneas excedido, além, é claro, de otimizarmos nosso código. Sabemos que uma conexão é muito custosa computacionalmente.
Como o banco não dá suporte à pool de conexões de forma nativa, então precisaremos controlar todas as conexões que nossa aplicação irá abrir para se comunicar com ele, assim, em cada ação que formos executar, seja um select, delete, insert ou update, precisaremos de um objeto de conexão em memória de forma atômica e para isso vamos utilizar o pattern singleton.
Antes de implementarmos nossa solução usando Pattern Singleton para resolver o problema do pool de conexões, vamos conferir alguns outros exemplos da necessidade da utilização do pattern singleton.
Abaixo alguns exemplos de diversas formas de tentar proteger nossa variável global.
Protegendo variável Global Exemplo 1
// type global
type singleton map[string]string
var (
instance singleton
)
func NewClass() singleton {
if instance == nil {
instance = make(singleton) // <-- not thread safe
}
return instance
}
Protegendo variável Global Exemplo 2
var lock = &sync.Mutex{}
// type global
type singleton map[string]string
var (
instance singleton
)
func NewClass() singleton {
lock.Lock()
defer lock.Unlock()
if instance == nil {
instance = make(singleton) // <-- thread safe
}
return instance
}
Protegendo variável Global Exemplo 3
var once sync.Once
// type global
type singleton map[string]string
var (
instance singleton
)
func NewClass() singleton {
once.Do(func() { // <-- atomic, does not allow repeating
instance = make(singleton) // <-- thread safe
})
return instance
}
Acima é mostrado três formas de proteger nossa variável global “instance”, para soluções síncronas o exemplo 1 já irá funcionar, para soluções assíncronas o exemplo 2 e 3 são o mais indicados sendo que o exemplo 3 é a melhor solução para protegermos de “race condition” garantindo que a instância seja declarada uma única vez tornando-se atômica.
O objetivo do artigo é mostrar exatamente as formas de tentar proteger a variável global, em Golang temos diversas maneiras de fazermos isso.
Abaixo temos mais alguns exemplos para complementar o raciocínio, mostrando como fazemos as instanciações do nosso singleton, seja ele de forma síncrona ou assíncrona usando goroutine.
Exemplos de como instanciar nosso Singleton
O código abaixo é um bom exemplo de aberturas de diversas conexões de forma síncrona, dependendo da forma que implementarmos nossa instância de conexão o banco não suportaria.
func main() {
objeto := NewClass() // instance make
objeto["this"] = "Object NewClass"
objeto2 := NewClass() // return instance
fmt.Println("This is:", objeto2["this"])
objeto3 := NewClass() // return instance
objeto4 := NewClass() // return instance
objeto5 := NewClass() // return instance
fmt.Println("This is:", objeto3["this"])
fmt.Println("This is:", objeto4["this"])
fmt.Println("This is:", objeto5["this"])
}
Agora vamos visualizar um exemplo de abertura de conexões de forma assíncrona utilizando goroutine. Fizemos centenas de goroutines e milhares de conexões simultâneas em nosso exemplo abaixo, funciona 100% sem travar e ou bugar a quantidade de conexões do banco de dados.
func main() {
objeto := NewClass() // instance make
go func() {
for i := 0; i < 1000; i++ {
objeto = NewClass() // <-- return instance
}
}()
go func() {
for i := 0; i < 100; i++ {
go func() {
objeto = NewClass() // <-- return instance
}()
}
}()
fmt.Scanln()
}
Goroutines e o pattern singleton
Quando pensamos nas possíveis soluções para a implementação do pattern singleton usando Golang, esbarramos nos Goroutines ele será responsável por nos proporcionar que nosso código execute de forma assíncrona e concorrente e quando usamos Goroutine em nossa aplicação toda nossa forma de pensar e implementar muda, ou seja não é mais uma aplicação síncrona e devido a isso temos que pensar da forma Golang de ser.
As Goroutines é um poderoso recurso e quando usado corretamente ele torna-se um forte aliado para combatermos as batalhas do dia a dia. Toda vez que implementamos códigos que usam concorrência temos alguns cenários já conhecidos que temos que tratar, o escopo do programa, variáveis globais, locais, passagem de parâmetros, ponteiros tudo isso tem que ser tratado para que possamos trabalhar com concorrência de forma correta e otimizada.
Um bom exemplo de possíveis problemas usando concorrência é a utilização de variáveis globais. Devido as goroutines nossa implementação do Pattern Singleton e nossas possíveis soluções será escrita para aceitar o uso de concorrência. Vamos escrever o nosso código usando boas práticas de programação, para tentar mitigar os possíveis bugs sigilosos que podem ocorrer em tempo de execução do seu programa escrito em Golang.
200 goroutines sendo inicializadas
Em nosso exemplo abaixo está sendo criado 200 goroutines e colocando todas em concorrência e dez mil interações estão sendo feitas em nosso banco de dados ou seja “connect e faça: select email from login where id =?”.
for x := 0; x < 200; x++ {
go func(x int) {
for j := 0; j < 10000; j++ {
fmt.Println(" login: ", Conn.Connet().GetUserEmail(x))
time.Sleep(time.Millisecond * 150)
}
}(x)
}
O bom entendimento sobre as goroutines irá nos ajudar a escrever códigos melhores e mais poderosos em Golang, isto não tenho dúvidas. Em um próximo artigo iremos descrever alguns cases usando goroutines, estou ansioso para escrever sobre Goroutines e mostrar como resolvemos alguns de nossos problemas na empresa.
O assunto sobre Pattern Singleton é tão interessante que tive que fazer uma pesquisa bem mais profunda sobre o assunto antes de escrever este artigo e como uso muito em meu dia a dia decidi colaborar um pouco mostrando alguns pontos importantes deste pattern e por que muita das vezes ele é considerado um Anti Pattern. Descrevendo os detalhes técnicos no artigo percebemos claramente diversas boas práticas que poderemos utilizar em nosso dia dia quando estamos codando em Golang, boas práticas, formas clean de implementação, implementações menos complexas e menos custosas e com maior desempenho.
O que é Pattern Singleton?
A transcrição do que é um Pattern Singleton seria: *“*Singleton *é um padrão de projeto de software. Este padrão garante a existência de apenas uma instância de uma classe, mantendo um ponto global de acesso ao seu objeto”.*Singleton é um padrão de design que restringe a instanciação a um objeto, temos que garantir que isto ocorra somente uma única vez.
Basicamente singleton é uma maneira de usar variáveis globais. Sabemos o quanto é perigoso o uso de variáveis globais, nosso código fica vulnerável ao acesso da variável global ou seja em qualquer parte do sistema podemos alterar seu valor. Portanto, ao tentarmos debugar ou depurar nosso programa não será uma tarefa fácil descobrir qual caminho de código leva ao estado atual, este é um dos motivos que não considero o Pattern Singleton um Anti Pattern e sim uma formas de proteger as variáveis globais.
No entanto, o problema com Singleton usando concorrência que será nosso objetivo, em um ambiente multiencadeado, a inicialização deve ser protegida para evitar a reinicialização, de forma atômica.
O Pattern Singleton é uma característica proveniente do paradigma orientado a objeto, então como iremos implementar em Golang se o mesmo não possui suporte a OO ?
Para responder esta pergunta temos que entender que a Programação Orientada a Objeto é um conceito e pode ser implementando em qualquer linguagem de programação mesmo sendo de outros paradigmas. É claro que o nível de abstração e dificuldade torna-se uma tarefa árdua aumentando muito o nível de complexidade do código dependendo da linguagem, nossa intenção é puramente didática com intuito de entendermos melhor o cenário proposto quando falamos em pattern singleton*.*
go run -race singleton.go
Executa todos os códigos usando -race como parâmetro: go run -race ..
O “-race detector” é um recurso que temos disponível em Golang para detectar acessos indevidos em memória quando estamos usando concorrência em nossa aplicação. É possível gerar relatório que contém rastreamentos de pilha para acessos conflitantes, bem como pilhas nas quais as goroutines envolvidas foram criadas, em breve vou criar um artigo abordando exatamente este assunto e vamos falar sobre “Profiling“ em Go.
Abaixo as formas que podemos fazer a chamada do “-race”.
$ go test -race seupkg // to test the package
$ go run -race seusrc.go // to run the source file
$ go build -race seucmd // to build the command
$ go install -race seupkg // to install the package
Pattern Singleton em Golang
A solução apresentada abaixo seria ideal se nossa aplicação fosse síncrona, e o problema seria resolvido com código abaixo, mas como nosso objetivo é implementar usando concorrência a solução abaixo está longe de ser a ideal.
Vamos conferir o código abaixo e iniciar nossas possibilidades de implementação:
type DriverPg struct {
conn string
}
var instance *DriverPg
func Connect() *DriverPg {
if instance == nil {
// <--- NOT THREAD SAFE / Quando usarmos Goroutine
instance = &DriverPg{conn: "DriverConnectPostgres"}
}
return instance
}
func main() {
// chamada
go func() {
for i := 0; i < 100; i++ {
time.Sleep(time.Millisecond * 600)
fmt.Println(*Connect(), " - ", i)
}
}()
go func() {
fmt.Println(*Connect())
}()
fmt.Scanln()
}
Na implementação acima, temos nossa primeira abordagem implementando Singleton em Golang e com goroutines para executar simultaneamente o nosso Singleton.
O problema visível neste código é que várias rotinas de goroutine poderiam avaliar a primeira verificação e todas criariam uma instância do tipo singleton e substituiriam uma à outra. Não há garantia de qual instância será retornada no código acima, e outras operações adicionais na instância podem ser inconsistentes com as expectativas do desenvolvedor e problemas sigilosos podem ocorrer em tempo de execução.
Muito ruim esta abordagem, diversos erros muitos sutis podem ocorrer, se as referências à instância singleton estiverem sendo mantidas por meio do código, existe uma grande chance de haver potencialmente várias instâncias do tipo com estados diferentes, gerando potenciais comportamentos de código. Ele também se torna um verdadeiro pesadelo durante a depuração e se torna realmente difícil detectar o bug, já que no tempo de depuração nada parece estar errado devido às pausas de tempo de execução, minimizando o potencial de uma execução “Not Thread Safe”, ofuscando totalmente o problema para quem está codando.
Bloqueios com Mutex
No código abaixo é uma solução pobre para a tentativa de resolver o problema de “Thread Safe”. Na verdade, isso resolve o problema “Thread Safe”, mas cria outros problemas sérios em potencial. Ele introduz uma contenção nas goroutines executando um bloqueio agressivo de toda a função, vamos conferir o código abaixo:
// nosso lock mutex
var lock = &sync.Mutex{}
type DriverPg struct {
conn string
}
var instance *DriverPg
func Connect() *DriverPg {
// <--- Desnecessario a lock
// se a instancia já tiver
// sido criada muito agressivo
lock.Lock()
defer lock.Unlock()
if instance == nil {
// ainda não é a melhor implementação devido
// os bloqueios
instance = &DriverPg{conn: "DriverConnectPostgres"}
}
return instance
}
func main() {
go func() {
for i := 0; i < 100; i++ {
time.Sleep(time.Millisecond * 600)
fmt.Println(*Connect(), " - ", i)
}
}()
go func() {
fmt.Println(*Connect())
}()
fmt.Scanln()
}
O problema de “Thread Safe” foi resolvido com a implementação acima usando sync.Mutex onde ocorre o Bloqueio antes de criar a instância Singleton. O grande problema desta abordagem é o bloqueio excessivo mesmo quando não seria necessário fazer isso, no caso de a instância já ter sido criada e deveríamos simplesmente ter retornado a instância singleton. Se nosso programa for escrito para tornar-se altamente concorrente, isso pode gerar um gargalo, pois somente uma rotina de goroutine pode obter a instância singleton de cada vez, tornando-se nossa solução mais lenta.
Vamos conferir outra solução, por que está acima não é nossa melhor abordagem.
Check-Lock-Check em Go
Um forma de melhorar e a segurar a garantia de um mínimo de bloqueio e ainda ser seguro para a goroutine é utilizar o padrão chamado “Check-Lock-Check*”*, ao adquirir bloqueios. Usamos este mesmo Patter em C e C++.
O padrão funciona com a ideia de verificar primeiro, para minimizar qualquer bloqueio agressivo, já que uma instrução IF é menos dispendiosa do que o bloqueio.
No próximo momento teríamos de esperar e adquirir o bloqueio exclusivo para que apenas uma execução esteja dentro desse bloco em uma única vez. Com a primeira verificação e com bloqueio exclusivo, poderia haver outra goroutine que adquiriu o bloqueio, portanto, precisaríamos verificar novamente dentro do bloqueio para evitar a substituição da instância por outro.
Confira o código abaixo:
var lock = &sync.Mutex{}
type DriverPg struct {
conn string
}
var instance *DriverPg
func Connect() *DriverPg {
// ainda não está perfeita, não é totalmente atomico
if instance == nil {
lock.Lock()
defer lock.Unlock()
instance = &DriverPg{conn: "DriverConnectPostgres"}
}
return instance
}
func main() {
go func() {
for i := 0; i < 100; i++ {
time.Sleep(time.Millisecond * 600)
fmt.Println(*Connect(), " - ", i)
}
}()
go func() {
fmt.Println(*Connect())
}()
fmt.Scanln()
}
Está abordagem acima é a melhor até o momento, mas ainda não é perfeita. Não há uma verificação atômica no estado de armazenamento da instância. Levando em consideração todas as considerações técnicas, isso ainda não é perfeito. Mas é muito melhor que as abordagens iniciais.
Usando o pacote sync/atomic, podemos carregar e definir de forma atômica um sinalizador que indicará se inicializamos ou não nossa instância.
Confira o código abaixo:
// manter o estado
var atomicinz uint64
// lock mutex
var lock = &sync.Mutex{}
// driver
type DriverPg struct {
conn string
}
// instancia Global
var instance *DriverPg
// funcao retornando
// o ponteiro de nossa
// struct
func Connect() *DriverPg {
// garantindo que já entrou
if atomic.LoadUint64(&atomicinz) == 1 {
return instance
}
lock.Lock()
defer lock.Unlock()
// entra somente uma
// únic vez
if atomicinz == 0 {
instance = &DriverPg{conn: "DriverConnectPostgres"}
atomic.StoreUint64(&atomicinz, 1)
}
return instance
}
func main() {
// chamada
go func() {
time.Sleep(time.Millisecond * 600)
fmt.Println(*Connect())
}()
// 50 goroutine
for i := 0; i < 50; i++ {
go func(i int) {
for {
time.Sleep(time.Millisecond * 60)
fmt.Println(Connect().conn, " - ", i)
}
}(i)
}
fmt.Scanln()
}
A biblioteca sync/atomic nos permite sinalizar e armazenar um conteúdo de forma segura e garantindo sua unicidade, bem parecido com sync.Map, onde ele está armazenando em um storage e aceitando concorrência em sua implementação. O problema que tivemos que usar mais uma funcionalidade, mais recurso e consecutivamente um pouco mais lento nossa execução.
sync.Once clean e poderoso
Temos o tipo “Once” na biblioteca sync, lembrando que esta biblioteca nativa em Golang é muito poderosa e temos que explora-la o máximo que conseguirmos, o objeto sync.Once executará uma ação exatamente uma vez e não mais, era o que faltava para nosso código ficar ainda mais poderoso e clean.
// call somente
// uma unica vez
var once sync.Once
type DriverPg struct {
conn string
}
// variavel Global
var instance *DriverPg
func Connect() *DriverPg {
once.Do(func() {
instance = &DriverPg{conn: "DriverConnectPostgres"}
})
return instance
}
func main() {
// chamada
go func() {
time.Sleep(time.Millisecond * 600)
fmt.Println(*Connect())
}()
// 100 goroutine
for i := 0; i < 100; i++ {
go func(ix int) {
time.Sleep(time.Millisecond * 60)
fmt.Println(ix, " = ", Connect().conn)
}(i)
}
fmt.Scanln()
}
Com esta abordagem e cenário proposto nosso código além de ficar mais clean ficou bem melhor, a função sync.Once garante a unicidade de nossa instância, nosso código agora pode ter 100 goroutines ou muito mais conforme a sua necessidade, coloca-los em concorrência não teremos problemas de “Thread Safe” ou checagem agressiva como vimos acima em outras abordagens. Uma forma simples e segura de escrever o código Golang para implementação do Pattern Singleton.
init( ) Uma outra abordagem
Uma outra abordagem e também válida é utilizar o init( ), ele é executado somente uma única vez e é chamado antes da função maim.
Confere o código abaixo:
type DriverPg struct {
conn string
}
var instance *DriverPg
func Connect() *DriverPg {
instance = &DriverPg{conn: "DriverConnectPostgres"}
return instance
}
func init() {
Connect()
}
func main() {
// chamada
go func() {
time.Sleep(time.Millisecond * 600)
fmt.Println(instance.conn)
}()
go func() {
fmt.Println(*Connect())
}()
// 100 goroutine
for i := 0; i < 100; i++ {
go func(ix int) {
time.Sleep(time.Millisecond * 60)
fmt.Println(ix, " = ", instance.conn)
}(i)
}
fmt.Scanln()
}
Porém esta abordagem existe uma desvantagem quando usamos init( ). Percebesse claramente que não é segura, nada impede de fazer uma chamada direta na função “Connect” como ocorreu na linha #28 além de que existe uma limitação no uso do init( ) com relação ao seu tempo de carregamento e o mais importante em Golang podemos ter vários init sendo executados não somente em um arquivo ou pacote mas em vários, e existe uma ordem de execução entre eles.
A função init( ) não aceita argumentos nem retorna nenhum valor. Em contraste com nossa abordagem usando sync.Once, o identificador init() não é declarado, portanto não pode ser referenciado.
O melhor cenário é escrever códigos que não dependam da ordem de inicialização, em versões anteriores do golang houve algumas reclamações e diversos problemas relatados, não escrevam códigos em um ***init( )***que você precise de garantias de execução em determinado momento. A solução quando precisa de garantia explícita é escrever chamadas explícitas.
Para ter mais detalhes tem um página só disto em Golang https://golang.org/doc/effective_go.html#initialization, vale a pena a leitura detalhada sobre o init( ), é uma implementação poderosa e robusta nas versões atuais do Golang mas sempre é bom ficar atento.
Variável recebendo a função
Uma outra abordagem e também válida é utilizar uma variável global recebendo a função em um escopo global, sabemos que em Golang as variáveis são atribuídas e declaradas antes da chamada do init( ) e da função main, então nesta abordagem o método retorna exatamente uma única vez a instância. Confere o código abaixo:
type DriverPg struct {
conn string
}
var instance *DriverPg
var instanceNew = *Connect()
func Connect() *DriverPg {
if instance == nil {
// <--- NOT THREAD SAFE
instance = &DriverPg{conn: "DriverConnectPostgres"}
}
return instance
}
func main() {
// chamada
go func() {
time.Sleep(time.Millisecond * 600)
fmt.Println("goroutine 1: ", instanceNew.conn)
}()
go func() {
fmt.Println("goroutine 2: ", *Connect())
}()
fmt.Scanln()
}
Esta abordagem é falha por que nada garante que a função irá ser chamado novamente em alguma parte do código, como ocorreu na linha #25 a função foi chamada novamente, na linha #30 é feito a instância do nosso singleton, porém nada garante esta unicidade.
Conclusão
O ideal sem dúvida é utilização do sync.Once que garante com certeza a unicidade e que seja “Thread Safe” nos garantindo que não ocorra uma “race condition”, ele só permite que a função seja executada somente uma única vez Golang flexibilizou e automatizou toda complexidade que teríamos em outras langs se fôssemos trabalhar com concorrência e simultaneidade. Realmente Golang ficou poderoso nestas abordagens, tornando-se simples de implementar e entender.
Quando falamos de concorrência toda nossa forma de pensar e codar as soluções utilizando Golang mudam drasticamente. Existem vários cenários que precisamos aplicar padrões e práticas em nossos projetos para aproveitar o máximo do poder que o Golang oferece.
Todo projeto que participo torna-se quase um “mantra” entre as equipes de desenvolvimento fazer diversos testes e revisões para aproveitar o máximo que a linguagem proporciona. Quanto mais aprofundamos nos estudos mais descobrimos que pouco sabemos sobre a linguagem. O legal de tudo é o desafio, quando trabalhamos com goroutines em Golang precisamos entender o quanto antes as funcionalidades e o que é possível fazer, comportamentos, segurança das goroutines são todos essenciais para melhorar cada vez mais o nosso código.
Caso tenha interesse em acompanhar ou trocar ideias sejam elas a nível iniciante, intermediário ou “hardcore” sobre a linguagem Golang existe alguns grupos de Golang espalhados na internet dois deles em especial sempre que possível estou presente é o do https://t.me/go_br (GoBr) e o gophers.slack.com, comunidades ativas usando Golang, sempre estão compartilhando ideias, projetos, vagas e dúvidas todo o tempo e a todo momento estamos reaprendendo.
Espero ter ajudado a esclarecer um pouco as dúvidas sobre utilização do Pattern Singleton, utilizando variáveis globais, goroutines, sync.Once, Init( ), mutex e diversos outros pontos importantes e técnicos abordados no artigo.
Logo abaixo está todos os códigos fontes que utilizamos como exemplo no artigo caso tenha interesse de baixar e testar fique a vontade.
Códigos fontes e exemplos do artigo
https://github.com/jeffotoni/medium-posts
Gostaria de agradecer alguns colegas que ajudaram na revisão deste artigo para mantermos a qualidade do conteúdo e deixa-lo denso sem ficar complexo e jato. Obrigado :
, Marco Paganinii, Francisco Oliveira
No próximo artigo que está no forno será sobre lambdas, como escrever lambdas em Golang, será mais um desafio bem interessante, que estou preparando.
Quem curtiu e chegou até o final deixa uma palminha aí… Obrigado 🤘🤓
Top comments (0)