Pull to refresh

Comments 23

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

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

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} выдаст ошибку

}

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


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

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

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

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


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

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

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

Сами разработчики 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).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • singleton

  • prototype

  • request

  • session

  • application

  • websocket

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

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

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

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

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

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

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

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

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

во-первых, просто надо привыкнуть, что в 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
}

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

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

Sign up to leave a comment.

Articles