company_banner

Внедрение зависимостей в Go

https://blog.drewolson.org/dependency-injection-in-go/
  • Перевод

Недавно я создал небольшой проект на языке Go. После нескольких лет работы с Java я был сильно удивлён тем, как вяло внедрение зависимостей (Dependency Injection, DI) применяется в экосистеме Go. Для своего проекта я решил использовать библиотеку dig от компании Uber, и она меня по-настоящему впечатлила.

Я обнаружил, что внедрение зависимостей позволяет решить множество проблем, с которыми я сталкивался в работе над Go-приложениями: злоупотребление функцией init и глобальными переменными, чрезмерная сложность настройки приложений и др.

В этой статье я расскажу об основах внедрения зависимостей, а также покажу пример приложения до и после применения этого механизма (посредством библиотеки dig).

Краткий обзор механизма внедрения зависимостей


Механизм DI предполагает, что зависимости предоставляются компонентам (struct в Go) при их создании извне. Это противопоставляется антипаттерну компонентов, которые сами формируют свои зависимости при инициализации. Давайте обратимся к примеру.

Предположим, у вас есть структура Server, которая требует Config для реализации своего поведения. Как один из вариантов, Server может создать собственную структуру Config во время инициализации.

type Server struct {
  config *Config
}

func New() *Server {
  return &Server{
    config: buildMyConfigSomehow(),
  }
}

Выглядит удобно. Вызывающему оператору не обязательно знать о том, что Server требует доступ к Config. Подобная информация скрыта от пользователя функции.

Однако здесь есть и недостатки. Прежде всего, если мы решим изменить функцию создания Config, то вместе с ней придётся менять все те места, которые вызывают соответствующий код. Предположим, что функция buildMyConfigSomehow теперь запрашивает аргумент. А значит, доступ к этому аргументу теперь нужен при любом вызове этой функции.

Кроме того, в такой ситуации будет трудно смоделировать структуру Config для её тестирования без учёта зависимостей. Чтобы тестировать создание Config с использованием произвольных данных (monkey-тестирование), нам придётся каким-то образом забраться в недра функции New.

А вот как решить эту задачу с помощью DI:

type Server struct {
  config *Config
}

func New(config *Config) *Server {
  return &Server{
    config: config,
  }
}

Теперь структуры Server и Config создаются отдельно друг от друга. Мы можем использовать любую подходящую логику для создания Config, а затем передать полученные данные в функцию New.

Кроме того, если Config является интерфейсом, то мы сможем с легкостью провести для него mock-тестирование. Любой аргумент, который позволяет реализовать наш интерфейс, мы можем передать в функцию New. Это упрощает тестирование структуры Server с помощью mock-объектов Config.

Основной недостаток этого подхода связан с необходимостью вручную создавать структуру Config, прежде чем мы сможем создать Server. Это очень неудобно. Здесь у нас появляется граф зависимостей: сначала нужно создавать структуру Config, потому что Server зависит от неё. В реальных приложениях такие графы могут слишком сильно разрастаться, что усложняет логику создания всех компонентов, необходимых для правильной работы приложения.

Ситуацию может исправить DI за счёт следующих двух механизмов:

  1. Механизм «предоставления» новых компонентов. Если коротко, он сообщает фреймворку DI, какие компоненты вам необходимы для создания объекта (ваши зависимости), а также как создать этот объект после получения всех нужных компонентов.
  2. Механизм «извлечения» созданных компонентов.

Фреймворк DI строит граф зависимостей на основе «поставщиков», о которых вы ему сообщаете, а затем определяет способ создания ваших объектов. Это сложно объяснить теоретически, поэтому давайте рассмотрим один относительно небольшой практический пример.

Пример приложения


В качестве примера давайте использовать код HTTP-сервера, который возвращает ответ в формате JSON, когда клиент делает запрос GET к /people. Мы будем рассматривать его по частям. Чтобы упростить этот пример, весь наш код будет находиться в одном пакете (main). В реальных приложениях Go так делать не следует. Полную версию кода из этого примера вы можете найти здесь.
Для начала обратимся к структуре Person. В ней не реализовано никакого поведения, только объявлены несколько тегов JSON.

type Person struct {                    
  Id   int    `json:"id"`
  Name string `json:"name"`                              
  Age  int    `json:"age"`                   
}  

В структуре Person есть теги Id, Name и Age. И всё.

А теперь посмотрим на Config. Как и у Person, у этой структуры нет никаких зависимостей. Однако, в отличие от Person, у неё есть конструктор.

type Config struct {               
  Enabled      bool         
  DatabasePath string        
  Port         string                       
}                                         
                     
func NewConfig() *Config {                            
  return &Config{            
    Enabled:      true,                           
    DatabasePath: "./example.db",       
    Port:         "8000",
  }                 
}   

Поле Enabled определяет, будет ли наше приложение возвращать реальные данные. Поле DatabasePath указывает путь к базе данных (мы используем SQlite). Поле Port задаёт порт, на котором будет выполняться наш сервер.

Для подключения к базе данных мы будем использовать следующую функцию. Она работает с Config и возвращает *sql.DB.

func ConnectDatabase(config *Config) (*sql.DB, error) {
  return sql.Open("sqlite3", config.DatabasePath)
} 

Теперь посмотрим на структуру PersonRepository. Она будет отвечать за извлечение информации о людях из нашей базы данных и её десериализацию по соответствующим структурам Person.

type PersonRepository struct {                           
  database *sql.DB                                              
}                                        
                                      
func (repository *PersonRepository) FindAll() []*Person {            
  rows, _ := repository.database.Query(
    `SELECT id, name, age FROM people;`
  )   
  defer rows.Close()                                           
                                                                                  
  people := []*Person{}                          
                                                       
  for rows.Next() {    
    var (               
      id   int      
      name string
      age  int              
    )                   
                                          
    rows.Scan(&id, &name, &age)         
                                           
    people = append(people, &Person{
      Id:   id,
      Name: name,
      Age:  age,
    })                 
  }                                         
                                          
  return people
}                                                     
                                   
func NewPersonRepository(database *sql.DB) *PersonRepository {
  return &PersonRepository{database: database}
}   

Структура PersonRepository требует подключения к базе данных. Она предоставляет лишь одну функцию — FindAll, которая использует это подключение для возвращения списка структур Person, соотносящихся с информацией в базе данных.

Нам понадобится структура PersonService, чтобы создать слой между HTTP-сервером и PersonRepository.

type PersonService struct {                                                          
  config     *Config                                                  
  repository *PersonRepository    
}                                                                                                        
                                                                  
func (service *PersonService) FindAll() []*Person {
  if service.config.Enabled {     
    return service.repository.FindAll()
  }                                                
                                                                      
  return []*Person{}               
}                                     
                                                                     
func NewPersonService(config *Config, repository *PersonRepository) *PersonService {
  return &PersonService{config: config, repository: repository}
}

PersonService зависит не только от Config, но и от PersonRepository. Она содержит функцию FindAll, которая условно вызывает PersonRepository, если приложение включено.

И наконец, структура Server. Она отвечает за выполнение HTTP-сервера и передачу соответствующих запросов в PersonService.

type Server struct {                                   
  config        *Config
  personService *PersonService
}                                     
                                   
func (s *Server) Handler() http.Handler {
  mux := http.NewServeMux()   
                                          
  mux.HandleFunc("/people", s.people)
                                           
  return mux                              
}                                
                                   
func (s *Server) Run() {     
  httpServer := &http.Server{
    Addr:    ":" + s.config.Port,
    Handler: s.Handler(),
  }            
                                                      
  httpServer.ListenAndServe()             
}                                    
                          
func (s *Server) people(w http.ResponseWriter, r *http.Request) {
  people := s.personService.FindAll()
  bytes, _ := json.Marshal(people)
                               
  w.Header().Set("Content-Type", "application/json")
  w.WriteHeader(http.StatusOK)          
  w.Write(bytes)               
}                                                  
                              
func NewServer(config *Config, service *PersonService) *Server {
  return &Server{   
    config:        config,
    personService: service,            
  }                                                                           
} 

Server зависит от структур PersonService и Config.
Итак, нам известны все компоненты. Так как же их теперь инициализировать и запустить нашу систему?

Великий и ужасный main()


Для начала давайте напишем функцию main() традиционным образом.

func main() {
  config := NewConfig()

  db, err := ConnectDatabase(config)

  if err != nil {
    panic(err)
  }

  personRepository := NewPersonRepository(db)

  personService := NewPersonService(config, personRepository)

  server := NewServer(config, personService)

  server.Run()
}

Сначала мы задаём структуру Config. Затем с её помощью создаём подключение к базе данных. После этого можно создать структуру PersonRepository, а на её основе — структуру PersonService. Наконец, мы используем всё это для создания и запуска Server.

Довольно сложный процесс. А что ещё хуже — по мере усложнения нашего приложения функция main будет становиться всё запутаннее. Каждый раз при добавлении зависимости к какому-либо из наших компонентов нам придётся дописывать логику и заново пересматривать функцию main.

Как вы могли догадаться, решить эту проблему можно с помощью механизма внедрения зависимостей. Давайте выясним, как этого добиться.

Создание контейнера


В рамках фреймворка DI «контейнеры» — это то место, куда вы добавляете «поставщиков» и откуда запрашиваете полностью готовые объекты. Библиотека dig предоставляет нам функции Provide и Invoke. Первая используется для добавления поставщиков, вторая — для извлечения полностью готовых объектов из контейнера.

Сначала создадим новый контейнер.

container := dig.New()

Теперь мы можем добавить поставщиков. Для этого вызовем функцию контейнера Provide. У неё один аргумент: функция, которая может иметь любое количество аргументов (они отражают зависимости создаваемого компонента), а также одно или два возвращаемых значения (предоставляемый функцией компонент и при необходимости ошибка).

container.Provide(func() *Config {
  return NewConfig()
})

Этот код сообщает: «Я предоставляю контейнеру тип Config. Для его создания мне больше ничего не требуется». Теперь, когда наш контейнер знает, как создавать тип Config, мы можем использовать его для создания других типов.

container.Provide(func(config *Config) (*sql.DB, error) {
  return ConnectDatabase(config)
})

Код сообщает: «Я предоставляю контейнеру тип *sql.DB. Для его создания мне необходим Config. Кроме того, при необходимости я могу вернуть ошибку».
В обоих случаях мы чересчур многословны. Так как у нас есть уже определённые функции NewConfig и ConnectDatabase, мы можем напрямую использовать их в качестве поставщиков для контейнера.

container.Provide(NewConfig)
container.Provide(ConnectDatabase)

Теперь можно попросить контейнер предоставить нам полностью готовый компонент любого из предложенных типов. Для этого мы используем функцию Invoke. Аргументом функции Invoke служит функция с любым количеством аргументов. В их качестве выступают типы, создать которые мы и просим наш контейнер.

container.Invoke(func(database *sql.DB) {
  
})

Контейнер выполняет действительно небанальные действия. Вот что происходит:

  • контейнер определяет, что нам нужен тип *sql.DB;
  • он выясняет, что данный тип предоставляет функция ConnectDatabase;
  • затем он определяет, что функция ConnectDatabase зависит от типа Config;
  • контейнер находит поставщика типа Config — функцию NewConfig;
  • у NewConfig нет никаких зависимостей, поэтому эту функцию можно вызвать;
  • полученный в результате работы функции NewConfig тип Config передаётся в функцию ConnectDatabase;
  • результат работы функции ConnectionDatabase, тип *sql.DB, возвращается к вызвавшему функцию Invoke.

Контейнер выполняет для нас целую гору работы, а на самом деле даже делает ещё больше. Он достаточно умён, чтобы создавать только один экземпляр каждого предоставленного типа. А это означает, что мы никогда случайно не создадим лишнее подключение к базе данных, если используем его в нескольких местах (например, в нескольких репозиториях).

Улучшенная версия main()


Теперь, когда мы знаем, как работает контейнер dig, давайте использовать его, чтобы оптимизировать функцию main.

func BuildContainer() *dig.Container {
  container := dig.New()

  container.Provide(NewConfig)
  container.Provide(ConnectDatabase)
  container.Provide(NewPersonRepository)
  container.Provide(NewPersonService)
  container.Provide(NewServer)

  return container
}

func main() {
  container := BuildContainer()

  err := container.Invoke(func(server *Server) {
    server.Run()
  })

  if err != nil {
    panic(err)
  }
}

Единственная вещь, с которой мы ещё не сталкивались, — возвращаемое функцией Invoke значение error. Если какой-либо из поставщиков, используемых функцией Invoke, возвращает ошибку, выполнение функции будет приостановлено, и эта ошибка будет возвращена вызывающему оператору.

Несмотря на небольшие размеры указанного примера, легко отметить преимущества этого подхода по сравнению со стандартным main. Чем больше становится наше приложение, тем очевиднее будут и эти преимущества.

Один из самых важных положительных моментов — разделение процессов создания компонентов и их зависимостей. Предположим, что нашему PersonRepository теперь необходим доступ к Config. Всё, что нам нужно сделать, — это добавить Config в качестве аргумента в конструктор NewPersonRepository. Никаких дополнительных изменений в коде не потребуется.

В числе других важных преимуществ можно назвать снижение числа используемых глобальных переменных и объектов, а также вызовов функции init (зависимости создаются лишь однажды, когда это необходимо, поэтому больше не нужно использовать подверженные ошибкам механизмы init). Кроме того, этот подход позволяет упростить тестирование отдельных компонентов. Представьте, что при тестировании вы создаёте контейнер и запрашиваете полностью готовый объект. Или что вам нужно создать объект с фиктивными реализациями всех его зависимостей (mock-объект). Всё это гораздо проще сделать с помощью механизма внедрения зависимостей.

Идея, достойная распространения


Я уверен, что механизм внедрения зависимостей позволяет создавать более надёжные приложения, которые к тому же проще тестировать. Чем больше приложение, тем сильнее это проявляется. Язык Go отлично подходит для создания больших приложений, а библиотека dig — прекрасный инструмент для внедрения зависимостей. Думаю, что сообществу программистов на Go следует обратить больше внимания на DI и чаще использовать этот механизм в приложениях.

FunCorp

102,00

Разработка развлекательных сервисов

Поделиться публикацией
Комментарии 89
  • НЛО прилетело и опубликовало эту надпись здесь
      +2
      по-моему, это ужасно… Ребята с Убера молодцы, конечно…

      по-моему #2: у меня складывается впечатление, что все это ради красивого тела main()…

      опыт шепчет: чем больше явного, тем лучше…

      а для проекта, средней сложности и объема, использование такого подхода приведет к тому, что для фикса мелочи незнакомым человеком, придется просмотреть и вникнуть во все щели кода и логики, абы ничего не упустить и не сломать.
        +3
        Имею опыт работы как с Go, так и с PHP (Laravel). Насколько красиво и удобно смотрится DI в Laravel, например, настолько неудобно и чужеродно подобный DI смотрится в Go. Понятно, что разная типизация и вот это все… Но хорошего DI для Go я так пока и не увидел.
        И согласен, лучше, если код более явный, если хорошей «магии» из-за особенностей языка не применить.
          0
          Он достаточно умён, чтобы создавать только один экземпляр каждого предоставленного типа

          Создаёт только синглтоны?

          Если я правильно понимаю со слов автора — DI в dig не совсем то же что IoC контейнер Laravel с его DI. Не в плане реализаций, а в плане для чего можно библиотеку использовать. И разработчики dig уточняют:
          Good for:
          Powering an application framework, e.g. Fx.
          Resolving the object graph during process startup.
          Bad for:
          Using in place of an application framework, e.g. Fx.
          Resolving dependencies after the process has already started.
          Exposing to user-land code as a Service Locator.


          Т. е. используйте для усиления возможностей фреймфорка либо для запуска приложения. Всё.
          В Laravel же куда не глянь — везде контейнер с его внедрением зависимостей.

          А на счёт магии… Она тоже разной бывает. И не всегда тёмной.
            0
            А можно пример того, что вы считаете красивым и удобным?
            +3

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


            Я все таки предпочитаю явно передавать и создавать зависимости, что может быть многословно, но зато обо всех косяках мне расскажет компилятор при сборке.

              0
              Как бы да, но для внедрения зависимостей, как бы, и нет.

              Все дело в том, что в Go внедрение зависимостей происходит на этапе инициализации приложения. То есть, с начала «всё подняли», а потом «бежим в рантайм». И если у Вас есть хотя бы 1 E2E-тест, то проблемы с паниками в механизме внедрения полностью пресекаются. И даже более того, поощряются, потому что это нормально хлопнуть приложение полностью на этапе инициализации, а бойлерплейт это уменьшает невообразимо.
              +3
              Чем больше приложение, тем сильнее это проявляется. Язык Go отлично подходит для создания больших приложений, а библиотека dig — прекрасный инструмент для внедрения зависимостей.

              Мне казалось, что как раз потому, что go плохо подходит для написания больших приложений, то внедрение зависимостей предлагается делать через сервисную архитектуру.

                +3
                199х год, язык C… неожиданно оказывается новостью и DI…
                  +5
                  Господи, какой ужас. Простая, явная и прозрачная инициализация сервиса заменяется на неявную магическую хрень, абсолютно не читаемую и подверженную ошибкам при исполнении.

                  Все таки Java головного мозга очень тяжело поддается лечению.
                    +1

                    Это просто пример такой. А когда таких "простых и явных" мест будет десятки и изменение в одном месте надо будет дублировать в остальных — вот тут и придет нужна в DI. А еще DI нужен для абстрактных развязок, что бы была возможность гибко настраивать абстракции.

                      +4
                      Не надо гибко настраивать абстракции. Они протекать начнут.

                      Go создавался как простой до идиотизма язык, где очень тяжело выстрелить себе в ногу. Не надо делать из него Джаву, ничего хорошего не получится.

                      Нигде, кроме Джавы, DI широко не используются. И ничего, живут как то люди, и неплохо живут.
                        +1
                        Не надо гибко настраивать абстракции. Они протекать начнут.

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


                        Почему-то программисты, которые любят рассказывать про "дырявые" абстракции не предлагают альтернативу, так как ее обычно нет.


                        Под абстракцией я имел ввиду, что если у вас компонент A зависит от компонента B, то его стоит сделать зависимым только от абстрактного компонента asbtract_B, для которого B будет имплементацией. И это стоит делать как минимум для того, что бы разделить эти компоненты и дать возможность менять их независимо. Могу отправить вас почитать "Чистую архитектуру", поверьте, стоит потратить на это немного времени.


                        Go создавался как простой до идиотизма язык, где очень тяжело выстрелить себе в ногу. Не надо делать из него Джаву, ничего хорошего не получится.

                        И именно поэтому в комплекте с Go вам предлагают пользоватся микросервисами, что бы прокладывать храницы на уровне API спецификаций. Я нахожу это несколько ироничным, потому что для написания крупных приложений приходится управлять кучей микросервисами с оверхедами на сетевые запросы вместо вызовов функций, и плюсы микросервисной архитектуры не всегда это окупают, даже если получится их сохранить.


                        Нигде, кроме Джавы, DI широко не используются. И ничего, живут как то люди, и неплохо живут.

                        Даже в C#? Ой, вряд ли. А так, используется много где, в тех же rails или django, когда вам дают динамический cursor к базе данных — это тоже DI.

                          +3
                          Не обижайтесь, но вам абсолютно верно указали.
                          Описанный подход лично я считаю вредительским и явным оверинжинирингом.
                          Вместо того чтобы сделать нормальные абстракции поверх интерфейсов, а ещё лучше вообще их не делать, пока не возникло ВНЕЗАПНО НЕОБХОДИМОСТИ поменять реализацию коннекта к базе или ещё чего-то такого (как по мне, это все равно невозможно сделать таким способом irl)
                          Посыл то правильный — надо писать нормальный тестируемый код с нормальными абстракциями и разделением ответственности, но реализация просто чудовищная
                            0
                            Вместо того чтобы сделать нормальные абстракции поверх интерфейсов, а ещё лучше вообще их не делать, пока не возникло ВНЕЗАПНО НЕОБХОДИМОСТИ поменять реализацию коннекта к базе или ещё чего-то такого


                            Золотые слова! Не надо заранее изобретать абстракцию, которая с вероятностью 95% никогда не понадобится!
                              0

                              Как бы, именно в этом проблема. В том, что эти 5%, которые вы надеетесь сделать потом, вы не сделаете.


                              Если вы завяжетесь на конкретную реализацию, а окажется, что выбор был не удачным — отказаться от нее будет сложно.
                              И ладно, заменить конкретную реализацию в коде абстрактной, если она хорошо изолирована, но выпилить конкретный компонент или отказаться от технологии, если она пронизывает весь ваш код, а не отсечена абстракцией практически нереально.

                                0
                                У нас есть класс который пронизывает весь код, мы делаем из класса интерфейс, и все, компилятор сам покажет где надо поменять одну релизацию на другую.
                                  0

                                  Если вы опираетесь на какую-то технологию, то скорее всего:


                                  1. Таких классов у вас довольно много
                                  2. Код, вместо использования абстракций, которые бы можно было подложить использует особенности технологии, на которую вы завязались.

                                  Поэтому вы прикидываете, что надо будет переписать и перепроверить весь код приложения и грустно вздыхая откладываете это. В той же "чистой архитектуре" есть отличный пример, когда они сильно завязались на SQL диалект одной базы, а потом, когда попытались перейти на другую из-за того, что поддержка первой закончилась — столкнулись с тем, что они не могут это сделать не переписав все приложение. В итоге, бизнес нанял специальных людей, что бы они поддерживали эту БД.


                                  Понятное дело, если у вас, скажем, правильная микросервисная архитектура, то вы можете довольно свободно менять базы данных для приложения, но микросервисная архитектура не для всех, а уж правильная — тем более.

                                    +1
                                    Код, вместо использования абстракций, которые бы можно было подложить использует особенности технологии, на которую вы завязались.
                                    Т.е. вместо того чтобы использовать все преимушества технологии, надо ограничить себя абстракцией
                                    когда попытались перейти на другую из-за того, что поддержка первой закончилась — столкнулись с тем, что они не могут это сделать не переписав все приложение.
                                    этого можно избежать если делать многослойную архитектуру, т.е. надо переписать только нижнии слой, а где надо исправить покажут тесты
                                      0
                                      Т.е. вместо того чтобы использовать все преимушества технологии, надо ограничить себя абстракцией

                                      Если вы на 100% уверены, что вам никогда не надо будет сменить эту технологию, то зачем? Например, вроде как почти все программисты стараются на 100% использовать возможности языка программирования. А если же не уверены, то стоит задуматься, потому что, например, те же базы данных очень часто выбираются чисто по инерции.


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

                                      Так разве в этом случае верхние слои не будут работать с абстракцией как раз?

                                        0
                                        Если вы на 100% уверены, что вам никогда не надо будет сменить эту технологию, то зачем?
                                        Может быть обычно меняют технологию те кто абстрагируются, и не используют все преимушества технологии? Если надо переходить на другую технологию, значит она работает иначе, т.е есть возможности которых нет в абстракции, что тогда делать?
                                        Так разве в этом случае верхние слои не будут работать с абстракцией как раз?
                                        Нет, сервис как работал с `ProductsRepository.list` так и работает.
                                          0
                                          Может быть обычно меняют технологию те кто абстрагируются, и не используют все преимушества технологии? Если надо переходить на другую технологию, значит она работает иначе, т.е есть вожможности которых нет в абстракции, что тогда?

                                          Скорее всего, тут стоит исходит из конкретной причины перехода. Например, если вы мигрируете с базы, в которой нет индекса нужного вам типа на другую, сильно ли это отразится на абстракциях? Мне кажется, не очень, максимум, надо будет добавить какой-нибудь fallback_index_type.


                                          Нет, сервис как работал с ProductsRepository.list так и работает.

                                          Возможно я не совсем вас понимаю, но разве ProductsRepository.list не абстракция над тем, где и как у вас реально лежат данные?

                                            0
                                            Например, если вы мигрируете с базы, в которой нет индекса нужного вам типа на другую, сильно ли это отразится на абстракциях? Мне кажется, не очень, максимум, надо будет добавить какой-нибудь fallback_index_type.
                                            Да, но другая база может не работать как старая в других местах, где опять надо вносить изменения, а если работате как старая то и старый код должен работать
                                            Возможно я не совсем вас понимаю, но разве ProductsRepository.list не абстракция над тем, где и как у вас реально лежат данные?
                                            list это статический метод в классе ProductsRepository, если для вас это абстракция, ок пусть будет так, но тогда код без абстракции может быть только если у вас один метод
                                              0
                                              Да, но другая база может не работать как старая в других местах, где опять надо вносить изменения, а если работате как старая то и старый код должен работать

                                              Если у вас нормальная абстракция, то большинства таких проблем получится избежать. Разумеется, если вы меняете одну технологию на кардинальную другую, то написать качественную абстракцию довольно сложно, но для тех же реляционных баз данных абстракции в виде ORM пишутся довольно легко и миграция с, к примеру, mysql на postgres может пройти довольно бесшовно. Но вы же не будете утверждать, что они работают одинаково?


                                              Другой вариант, это когда программный код восполняет недостаток функционала у технологии. Например, в memcached нельзя хранить списки, но ваша абстракция позволяет это делать упаковывая их в memcached неким способом.

                                                0
                                                но для тех же реляционных баз данных абстракции в виде ORM пишутся довольно легко и миграция с, к примеру, mysql на postgres может пройти довольно бесшовно. Но вы же не будете утверждать, что они работают одинаково?
                                                В postgres есть масивы, в mysql нет, т.е. можно использовать либо весь функционал без абстракции, либо урезаный функционал но с абстракциями, на случай когда надо поменять базу, что случается очень редко

                                                  0
                                                  что случается очень редко

                                                  Это если мы говорим про такие швейцарские ножи как postgres и mysql.


                                                  А необходимости менять скажем, одно kv на другое или одну nosql документную базу на другую возникает чаще.


                                                  Из того, что случалось со мной за не очень долгую карьеру:


                                                  • Надо было заменить чем-то redis, так как у него нет внятного master-master
                                                  • Надо уходить от использования rethinkdb, потому что его поддержка уже совсем скурвилась.

                                                  В первом случае, абстракция над kv делается довольн легко, второй случай это только pet проект, но и там все получается относительно неплохо.

                                                    0
                                                    Или эмулировать масссивы для MySQL через сериализованные поля. Собственно задача перейти на Postgre может возникнуть, когда єта ємуляция станет очень дорогой, дороже плюсов мускуля.
                                        0
                                        В той же «чистой архитектуре» есть отличный пример, когда они сильно завязались на SQL диалект одной базы, а потом, когда попытались перейти на другую из-за того, что поддержка первой закончилась — столкнулись с тем, что они не могут это сделать не переписав все приложени

                                        Да, но это пример другой проблемы. Здесь надо только абстрагировать интерфейс и реализацию работы с БД. При этом у вас вполне может по-прежнему быть иерархическая структура без DI.
                                          0
                                          То есть в main() вы будете сначала настраивать низкоуровневые компоненты, а потом вручную связывать их с высокоуровневыми?

                                          В зависимости от размера приложения, скоро или нет вам захочется автоматизации.
                                            0
                                            Почему в main? По месту использования.

                                            В зависимости от размера приложения, скоро или нет вам захочется автоматизации.

                                            На здоровье. Я лишь сказал о том, что приведенный выше пример из книги — о другой проблеме.
                                              0

                                              Ну, конкретно мой коммент был ответом на отрицание полезности всех абстракций в духе "Не надо гибко настраивать абстракции. Они протекать начнут."

                                                0
                                                Я не против абстракций вообще. Я против преждевременной оптимизации. Я против того, что бы реализовать решение до того, как возникла проблема. А если она вообще не возникнет? А если она будет совсем другой, чем казалось разработчику (что чаще всего и бывает в жизни)?

                                                А конкретно про DI я просто не понимаю, какую проблему он пытается решить. Есть стойкое ощущение, что такой проблемы вообще нет.
                                                  0
                                                  Я не против абстракций вообще. Я против преждевременной оптимизации. Я против того, что бы реализовать решение до того, как возникла проблема. А если она вообще не возникнет? А если она будет совсем другой, чем казалось разработчику (что чаще всего и бывает в жизни)?

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


                                                  В случае с абстракциями, если вы не сделаете это в самом начале, потом это будет делать безумно сложнее и скорее всего, вы просто будете жить с вашим решением весь проект.


                                                  Основная идея абстракций — это сокрыть детали реализации определенного компонента, например, компонента для построения отчетов, что бы ваш код не зависел от конкретной реализации этого компонента. Если же вы решите завязаться на конкретную реализацию, то сложность ее выпиливания будет экспоненциально возрастать по мере развития проекта и довольно быстро достигнет точки, в которой компонент станет очень хрупким и вокруг него будут плодится всякие workaround'ы.


                                                  А конкретно про DI я просто не понимаю, какую проблему он пытается решить. Есть стойкое ощущение, что такой проблемы вообще нет.

                                                  Некоторой аналогией в DI можно назвать service discovery в микросервисной архитектуре. Не будете же вы отрицать, что в ней без service discovery никуда?


                                                  DI решает проблему инициализации и связывания различных компонентов вашего приложения, когда у вас их много и связывать их вручную в коде много мороки. Вместо этого вы описываете создание экземпляров этих компонентов, а они уже автоматически распихиваются в те места, где нужны.


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

                                                    0
                                                    Абстракция, скорытие — это тоже оптимизация. Минимизация количества связей с конкретной реализацией.
                                                    0
                                                    DI решает проблему слишком большого количества ответственностей. Грубо, он снимает с вашего класса ответственность за создание своих зависимостей, перекладывая её на внешний мир.
                                  +1
                                  Под абстракцией я имел ввиду, что если у вас компонент A зависит от компонента B, то его стоит сделать зависимым только от абстрактного компонента asbtract_B, для которого B будет имплементацией.


                                  Для этого в Go достаточно описать интерфейс. И не надо никаких провайдеров, никаких контейнеров, никакого, не дай бог, динамического связывания. Go статически компилируемый язык и все зависимости должен проверять на этапе компиляции, а не в рантайме.

                                  А так, используется много где, в тех же rails или django, когда вам дают динамический cursor к базе данных — это тоже DI.


                                  Где именно в django используется «динамический cursor к базе данных» и почему это является DI?
                                    –1
                                    Для этого в Go достаточно описать интерфейс. И не надо никаких провайдеров, никаких контейнеров, никакого, не дай бог, динамического связывания. Go статически компилируемый язык и все зависимости должен проверять на этапе компиляции, а не в рантайме.

                                    1. В момент инициализации приложения, а не просто в рантайме, все-таки есть небольшая разница.
                                    2. А как вы будете реализовывать в таких случаях конфигурационные файлы для выбора конкретной реализации интерфейса? Все равно читать в рантайме конфиг и подтягивать правильную реализацию.

                                    Где именно в django используется «динамический cursor к базе данных» и почему это является DI?

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


                                    DATABASES = {
                                        'default': {
                                            'NAME': 'app_data',
                                            'ENGINE': 'django.db.backends.postgresql',
                                            'USER': 'postgres_user',
                                            'PASSWORD': 's3krit'
                                        },
                                        'users': {
                                            'NAME': 'user_data',
                                            'ENGINE': 'django.db.backends.mysql',
                                            'USER': 'mysql_user',
                                            'PASSWORD': 'priv4te'
                                        }
                                    }

                                    И потом работаете с этой базой не меняя больше ничего в коде (разумеется, если вы использовали совместимые поля в моделях). Это значит, что каждая модель получает доступ к необходимому коннекту к базе данных. Как по мне, вполне похоже на DI.

                                      +1
                                      А по мне, так обыкновенный конфиг. Причем тут DI?
                                        0
                                        А что тогда DI?
                                          –1
                                          Ну апофеоз DI это Spring.
                                          Когда зависимости указываются в аннотациях (не входящих в стандарт языка), а сами модули загружаются (или не загружаются, хе-хе) на лету, путем поиска джарников по всему classpath
                                            0
                                            Мне кажется вы переносите свой личный опыт касательно конкретной реализации (вероятно не самый приятный опыт) на весьма общий архитектурный паттерн.

                                            Spring != DI.
                                          0
                                          Параметры подключения — это зависимость класса подключения. Либо он их хранит в себе, либо они в него инжектятся снаружи. В случае конфига — снаружи в общем случае.
                                          0
                                          А как вы будете реализовывать в таких случаях конфигурационные файлы для выбора конкретной реализации интерфейса? Все равно читать в рантайме конфиг и подтягивать правильную реализацию.


                                          Нет конечно. Я явно напишу нужную реализацию интефейса в Go коде
                                            0

                                            И что бы изменить поведение приложения, нужно будет его пересобрать?

                                              0
                                              да. Ровно так же, как в приведенном примере с django — там это был не внешний конфиг файл, а исполняемый python файл.
                                                0

                                                Ну, django конфиг файл в реальной жизни я пишу так (с дефолтными значениями, но не суть):


                                                DATABASES = {
                                                    'default': {
                                                        'NAME': os.environ.get('DATABASE_NAME'),
                                                        'ENGINE': os.environ.get('DATABASE_ENGINE'),
                                                        'USER': os.environ.get('DATABASE_USER),
                                                        'PASSWORD': os.environ.get('DATABASE_PASSWORD')
                                                    }
                                                }

                                                А значит, для того, что бы поменять базу мне не нужно менять код — я просто настраиваю приложение. А у вас настройка приложения только через пересборку кода — опять же, подход разумный только для мелких приложений.

                                                  0
                                                  А не страшно с таким хрупким кодом жить в продакшене?
                                                  А если вы забыли указать в env переменную DATABASE_NAME что с приложением будет? А если ее на лету админ поменял?

                                                    0
                                                    А не страшно с таким хрупким кодом жить в продакшене?

                                                    А по вашему, в код надо зашивать все до паролей от бд, что бы вдруг что, ничего нельзя было поменять?)


                                                    А если вы забыли указать в env переменную DATABASE_NAME что с приложением будет?

                                                    Дефолты, я же не копировал конфиг, писать их в комментарии мне было лень.


                                                    А если ее на лету админ поменял?

                                                    А что должно произойти? Ну да, приложение переключится на другую базу.

                                                0
                                                Хм, я, наверное, что-то не понимаю. Go гордится тем, что в результате сборки получается статически собранный бинарник. Где тут место для IOC/DI не на этапе компиляции? А если на этапе компиляции, тогда и шаблоны C++ — это такой DI. И из осмысленной концепции получается пустословие какое-то.
                                                  0

                                                  В go находится место даже для динамической типизации через interface{}, я думаю, место какому-то минимальному


                                                  if cute {
                                                  a := CuteLogic{}
                                                  } else {
                                                  a := UglyLogic{}
                                                  }

                                                  Найдется.

                                                    0
                                                    Вы уверены, что не путаете динамическую типизацию с динамической компоновкой?
                                                      0

                                                      Хм, возможно, я не совсем вас понял, но разве затирание типа через приведение к interface{} и потом приведение обратно к типу не считается попыткой получения динамической типизации?


                                                      А пример я привел просто того, как можно изменять поведение собранной программы. Так же можно и реализовать то, для чего нужно DI.

                                                        0
                                                        Мой комментарий был о том, что DI — это механизм времени компоновки, а динамическая типизация — скорее времени компиляции. То, о чем статья — скорее относится к IOC, чем к DI. Например, инстанциация шаблона конкретным типом — это может быть IOC. И инициализация переменной типа интерфейс конкретным типом тоже. А вот подстановка нужного типа на этапе загрузки и выполнения программы — уже DI. Естественно, имхо.
                                        0
                                        В PHP широко используется.
                                          +1
                                          Ну да, давайте еще у PHP передовые концепции перенимать. Как будто нам Java мало.
                                            0
                                            Ну конечно нет, зачем смотреть на чужой опыт. Лучше все делать самим, с нуля, не так как у всех ;) Учиться исключительно на своих ошибках и т.п.
                                              +1

                                              Это и есть go-way)

                                          0
                                          В шарпе и джаваскрипте ещё используются.
                                      0

                                      Гораздо проще и прозрачнее было бы поместить в структуру Server все нужные интерфейсы, создавать во время запуска приложения конкретные экземпляры, такой же построчно похожий симпатичный код получится. Зато любой сходу поймет!

                                      –1
                                      Предположим, что функция buildMyConfigSomehow теперь запрашивает аргумент. А значит, доступ к этому аргументу теперь нужен при любом вызове этой функции.

                                      Не обсуждая саму библиотеку, вопрос чисто по логике.
                                      Но почему просто не создать функцию getMyConfig, которая никогда не будет запрашивать аргументов, но будет знать как вызывать buildMyConfigSomehow?
                                      var config *Config
                                      
                                      func GetConfig() *Config {
                                      	if config == nil {
                                      		config = NewConfig(param1)
                                      	}
                                      	return config
                                      }

                                        +1
                                        Не знаток Go, но разве в нём глобальные переменные не считаются злом по умолчанию?
                                          –1
                                          Автор выше реализовал синглтон. Переменная config в целом не глобальна, т.к. начинается не с Заглавной буквы, и видима только внутри какого-то своего пакета.
                                            +1
                                            Переменная config в целом не глобальна

                                            Вполне себе глобальная приватная (скрытая) переменная.
                                        +2
                                        У меня сложилось впечатление, что все пытаются критиковать данный подход из-за, якобы, переусложнения и неявности. Но почему-то все забывают что, во-первых, это игрушечный пример, в котором затраты на создание и обслуживание контейнера только начинают быть сопоставимы и пользой от него. В реальных приложениях у вас будут десятки классов и структур и если не использовать DI, main превратится к огромных кусок нечитаемого и неподдерживаемого кода. Данный пример призван просто продемонстрировать концепцию, если бы он был сложнее, тем, кто впервые сталкнулся с понятием DI, было бы неимоверно сложно понять в чем же его суть. Во-вторых, я в корне не согласен с тем, что DI, распространен только в Java. Приведу, возможно, неожиданные примеры из фронтенда: DI — основная архитектурная идея Angular, с другой стороны React Context — своего рода DI для распространения данных и функционала между компонентами (а возможно и других компонентов). В-третьих, неужели ни у кого из комментаторов не возникало потребности иметь в системе две имплементации схожего функционала? И дело может быть не в таких фундаментальных сущностях как база данных, а, например, в алгоритме расчета налога в зависимости от страны или других персонализированных алгоритмах. DI — это зрелая и сложившаяся концепция которая показывает свою практическую полезность на протяжении уже долго времени. Довольно странно слышать, что это оверинженириг или глупое усложнение. Это техника, которая может показаться избыточной на «короткой дистанции», но если у вас действительно сложный коммерческий продукт, без DI вы просто в определенный момент начнете строить диаграммы зависимостей на бумажке, в то время как DI дает возможность абстрагироваться от полной картины зависимостей и переложить рутинную работу на подходящий для этого инструмент.
                                          –1
                                          В реальных приложениях у вас будут десятки классов и структур и если не использовать DI, main превратится к огромных кусок нечитаемого и неподдерживаемого кода.


                                          Во-первых, в Go нет классов, что не может не радовать.

                                          Во-вторых, структур конечно может быть очень много, но больших сервисов, которые запускаются из main, вряд ли будет больше 3-5. Если это не так, то у вас проблемы с архитектурой, и надо разбивать монолит на более мелкие компоненты.

                                          я в корне не согласен с тем, что DI, распространен только в Java. Приведу, возможно, неожиданные примеры из фронтенда: DI — основная архитектурная идея Angular


                                          Очень характерный пример. Angular — это идеологически таже Java, только поверх JS
                                            0
                                            Во-первых, в Go нет классов, что не может не радовать.
                                            А какие отличия между «классом» и «структурой»?

                                            структур конечно может быть очень много, но больших сервисов, которые запускаются из main, вряд ли будет больше 3-5.
                                            Ну это уж совсем мальенькое приложеньице. Для таких всякие DI ясен пень будут жутким оверкилом.

                                            Очень характерный пример. Angular — это идеологически таже Java, только поверх JS
                                            Простите, но WAT?
                                              0
                                              А какие отличия между «классом» и «структурой»?


                                              В структуре нет методов, только данные.
                                              А также нет наследования, полиморфизма, да и инкапсуляции честно говоря практически нет.
                                              Это просто данные, слегка структурированные.
                                                0
                                                class User {
                                                    String name;
                                                    int age;
                                                }
                                                
                                                  0
                                                  Это к чему?
                                                    +1
                                                    Класс может быть без методов. А в Go мы можем добавить методы структуре.
                                                  0
                                                  В структуре нет методов, только данные.

                                                  Зато есть квазинаследование, которое называется встраивание. Так что методы в структуре тоже есть, просто объявляются в другом месте.

                                                    0
                                                    Встраивание, это ни разу не наследование. Это композиция.

                                                      +1

                                                      Это квазинаследование, а не композиция, потому что допустим такой код.


                                                      Была бы чистая композиция, так бы нельзя было делать)

                                                    0
                                                    Чем будет отличаться от метода поле структуры, которому присвоено значение функционального типа?
                                                      0
                                                      В структуре нет методов, только данные.
                                                      Мы все еще про Go говорим?
                                                        0
                                                        Это уже чисто терминологический спор.
                                                        С некоторых пор кто-то решил, что в структуру можно добавлять методы и классификация поехала в /dev/null
                                                      +3
                                                      Во-вторых, структур конечно может быть очень много, но больших сервисов, которые запускаются из main, вряд ли будет больше 3-5. Если это не так, то у вас проблемы с архитектурой, и надо разбивать монолит на более мелкие компоненты.

                                                      Я нахожу довольно ироничным, что для того, что бы избежать DI, вы предлагаете тот же самый DI на основе микросервисной архитектуры, только еще более оверхедный и запутанный.

                                                        0
                                                        > но больших сервисов, которые запускаются из main, вряд ли будет больше 3-5. Если это не так, то у вас проблемы с архитектурой, и надо разбивать монолит на более мелкие компоненты.

                                                        Кому надо? Какие проблемы? Много сервисов? Это не проблема само по себе: если нужно 50 сервисов бизнесу, то ему всё равно будут они в одном монолите или разбиты на 50 микросервисов. Скорее даже выберет монолит при прочих равных, поскольку накладные расходы на эксплуатацию заведомо меньше

                                                        Микросервисы — это не решение проблемы больошого количества сервисов в монолите, это решение других проблем типа независимого масштабирования или необходимости останавливать все сервисы когда реально нужно только один стопнуть.
                                                          0
                                                          Это не проблема само по себе: если нужно 50 сервисов бизнесу, то ему всё равно будут они в одном монолите или разбиты на 50 микросервисов.


                                                          Бизнесу наверно все равно, а вот разработчику совсем нет. Я с трудом могу представить себе разработчика, который сможет удержать в голове одновременно 50 сервисов на go-рутинах, которые общаются между собой по многочисленным каналам (и не словит при этом deadlock). Логично разделить эти 50 сервисов на более крупные блоки функциональности для борьбы с когнитивной сложностью.
                                                            0
                                                            Кто сказал, что общаются? Они могут быть разделены, не иметь общих данных, по крайней мере на запись, разве что какой-то роутер решает какой из 49 дернуть.
                                                        +1
                                                        DI — основная архитектурная идея Angular


                                                        к нам, недавно, в компанию, пришел новый программист, который сделал попытку замутить DI на основе npm, а задача стояла — создать xml-парсер для чтения конкретного источника с данными которого должны работать сотрудники компании… вместо написания обычного класса на рнр нативными средствами для разбора хмл, было принято стягивать кучу зависимостей ради модного подхода и поставить под угрозу нормальное обновление лайва, жизнь которого зародилась, когда в рнр еще не было namespace'ов…

                                                        так и крутится в деве, и сотрудники по ссилке делают нужные импорты…

                                                        а вообще…

                                                        main превратится к огромных кусок нечитаемого и неподдерживаемого кода


                                                        main — должна задать вектор куда идти, и далее — все разветвляется по коду и пакетах, если Ваша main больше 100 строк кода — значить с архитектурой программы чтото не так…
                                                        0
                                                        Каждый язык программирования имеет свою идиоматику. Программисты, которые переходят с одного языка на другой, часто приносят с собой идиоматику старого языка и пытаются запинать ее грязными ногами в новый язык. Ничего хорошего из этого обычно не получается. Рождаются нежизнеспособные монстры, которых очень тяжело читать и поддерживать. (А не забываем. что поддержка — это 90% жизненного цикла любого успешного программного продукта)

                                                        Не надо так делать. Надо расслабится, сдаться, и принять идиоматику нового языка. Это гораздо более продуктивно. Но конечно нелегко морально. Инерция мышления — очень сильная штука.

                                                          –1
                                                          Язык это не что-то неподвижное и перманентное.
                                                          Появляются новые идеи, решения, подходы. Выходят новые версии языка (внезапно!).
                                                          Если не смотреть иногда по сторонам (ну так, на всякий случай), можно оказаться в уютной но глубокой яме.

                                                          Как, например, наболевшая проблема с зависимостями. Можно конечно сказать «фу всяким go dep, это не идеоматично». А можно посмотреть как сделано у соседей и перенять лучшие идеи.
                                                            0
                                                            Смотреть по сторонам очень полезно, но применять это лучше отталкиваясь от базовой идиоматики, не поперек идеологии языка.
                                                              0
                                                              А что, кто-то разве говорит иначе и спорит?

                                                              Как по мне, но вы высказываетесь немного аггресивно.
                                                              Программисты, которые переходят с одного языка на другой, часто приносят с собой идиоматику старого языка и пытаются запинать ее грязными ногами в новый язык.
                                                              Т.е. предлагают применить давно проработанный и обкатанный архитектурный паттерн. Применяемый во многих языках. Да, реализация получилась весьма далекой от совершенства. Да, она не идеоматична… Но может лучше разобраться что и как, попытаться сделать ее идеоматичной, адаптировать… Нежели нужно сразу опускаться до язвлений в стиле «грязные ноги»?
                                                            +1

                                                            Все это хорошо и красиво звучит, пока не сталкиваешься с проблемой, которая идиоматикой не решается нормально, потому что идиоматика просто не предусмотрела её решения.


                                                            И ладно бы это был бы какой-то rocket science, так нет же — обычная рутина, решенная уже не раз в других языках. Ты при этом как бы и не против идиоматики — спрашиваешь у опытных, и в итоге получаешь: "это не идиоматично, это тебе не нужно" (с)… ну зашибись теперь… а проблема типа "сама как-нибудь чудом там решится… может быть… ну или пропадёт… может быть".
                                                            Следующий этап — пытаешься решить сам (велосипед, ага) идиоматичными инструментами. И так, и эдак, крутишься, вертишься, но постоянно при этом теряешь другие плюшки языка — то здравствуй interface{}, и пока type safety, то горы бойлерплейта писанного руками и потому абсолютно неподдерживаемого.
                                                            Дальше до тебя начинает доходить осознание, что во всех других языках эта проблема решается именно так, потому что есть некие фундаментальные требования/ограничения, решения которых идиоматикой Go просто тупо не предусмотрено. И инерция мышления тут вообще ни причем, сколько не бейся, и сколько не разгоняй своё мышление. И начинается финальный акт, когда ты "замазываешь" дыры в дизайне Go банальной кодогенерацией, для которой, между прочим, тоже не предусмотрено никакого стандартного тулчейна/подхода/инструмента, и каждый кодогенерирует как умеет.
                                                            В результате, ты часто не пишешь код, но пишешь код, который генерирует код, причем на каких-то костылях. Что никак не user friendly, но вменяемой альтернативы — просто нет.


                                                            Чтобы не быть голословным — вот задачка: сделайте реализацию канала в Go, который при полном заполнении буфера не блокирует последующую попытку записи, ожидая чтения (стандартное поведение буферизированного канала в Go), но выкидывает самое старое значение и принимает новое, не блокируя пишущего в канал. А после того как реализуете и отладите (особенно доставят проблемы с утечкой ресурсов при окончании работы с каналом), сделайте так, чтобы эту реализацию можно было использовать для произвольного типа, желательно включая базовые, без необходимости ваять всякие обертки (бойлерплейт, ага), при этом сохраняя type safety (проверку типов на этапе компиляции).
                                                            Аргументы "type safety не нужно, interface{} рулит" и "вам подобные каналы не нужны"(с) не принимаются, ибо, во-первых, использовать interface{} вроде как не идиоматично, и вполне себе дорого (тайпкасты недешевые), да и гарантии на стадии компиляции никогда не лишние, а, во-вторых, подобные каналы нужны на практике.


                                                            Go имеет много плюсов, и во многом хорош, но и недостатки, как и конкретные проблемы, у этого языка есть. Отрицать их наличие, ссылаясь на идиоматику и инерцию мышления, — это тупо закрывать глаза на проблемы. А запинывать решения грязными сапогами в Go приходится потому, что он, как оказывается достаточно часто, просто не предлагает никакого решения взамен.

                                                            +1

                                                            Начали про безумный страшный DI, а теперь уже скатились к дженерикам :-)

                                                              +3
                                                              Закон ГО — по мере разрастания дискуссии про Golang, вероятность обсуждения дженериков, стремится к единице.

                                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                            Самое читаемое