Давайте начнем с определения из Википедии.
“Одиночка (англ. Singleton) — порождающий шаблон проектирования, гарантирующий, что в однопоточном приложении будет единственный экземпляр некоторого класса, и предоставляющий глобальную точку доступа к этому экземпляру.”
“Единственный экземпляр некоторого класса” означает что нет возможности написать код, в котором объект может быть скопирован или создан еще каким-либо способом.
В этом посте про "гарантирующий".
Рассмотрим общеизвестную реализацию. Поле в структуре - для лучшей визуализации результата.
type Singleton struct {
id int
}
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{1}
})
return instance
}
Протестируем его.
func main() {
// first object
s1 := GetInstance()
fmt.Printf("type: %T, value: %v, ptr: %p\n", s1, s1, s1)
// second object. Copying
s2 := *s1
s2.id = 2
fmt.Printf("type: %T, value: %v, ptr: %p\n", s2, s2, &s2)
// third object
s3 := new(Singleton)
s3.id = 3
fmt.Printf("type: %T, value: %v, ptr: %p\n", s3, s3, s3)
// many different objects
for i := 4; i < 8; i++ {
s := Singleton{i}
fmt.Printf("type: %T, value: %v, ptr: %p\n", s, s, &s)
}
}
Результат:
type: *main.Singleton, value: &{1}, ptr: 0xc0000b4000
type: main.Singleton, value: {2}, ptr: 0xc0000b4018
type: *main.Singleton, value: &{3}, ptr: 0xc0000b4020
type: main.Singleton, value: {4}, ptr: 0xc0000b4030
type: main.Singleton, value: {5}, ptr: 0xc0000b4038
type: main.Singleton, value: {6}, ptr: 0xc0000b4040
type: main.Singleton, value: {7}, ptr: 0xc0000b4048
Вывод: создано неограниченное количество объектов!
Немного рассуждений.
Почему-то реализация паттерна в Go указывает программисту пользоваться функцией GetInstance().
А как следует из определения паттерна это должно быть не словесное правило, а сама реализация типа не должна позволять создать более одного экземпляра.
Если использовать словесные правила для программистов, то реализация паттерна могла бы быть и такой: "Не создавайте более одного экземпляра." И код писать не надо. )
Сравним с реализацией на языке С++.
Все конструкторы и оператор копирования или спрятаны в private
секции, или удалены. Или сразу оба способа.
В секции public
только один метод - GetInstance()
. Нет никаких вариантов создать более одного объекта.
class Singlеton
{
public:
static Singleton& GetInstance()
{
static Singleton theSingleInstance;
return theSingleInstance;
}
private:
Singleton(){}
Singleton(const Singleton& root) = delete;
Singleton& operator=(const Singleton&) = delete;
};
Совсем другая картина в Go.
В тесте видно, что легитимными способами возможен не «единственный экземпляр некоторого класса».
«Для создания объекта используйте GetInstance()» — слабый аргумент. Программист может не знать что этот тип должен быть Singleton‑ом и создать много экземпляров другими способами. А паттерн как раз и нужен для того, чтоб гарантированно исключить «не один экземпляр» в любом случае.
Комментарии (23)
MaxPro33
04.12.2023 13:04+2В вашем примере кода вы используете функцию
GetInstance()
, однако в тесте видно, что также возможны другие способы создания экземпляров структуры Singleton. В C++, например, существует более строгий контроль над созданием экземпляров класса.Мой вопрос заключается в следующем: почему в Go не реализован более строгий контроль над созданием экземпляров типа, например, как в приведенном вами примере на C++? Не считаете ли вы, что использование словесных правил (например, "используйте GetInstance()") является менее надежным подходом, чем более строгий контроль, предоставляемый языками с более жесткой системой типов?
Arsen1972 Автор
04.12.2023 13:04Именно об этом и написано в статье. О неправильности подхода "используйте GetInstance()". О контроле над созданием объектов.
Kahelman
04.12.2023 13:04-2Яны предложил посмотреть на истоки и причины появления синглетонов в С++
В этом языке не определён порядок инициализации статических объектов.
Поэтому пришлось городить огород с классом синглетон, поскольку тут гарантированно с к моменту вызова метода класса, объект будет создан. В с#, go это решается созданием глобальной статической переменной. ( про статическую в go могу наврать, можно создать константу )но смысл тот же.
rsashka
04.12.2023 13:04гарантирующий, что в однопоточном приложении будет единственный экземпляр некоторого класса
Я понимаю, что это цитата с вики и конкретная реализация Singleton`а может быть потокобезопасной или нет, но как шаблон проектирования связан с многопоточностью? И почему только для однопоточных? В многопоточном приложении это как-то по другому называется?
sshikov
04.12.2023 13:04Если вы посмотрите еще раз, то в английской вики ничего нет про однопоточность.
rsashka
04.12.2023 13:04Если вы посмотрите еще раз на статью, то увидите, что цитата автора из русской вики, в которой и присутствует это сомнительное уточнение.
sshikov
04.12.2023 13:04+1Я именно это сразу сделал (заглянул в обе). Отсюда и комментарий, что слова про однопоточность - изобретение исключительно автора русской вики.
Совершенно не понимаю, при чем тут однопоточность. Синглтон - про гарантию единственности экземпляра чего-либо, в определенном контексте. Если в программе много потоков - им могут быть нужны как экземпляр чего-то на поток, так и общий экземпляр чего-то другого на все потоки. Обе гарантии единственности вполне имеют смысл.
Более того, скажем spring называет нечто похожее scope, и гарантирует, что некий бин существует один на приложение, один на запрос, или один на какой-то другой scope из вот такого списка:
singleton
prototype
request
session
application
websocket
Это вполне осмысленное обобщение сиглтона. А синглтон только на поток - это какое-то странное ограничение.
GospodinKolhoznik
04.12.2023 13:04-1единственный экземпляр некоторого класса
Т.к. в go нет классов (т.е. множество классов в go пустое), следовательно любой ответ на вопрос "есть ли singleton а golang?" будет истинным. Потому что для любого элемента, принадлежащего пустому множеству, любое утверждение является истинным.
Arsen1972 Автор
04.12.2023 13:04+2Давайте использовать термин не "класс" или "структура", а просто "пользовательский тип данных".
Думаете что если использовать термин "структуру", то что-то меняется?
Написать Singleton на С++ для структуры? )
Это я к тому, что не в словах дело.GospodinKolhoznik
04.12.2023 13:04-1Давайте, я только за. Ваша претензия не ко мне, а к автору определения, у которого свет клином сошелся на ООП-шных классах.
tarandro
04.12.2023 13:04-1А почему в GO
конструкторструктура публичная, а в C++ конструктор приватный?
А что будет, если в C++ сделать конструктор тоже публичным?(если что, я знаю ответ, просто код GO не эквивалентен коду C++)
itmind
04.12.2023 13:04+2Пакеты в Go можно рассматривать как "классы". Пакет имет приватные и публичные переменные, приватные и публичные функции, а так же конструктор ( init ). Таким образом Пакет можно рассматривать как Singleton. Подключаем его например к main пакету, инициализируем в нем внутренние переменные и далее это один экземпляр на все приложение, т.к. подключение пакета к другим пакетам не вызывает его "пересоздание".
amkartashov
04.12.2023 13:04во-первых, просто надо привыкнуть, что в Go так принято. Если вам надо чтобы пользователь пакета использовал только конструктор, а не инициализировал объект напрямую - пишите это в документации к пакету. Например всякие логгеры - никому и в голову не придёт инстанциировать руками
zap.Logger
во-вторых, если так уж нужно защититься от самозлыхбуратин разработчиков, то в качестве Singleton используемstruct{}
а всю логику зашиваем в методы, которые обращаются к глобальным данным внутри пакета. Можно плодить сколько угодно объектов, но они будут работать с одними и теми же данными.package singleton type Singleton struct{} var ( _id int ) func (Singleton) Get_id() int { return _id } func (Singleton) Set_id(id int) { _id = id }
Либо как выше предлагает itmind https://habr.com/ru/articles/778378/#comment_26230338 - весь пакет использовать как singleton:
package singleton var ( _id int ) func Get_id() int { return _id } func Set_id(id int) { _id = id }
Arsen1972 Автор
04.12.2023 13:04Для пустой структуры (
type singleton struct{}
) всегда создается один объект, или точнее не создается новый. Вообще без использованияGetInstance()
s2 := *s1
адрес у обоих объектов один и тот же.
Как только появляется поле - ситуация меняется. Даже если это поле вообще не используется.
После тестирования обоих вариантов для структуры с полем определенной в отдельном пакете копирование продолжает создавать новый объект.
Прямое создание с singleton{} конечно не видится.
А просто переменнуюvar _id int можно менять и без ресивера. Обычной func SetId(id int) {_id = id }
falconandy
Можно же сделать сам тип Singleton неэкспортируемым, добавить экспортируемый интерфейс с нужными функциями, а в GetInstance() поменять тип результата на этот интерфейс.
Mavolio-Bent
Можно даже интерфейс при большом желании не делать! Вот так можно сделать:
Конечно, интерфейс понадобится чтобы взаимодействовать с полями и вообще иметь о них представление, но все же.
Да и зачем искать ООП паттерн в не-ООП языке...
falconandy
Можно, но линтеры будут ругаться примерно так: Exported function with the unexported return type
Mavolio-Bent
Ну это да, но я же и не ставил целью показать идиоматический и кошерный способ :)
Arsen1972 Автор
.
Arsen1972 Автор
Думаю Go таки ООП-шный язык. Аргументы:
- Наследование
Есть инструмент interface. Не забываем, что главное в наследовании отношение 'is'.
"Наследование реализует отношение 'is' ". Можно же с другой стороны: если есть отношение 'is' то это наследование.
Допустим структура А реализует интерфейс I. Это же означает что A is I. Иначе как бы мы, например, использовали параметр типа I и передавали в него A ? Разве это не один из принципов SOLID ?
- Полиморфизм
Реализуем интерфейс I у двух типов A и B. И получаем чистый динамический полиморфизм. Создаем слайс типа I, складываем в него A и B. запускаем на каждом елементе слайса переопределенную функцию. Поведение будет отличаться на разных типах (A или B).
- Инкапсуляция
Здесь вообще полный порядок.
Все принципы ООП реализованы. Как же Go не ООП-шный ?
А конкретная реализация ООП да, непривычная. Писать методы вне пользовательского типа неожиданно. Но от этого они не перестают быть обычными методами с полным и управляемым разделением на приватные и публичные. И не становятся ничем другим, кроме как методами по сути.
Алан Кэй считает реализацию ООП в С++ не такой, как он ее себе создал. Это же не говорит о том, что С++ не ООП-шный язык.
Mavolio-Bent
Сами разработчики Go говорят, что их язык не то, чтобы ООП. Да, в нем есть черты, присущие ООП, как вы обозначили, полиморфиизм возможен, своего рода наследования тоже возможно. Но это, скорее делает Go языкоп OOП-like, а не полноценно ООП языком. Вот цитата с сайта Go
Sanchous98
Все равно через разыменование указателя вы создадите копию объекта, так что интерфейс нужен. Хотя вариант с unsafe и reflect все сломает