Давайте начнем с определения из Википедии.

“Одиночка (англ. 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)


  1. falconandy
    04.12.2023 13:04
    +9

    Можно же сделать сам тип Singleton неэкспортируемым, добавить экспортируемый интерфейс с нужными функциями, а в GetInstance() поменять тип результата на этот интерфейс.


    1. Mavolio-Bent
      04.12.2023 13:04
      +1

      Можно даже интерфейс при большом желании не делать! Вот так можно сделать:

      package singleton
      
      import "sync"
      
      
      
      type singleton struct {
      	Id int 
      }
      
      var (
      
      	instance *singleton
      
      	once     sync.Once
      
      )
      
      func GetInstance() *singleton {
      	once.Do(func() {
      
      		instance = &singleton{1}
      
      	})
      
      	return instance
      }
      package main
      
      import (
      	"fmt"
      	"go-singleton/singleton"
      )
      
      func main() {
      
      	// first object
      
      	s1 := singleton.GetInstance()
      	fmt.Printf("type: %T, value: %v, ptr: %p\n", s1, s1, s1)
      	s1.Id = 2
      	fmt.Printf("type: %T, value: %v, ptr: %p\n", s1, s1, s1)
          // s2 = singleton.singleton{1} выдаст ошибку
      
      }

      Конечно, интерфейс понадобится чтобы взаимодействовать с полями и вообще иметь о них представление, но все же.


      Да и зачем искать ООП паттерн в не-ООП языке...


      1. falconandy
        04.12.2023 13:04
        +1

        Можно, но линтеры будут ругаться примерно так: Exported function with the unexported return type


        1. Mavolio-Bent
          04.12.2023 13:04

          Ну это да, но я же и не ставил целью показать идиоматический и кошерный способ :)


      1. Arsen1972 Автор
        04.12.2023 13:04

        .


      1. Arsen1972 Автор
        04.12.2023 13:04
        +1

        Думаю Go таки ООП-шный язык. Аргументы:
        - Наследование
        Есть инструмент interface. Не забываем, что главное в наследовании отношение 'is'.
        "Наследование реализует отношение 'is' ". Можно же с другой стороны: если есть отношение 'is' то это наследование.
        Допустим структура А реализует интерфейс I. Это же означает что A is I. Иначе как бы мы, например, использовали параметр типа I и передавали в него A ? Разве это не один из принципов SOLID ?
        - Полиморфизм
        Реализуем интерфейс I у двух типов A и B. И получаем чистый динамический полиморфизм. Создаем слайс типа I, складываем в него A и B. запускаем на каждом елементе слайса переопределенную функцию. Поведение будет отличаться на разных типах (A или B).
        - Инкапсуляция
        Здесь вообще полный порядок.


        Все принципы ООП реализованы. Как же Go не ООП-шный ?

        А конкретная реализация ООП да, непривычная. Писать методы вне пользовательского типа неожиданно. Но от этого они не перестают быть обычными методами с полным и управляемым разделением на приватные и публичные. И не становятся ничем другим, кроме как методами по сути.

        Алан Кэй считает реализацию ООП в С++ не такой, как он ее себе создал. Это же не говорит о том, что С++ не ООП-шный язык.


        1. Mavolio-Bent
          04.12.2023 13:04
          +1

          Сами разработчики Go говорят, что их язык не то, чтобы ООП. Да, в нем есть черты, присущие ООП, как вы обозначили, полиморфиизм возможен, своего рода наследования тоже возможно. Но это, скорее делает Go языкоп OOП-like, а не полноценно ООП языком. Вот цитата с сайта Go

          Although Go has types and methods and allows an
          object-oriented style of programming, there is no type hierarchy.
          The concept of “interface” in Go provides a different approach that
          we believe is easy to use and in some ways more general. There are
          also ways to embed types in other types to provide something
          analogous—but not identical—to subclassing.
          Moreover, methods in Go are more general than in C++ or Java:
          they can be defined for any sort of data, even built-in types such
          as plain, “unboxed” integers.
          They are not restricted to structs (classes).


      1. Sanchous98
        04.12.2023 13:04

        Все равно через разыменование указателя вы создадите копию объекта, так что интерфейс нужен. Хотя вариант с unsafe и reflect все сломает


  1. MaxPro33
    04.12.2023 13:04
    +2

    В вашем примере кода вы используете функцию GetInstance(), однако в тесте видно, что также возможны другие способы создания экземпляров структуры Singleton. В C++, например, существует более строгий контроль над созданием экземпляров класса.

    Мой вопрос заключается в следующем: почему в Go не реализован более строгий контроль над созданием экземпляров типа, например, как в приведенном вами примере на C++? Не считаете ли вы, что использование словесных правил (например, "используйте GetInstance()") является менее надежным подходом, чем более строгий контроль, предоставляемый языками с более жесткой системой типов?


    1. Arsen1972 Автор
      04.12.2023 13:04

      Именно об этом и написано в статье. О неправильности подхода "используйте GetInstance()". О контроле над созданием объектов.


    1. Kahelman
      04.12.2023 13:04
      -2

      Яны предложил посмотреть на истоки и причины появления синглетонов в С++

      В этом языке не определён порядок инициализации статических объектов.

      Поэтому пришлось городить огород с классом синглетон, поскольку тут гарантированно с к моменту вызова метода класса, объект будет создан. В с#, go это решается созданием глобальной статической переменной. ( про статическую в go могу наврать, можно создать константу )но смысл тот же.


  1. rsashka
    04.12.2023 13:04

    гарантирующий, что в однопоточном приложении будет единственный экземпляр некоторого класса

    Я понимаю, что это цитата с вики и конкретная реализация Singleton`а может быть потокобезопасной или нет, но как шаблон проектирования связан с многопоточностью? И почему только для однопоточных? В многопоточном приложении это как-то по другому называется?


    1. Arsen1972 Автор
      04.12.2023 13:04

      Кратко конкретно по этой реализации. Обратите внимание на once.Do()
      Но в статье речь не о потокобезопасности, а о том, что удачно сформулировал
      MaxPro33 "в Go не реализован более строгий контроль над созданием экземпляров типа"


    1. sshikov
      04.12.2023 13:04

      Если вы посмотрите еще раз, то в английской вики ничего нет про однопоточность.


      1. rsashka
        04.12.2023 13:04

        Если вы посмотрите еще раз на статью, то увидите, что цитата автора из русской вики, в которой и присутствует это сомнительное уточнение.


        1. sshikov
          04.12.2023 13:04
          +1

          Я именно это сразу сделал (заглянул в обе). Отсюда и комментарий, что слова про однопоточность - изобретение исключительно автора русской вики.

          Совершенно не понимаю, при чем тут однопоточность. Синглтон - про гарантию единственности экземпляра чего-либо, в определенном контексте. Если в программе много потоков - им могут быть нужны как экземпляр чего-то на поток, так и общий экземпляр чего-то другого на все потоки. Обе гарантии единственности вполне имеют смысл.

          Более того, скажем spring называет нечто похожее scope, и гарантирует, что некий бин существует один на приложение, один на запрос, или один на какой-то другой scope из вот такого списка:

          • singleton

          • prototype

          • request

          • session

          • application

          • websocket

          Это вполне осмысленное обобщение сиглтона. А синглтон только на поток - это какое-то странное ограничение.


  1. GospodinKolhoznik
    04.12.2023 13:04
    -1

    единственный экземпляр некоторого класса

    Т.к. в go нет классов (т.е. множество классов в go пустое), следовательно любой ответ на вопрос "есть ли singleton а golang?" будет истинным. Потому что для любого элемента, принадлежащего пустому множеству, любое утверждение является истинным.


    1. Arsen1972 Автор
      04.12.2023 13:04
      +2

      Давайте использовать термин не "класс" или "структура", а просто "пользовательский тип данных".

      Думаете что если использовать термин "структуру", то что-то меняется?
      Написать Singleton на С++ для структуры? )
      Это я к тому, что не в словах дело.


      1. GospodinKolhoznik
        04.12.2023 13:04
        -1

        Давайте, я только за. Ваша претензия не ко мне, а к автору определения, у которого свет клином сошелся на ООП-шных классах.


  1. tarandro
    04.12.2023 13:04
    -1

    А почему в GO конструктор структура публичная, а в C++ конструктор приватный?
    А что будет, если в C++ сделать конструктор тоже публичным?

    (если что, я знаю ответ, просто код GO не эквивалентен коду C++)


  1. itmind
    04.12.2023 13:04
    +2

    Пакеты в Go можно рассматривать как "классы". Пакет имет приватные и публичные переменные, приватные и публичные функции, а так же конструктор ( init ). Таким образом Пакет можно рассматривать как Singleton. Подключаем его например к main пакету, инициализируем в нем внутренние переменные и далее это один экземпляр на все приложение, т.к. подключение пакета к другим пакетам не вызывает его "пересоздание".


  1. 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
    }


    1. Arsen1972 Автор
      04.12.2023 13:04

      Для пустой структуры (type singleton struct{}) всегда создается один объект, или точнее не создается новый. Вообще без использования GetInstance()

      s2 := *s1
      адрес у обоих объектов один и тот же.
      Как только появляется поле - ситуация меняется. Даже если это поле вообще не используется.
      После тестирования обоих вариантов для структуры с полем определенной в отдельном пакете копирование продолжает создавать новый объект.
      Прямое создание с singleton{} конечно не видится.
      А просто переменную var _id int можно менять и без ресивера. Обычной func SetId(id int) {_id = id }