O padrão de design Singleton é um dos mais importantes e frequentemente utilizados na programação de software. Ele assegura que uma classe tenha apenas uma única instância durante o tempo de execução da aplicação e fornece um ponto de acesso global a essa instância. Neste artigo, discutiremos a importância do Singleton, como implementá-lo em Golang e os benefícios que ele traz, especialmente em ambientes concorrentes.
O que é o Singleton?
O Singleton é um padrão de design que restringe a instância de uma classe a uma única instância. É particularmente útil em situações onde um único ponto de controle ou um único recurso compartilhado é necessário, como:
- Gerenciadores de configuração, onde as configurações da aplicação precisam ser centralizadas.
- Pools de conexão a banco de dados, onde um número limitado de conexões deve ser gerenciado de forma eficiente.
- Loggers, onde a consistência dos registros é crucial.
Por que utilizar o Singleton?
Vou listar alguns pontos sobre a implementação desde Pattern que fazem mais sentido e também para mostrar que nem tudo são flores, alguns dos problemas que podemos ter com ele.
Vantagens
- Consistência Global: Garante que todos os pontos da aplicação utilizem a mesma instância, proporcionando consistência de dados e comportamento.
- Controle de Acesso: Centraliza o controle de criação e acesso à instância, facilitando a manutenção e o gerenciamento do ciclo de vida do objeto.
- Eficiência de Recursos: Evita a criação desnecessária de múltiplas instâncias, economizando recursos de memória e processamento.
Desvantagens
- Dificuldade de Testes: Singletons podem tornar a escrita de testes unitários mais difícil, pois introduzem estados globais que precisam ser gerenciados.
- Aumento do Acoplamento: O uso excessivo de Singletons pode levar a um acoplamento mais rígido entre componentes, dificultando a manutenção e evolução da aplicação.
Implementando uma Singleton
Para implementar uma singleton vou utilizar Golang. Nesta linguagem temos que ter uma atenção especial à concorrência para garantir que apenas uma instância seja criada, mesmo quando múltiplas goroutines tentam acessar a instância simultaneamente.
Para deixar nosso exemplo mais próximo do mundo real, vamos criar um Logger para nossa aplicação. Um logger é uma ferramenta comum em aplicações que precisa ser única para garantir a consistência dos logs.
1 - Definindo a estrutura
Primeiro, definimos a estrutura que queremos que tenha uma única instância.
package logger
import (
"fmt"
"sync"
)
type Logger struct {}
var loggerInstance *Logger
2 - Implementando a função NewInstance
A função NewInstance
é responsável por retornar a instância única da estrutura Singleton. Utilizamos um mutex para garantir a segurança em ambientes concorrentes, implementando a verificação dupla de bloqueio (double-checked locking) para eficiência.
package logger
import (
"fmt"
"sync"
)
type Logger struct{}
var logger *Logger
var mtx = &sync.Mutex{}
func NewInstance() *Logger {
if logger == nil {
mtx.Lock()
defer mtx.Unlock()
if logger == nil {
fmt.Println("Creating new Logger")
logger = &Logger{}
}
} else {
fmt.Println("Logger already created")
}
return logger
}
3 - Implementando os tipos de log
Uma ferramenta de Log sempre tem alguns tipos de log, como por exemplo Info para apenas mostrar as informações, Error para mostrar erros e assim por diante. É uma forma de filtrarmos também o tipo de informação que queremos mostrar em nossa aplicação.
Então vamos criar um método que irá mostrar nosso log com o tipo Info
. Para isso vamos criar uma função que receberá nossa mensagem de log e a formatará para o formato INFO.
package logger
import (
"fmt"
"sync"
"time"
)
const (
INFO string = "INFO"
)
type Logger struct{}
var logger *Logger
var mtx = &sync.Mutex{}
func NewInstance() *Logger {
if logger == nil {
mtx.Lock()
defer mtx.Unlock()
if logger == nil {
fmt.Println("Creating new logger")
logger = &Logger{}
}
} else {
fmt.Println("Logger already created")
}
return logger
}
func (l *Logger) Info(message string) {
fmt.Printf("%s - %s: %s\n", time.Now().UTC().Format(time.RFC3339Nano), INFO, message)
}
4 - Usando o Logger
E para utilizar nosso novo logger, vamos instancia-lo dentro do nosso package main e criar um log para ver como funciona essa implementação.
package main
import (
"playground-go/pkg/logger"
)
func main() {
log := logger.NewInstance()
log.Info("This is an example of log")
}
Esse é o resultado quando executamos o programa:
Creating new logger
2024-07-03T19:34:57.609599Z - INFO: This is an example of log
Se quisermos testar se o NewInstance
está realmente garantindo que apenas teremos uma instancia rodando, podemos fazer o seguinte teste.
package main
import (
"fmt"
"playground-go/pkg/logger"
)
func main() {
log := logger.NewInstance()
log.Info("This is an example of log")
log2 := logger.NewInstance()
log2.Info("This is another example of log")
if log == log2 {
fmt.Println("same instance")
} else {
fmt.Println("different instance")
}
}
Nossos logs mudaram e agora podemos ver que bloqueamos a criação de uma nova instancia:
Creating new logger
2024-07-03T19:45:19.603783Z - INFO: This is an example of log
Logger already created
2024-07-03T19:45:19.603793Z - INFO: This is another example of log
same instance
Conclusão
O padrão Singleton é uma ferramenta poderosa para garantir que apenas uma instância de uma classe específica exista durante o tempo de execução da aplicação. No exemplo do logger, vimos como esse padrão pode ser aplicado para garantir a consistência dos logs em toda a aplicação.
Espero que isso ajude você a entender melhor o Singleton em Golang.
Top comments (1)
Great write-up, we have a bunch of articles on Go in our Newsletter, check it out - packagemain.tech