![Одиночка](/images/patterns/cards/singleton-mini.png?id=914e1565dfdf15f240e766163bd303ec)
Одиночка на Go
Одиночка — это порождающий паттерн, который гарантирует существование только одного объекта определённого класса, а также позволяет достучаться до этого объекта из любого места программы.
Одиночка имеет такие же преимущества и недостатки, что и глобальные переменные. Его невероятно удобно использовать, но он нарушает модульность вашего кода.
Вы не сможете просто взять и использовать класс, зависящий от одиночки в другой программе. Для этого придётся эмулировать присутствие одиночки и там. Чаще всего эта проблема проявляется при написании юнит-тестов.
Концептуальный пример
Обычно, экземпляр одиночки создается во время начальной инициализации структуры. Для этого, мы определяем для структуры метод getInstance
. Этот метод будет создавать и возвращать экземпляры одиночки. После создания первого экземпляра, при каждом вызове метода getInstance
будет возвращаться именно он.
А что касательно потоков goroutine? Структура одиночки должна возвращать один и тот же экземпляр в случаях, когда разные потоки пытаются получить доступ к этому экземпляру. По этой причине можно легко ошибиться и неправильно реализовать паттерн Одиночка. Пример ниже показывает, как правильно создать Одиночку.
Некоторые важные детали, о которых нужно помнить:
-
В начале нужна
nil
-проверка, с ее помощью мы убеждаемся, что первый экземплярsingleInstance
— пустой. Благодаря этому мы можем избежать ресурсоемких операций блокировки при каждом вызовеgetInstance
. Если эта проверка не пройдена, тогда полеsingleInstance
уже заполнено. -
Структура
singleInstance
создается внутри блокировки. -
После блокировки используется еще одна
nil
-проверка. В случаях, когда первую проверку проходит более одного потока, вторая обеспечивает создание экземпляра одиночки единым потоком. В противном случае, все потоки создавали бы свои экземпляры структуры одиночки.
single.go: Одиночка
package main
import (
"fmt"
"sync"
)
var lock = &sync.Mutex{}
type single struct {
}
var singleInstance *single
func getInstance() *single {
if singleInstance == nil {
lock.Lock()
defer lock.Unlock()
if singleInstance == nil {
fmt.Println("Creating single instance now.")
singleInstance = &single{}
} else {
fmt.Println("Single instance already created.")
}
} else {
fmt.Println("Single instance already created.")
}
return singleInstance
}
main.go: Клиентский код
package main
import (
"fmt"
)
func main() {
for i := 0; i < 30; i++ {
go getInstance()
}
// Scanln is similar to Scan, but stops scanning at a newline and
// after the final item there must be a newline or EOF.
fmt.Scanln()
}
output.txt: Результат выполнения
Creating single instance now.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Другой пример
Существуют и другие методы создания экземпляра одиночки в Go:
- Функция
init
Мы можем создавать экземпляр одиночки внутри функции init
. Это возможно только в тех случаях, когда ранняя инициализация экземпляра не является проблемой. Функция init
вызывается единожды для каждого файла в пакете, поэтому мы можем быть уверенны в том, что будет создан только один экземпляр.
-
sync.Once
sync.Once
выполнит операцию лишь один раз. Смотрите код ниже:
syncOnce.go: Одиночка
package main
import (
"fmt"
"sync"
)
var once sync.Once
type single struct {
}
var singleInstance *single
func getInstance() *single {
if singleInstance == nil {
once.Do(
func() {
fmt.Println("Creating single instance now.")
singleInstance = &single{}
})
} else {
fmt.Println("Single instance already created.")
}
return singleInstance
}
main.go: Клиентский код
package main
import (
"fmt"
)
func main() {
for i := 0; i < 30; i++ {
go getInstance()
}
// Scanln is similar to Scan, but stops scanning at a newline and
// after the final item there must be a newline or EOF.
fmt.Scanln()
}
output.txt: Результат выполнения
Creating single instance now.
Single instance already created.
Single instance already created.