Какие паттерны проектирования (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"))