Какие паттерны проектирования (GoF и другие) часто применяются в Go? Приведите примеры.

Ответ

В Go, благодаря его особенностям (интерфейсы, функции первого класса, горутины и каналы), классические паттерны GoF часто реализуются иначе или заменяются более идиоматичными подходами. Однако многие концепции остаются актуальными.

Классические паттерны

  1. Фабричный метод (Factory Method)

    • Описание: Создание объектов через специальную функцию, а не напрямую через конструктор. Это позволяет скрыть сложную логику инициализации.
    • Пример:
      // NewFileLogger и NewConsoleLogger — это фабричные функции
      func NewLogger(logType string) (Logger, error) {
      switch logType {
      case "file":
          return NewFileLogger("/var/log/app.log"), nil
      case "console":
          return NewConsoleLogger(), nil
      default:
          return nil, fmt.Errorf("unknown logger type: %s", logType)
      }
      }
  2. Одиночка (Singleton)

    • Описание: Гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему. В Go идиоматично реализуется с помощью sync.Once.
    • Пример:
      
      type singleton struct{}

    var ( instance *singleton once sync.Once )

    func GetInstance() *singleton { once.Do(func() { instance = &singleton{} }) return instance }

  3. Декоратор (Decorator)

    • Описание: Динамически добавляет объекту новую функциональность, «оборачивая» его. В Go легко реализуется через встраивание (embedding) интерфейсов.
    • Пример: Логгирование запросов к HTTP-хендлеру.
      
      func LoggingMiddleware(next http.Handler) http.Handler {
      return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
          log.Printf("Request: %s %s", r.Method, r.URL.Path)
          next.ServeHTTP(w, r)
      })
      }

    // Использование: http.Handle("/", LoggingMiddleware(myHandler))

  4. Стратегия (Strategy)

    • Описание: Определяет семейство схожих алгоритмов и помещает каждый из них в собственный класс, после чего алгоритмы можно взаимозаменять. В Go это достигается через интерфейсы.
    • Пример:
      
      type Sorter interface {
      Sort(data []int)
      }

    type BubbleSort struct{} func (b BubbleSort) Sort(data []int) { / ... / }

    type QuickSort struct{} func (q QuickSort) Sort(data []int) { / ... / }

    // Контекст, который использует стратегию type Context struct { sorter Sorter }

    func (c *Context) DoSort(data []int) { c.sorter.Sort(data) }

Go-идиоматичные паттерны

  1. Наблюдатель (Observer) через каналы

    • Описание: Вместо сложных интерфейсов и списков подписчиков, в Go для рассылки событий используются каналы. Это проще и безопаснее с точки зрения конкурентности.
    • Пример:
      
      // Издатель рассылает события в канал
      func publisher(ch chan<- string) {
      for i := 0; i < 5; i++ {
          ch <- fmt.Sprintf("Event %d", i)
          time.Sleep(time.Second)
      }
      close(ch)
      }

    // Подписчик читает из канала func subscriber(ch <-chan string) { for event := range ch { log.Println("Received:", event) } }

  2. Функциональные опции (Functional Options)

    • Описание: Паттерн для создания сложных объектов с множеством необязательных параметров. Он более гибок, чем использование структуры с настройками.
    • Пример:
      
      type Server struct {
      Addr    string
      Port    int
      Timeout time.Duration
      }

    type Option func(*Server)

    func WithAddr(addr string) Option { return func(s *Server) { s.Addr = addr } }

    func WithPort(port int) Option { return func(s *Server) { s.Port = port } }

    func NewServer(opts ...Option) Server { // Значения по умолчанию srv := &Server{Addr: "localhost", Port: 8080, Timeout: time.Second 30} // Применяем опции for _, opt := range opts { opt(srv) } return srv }

    // Использование: server := NewServer(WithPort(9000), WithAddr("0.0.0.0"))

Ответ 18+ 🔞

Смотри, вот тебе про паттерны в Go, но без этой академической зауми. Тут всё как в жизни — либо берёшь классику и перекраиваешь под местные нравы, либо придумываешь своё, потому что старые подходы тут просто не влезают, блядь.

Классика, но в штанах из джинсы

  1. Фабричный метод (Factory Method)

    • Суть: Не пались с конструкторами напрямую, а спрячь эту хуйню в функцию. Чтобы потом не орать "а почему у меня тут nil, сука?", когда что-то пошло не так.
    • Как выглядит:
      // NewFileLogger и NewConsoleLogger — это и есть наши фабрики, ёпта
      func NewLogger(logType string) (Logger, error) {
      switch logType {
      case "file":
          return NewFileLogger("/var/log/app.log"), nil
      case "console":
          return NewConsoleLogger(), nil
      default:
          return nil, fmt.Errorf("unknown logger type: %s", logType) // Чёт не то ввели — получай ошибку, а не панику
      }
      }
  2. Одиночка (Singleton)

    • Суть: Чтобы этот ваш объект был один, как хуй в бане, на весь проект. В Go для этого есть sync.Once — штука, которая делает что-то один раз и потом уже ни в какую.
    • Смотри сюда:
      
      type singleton struct{}

    var ( instance *singleton once sync.Once // Главный по тарелочкам, блядь )

    func GetInstance() *singleton { once.Do(func() { // Эта хуйня сработает ровно один раз, даже если с десяти горутин дернуть instance = &singleton{} }) return instance }

  3. Декоратор (Decorator)

    • Суть: Надо добавить функциональность? Просто оберни, как портянкой! В Go это часто мидлвари для HTTP.
    • Пример: Хочешь логировать запросы? На, заверни хендлер!
      
      func LoggingMiddleware(next http.Handler) http.Handler {
      return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
          log.Printf("Запрос пришёл: %s %s", r.Method, r.URL.Path) // Сначала залогировали
          next.ServeHTTP(w, r) // А потом уже отдали дальше, по назначению
      })
      }

    // Используем: http.Handle("/", LoggingMiddleware(myHandler)) // И всё, хендлер теперь в логирующей обёртке

  4. Стратегия (Strategy)

    • Суть: Много алгоритмов, а выбрать нужно один. Засовываем каждый в свою структуру, но заставляем реализовывать один интерфейс. Красота!
    • Вот:
      
      type Sorter interface {
      Sort(data []int) // Контракт, блядь. Хочешь быть сортировщиком — сортируй.
      }

    type BubbleSort struct{} // Пузырёк, медленный, но свой func (b BubbleSort) Sort(data []int) { / ... / }

    type QuickSort struct{} // Быстрая, хитрая жопа func (q QuickSort) Sort(data []int) { / ... / }

    // А это наш главный, который будет решать, кого сегодня вызывать type Context struct { sorter Sorter }

    func (c *Context) DoSort(data []int) { c.sorter.Sort(data) // А кто там внутри — ему похуй, главное чтобы метод был }

А это уже наше, родное, Go-шное

  1. Наблюдатель (Observer) через каналы

    • Суть: Забудь про эти ваши списки подписчиков и интерфейсы с Update(). В Го события — это каналы. Просто, как три копейки, и конкурентно безопасно, ёпта.
    • Смотри, как просто:
      
      // Издатель, который шлёт события в канал
      func publisher(ch chan<- string) {
      for i := 0; i < 5; i++ {
          ch <- fmt.Sprintf("Событие %d", i) // Отправил
          time.Sleep(time.Second)
      }
      close(ch) // Всё, больше писем не будет, закрываем лавочку
      }

    // Подписчик, который жрёт события из канала func subscriber(ch <-chan string) { for event := range ch { // Читает, пока канал не закроют log.Println("Получил:", event) } }

  2. Функциональные опции (Functional Options)

    • Суть: Это пиздец какой удобный паттерн, когда у тебя объект с кучей настроек, и половина из них необязательные. Вместо того чтобы городить конструктор с двадцатью параметрами или передавать Config-структуру, ты просто накидываешь опций. Красота, ядрёна вошь!
    • Вот как жизнь налаживается:
      
      type Server struct {
      Addr    string
      Port    int
      Timeout time.Duration
      }

    // Опция — это просто функция, которая тыкает в сервер type Option func(*Server)

    func WithAddr(addr string) Option { return func(s *Server) { s.Addr = addr } }

    func WithPort(port int) Option { return func(s *Server) { s.Port = port } }

    func NewServer(opts ...Option) Server { // Сначала дефолтные настройки srv := &Server{Addr: "localhost", Port: 8080, Timeout: time.Second 30} // А теперь прогоняем все переданные опции for _, opt := range opts { opt(srv) // Каждая опция меняет что-то в сервере } return srv }

    // Использование — просто песня: server := NewServer(WithPort(9000), WithAddr("0.0.0.0")) // Хочу на другом порту и слушать все интерфейсы!

Вот и вся магия. В Go часто проще и надёжнее использовать его родные механизмы — горутины, каналы, интерфейсы и функции первого класса, чем пытаться впихнуть классические ООП-паттерны в лоб. Получается идиоматичнее и без лишнего геморроя.