Два года с Go

Сейчас подвожу итоги где-то 2-х лет разработки серии проектов на Go и довольно приятно удивляюсь тому, насколько хорошую защиту от дурака имеет Go по-умолчанию. Выстрелить себе в ногу просто невероятно сложно и в общем случае разработчик может просто сконцентрироваться на задаче не думая про UB, особенности move semantic и кучу других вещей, которые постоянно должны быть в голове у C++ разработчика.

Если отбросить очень сильное упрощение языка и попытаться выделить какие же именно решения позволили сильно упростить работу, то я бы отметил два: 1) отсутствие классов и наследования, 2) запрет на циклические импорты.

Классы в целом и наследование в частности являются довольно распространенной причиной сильного усложнения кода на ровном месте. Что можно считать разумной глубиной иерархии объектов, хороша ли идея ромбовидного наследования, должен ли квадрат наследоваться от прямоугольника или прямоугольник от квадрата, нарушен ли принцип разделения интерфейсов в данной реализации класса можно спорить довольно долго. Любители развести срач и потешить своё ЧСВ наличием единственно правильного ответа даже любят задавать подобные вопросы на собеседовании… А ведь распутать этот клубок можно довольно просто и кардинально, достаточно полностью убрать наследование и добавить интерфейсы к типам! Данное решение невероятно упрощает кодовую базу и позволяют получить код удовлетворяющий SOLID относительно просто.

Защита от дурака была бы не полной без второй гениальной особенности Go – запрет на циклические импорты между пакетами. Идея проста и гениальна: если пакет A импортирует пакет B, то пакет B не может импортировать пакет A, при этом понятие пакета идентично Python – любая директория в проекте является самостоятельным пакетом. Важна эта особенность в первую очередь тем, что связать компоненты из разных пакетов напрямую больше не представляется возможным и разработчик начинает в добровольно-принудительном порядке думать про интерфейсы. И тут мы опять приходим к коду, который всё ближе и ближе к выполнению принципов описанных в SOLID без серьезных трудозатрат.

Но может ли быть все исключительно хорошо? Конечно нет, и в Go сохранилась возможность нагадить самому себе. Что интересно, это можно сделать самым что ни на есть классическим способом – через глобальные статические переменные и функции! Проблема с глобальными переменными в Go стоит чуть менее остро по сравнению с тем же C++, так как заранее известен порядок их инициализации (см. детальное описание тут), тем не менее возможностей наделать глупостей это сильно не уменьшает. Проблема с глобальными переменными комбинированная, в Go можно не только создать глобальную статическую переменную, но и написать функцию init(), которая будет вызвана после создания статических объектов пакета. И вот в init() уже можно развернуться по полной и, к примеру, обратиться к глобальной переменной и/или функции за пределами своего пакета (пока нет циклической зависимости ничто не мешает это сделать). Благодаря такому подходу можно добиться довольно занятных побочных эффектов как, например, инициализацию конфигурации до входа в main() и как следствие единственный путь сообщить дополнительные параметры приложению в виде переменных окружения. Все тот же антипаттерн Singleton со всеми характерными для него подводными камнями и особенностями продолжает радовать и при работе с Go.

Решения проблемы init() два, простое и, как мне кажется, не очень правильное в виде полного запрета на статические объекты и метод init() в целом. И более мягкий и полезный вариант запрета на обращение к данным за пределами текущего модуля в рамках init(). Первые вариант можно принудительно проконтролировать линтером gochecknoinits, для второго либо надо писать линтер самому, либо немного более ответственно подходить к разработке (что довольно посредственно работает на практике, но почему бы и нет).

Вторая проблема оказалась довольно неожиданной и звать её – горутины, или скорее злоупотребление оными. Зеленые потоки, дешевые корутины и всеобщее счастье немного одурманивает умы разработчиков и в результате может родиться код с таким количеством мьютексов, что любой C++ проект обзавидуется. А всё дело в том, что хоть потоки и зеленые, они всё ещё потоки и гонки и взаимные блокировки никто не отменял. Какого-то внятного способа борьбы и набора правил как в случае с init() я не вижу, так что буду очень рад узнать рецепт счастья, если, конечно, кто-то захочет им поделиться. Да, само собой я знаю про флаг -race, только он, к сожалению, борется с последствиями и не дает ответа на вопрос как их избежать “by design”. Сейчас обдумываю вариант с использованием некого подобия акторов на основе каналов как средства для синхронизации запросов и горутин, но этот метод не очень хорошо ложится на тот же BeeGo, будь он не ладен, и некоторые другие сторонние библиотеки. Придумаю – обязательно напишу о том как всё героически решилось

Третья проблема к Go вообще никакого отношения не имеет и скорее является иллюстрацией того, почему я тут так часто SOLID вспоминаю. Суть проблемы в банальном игнорировании базовых правил дизайна, таких как SOLID. Чаще и сильнее всего нарушается принцип единственной ответственности; пусть циклические зависимости убраны, смешать логику из разных подсистем в одном месте вполне можно при помощи интерфейсов, а на интуитивном уровне подход “всё в одной куче” несколько более понятен по сравнению с той же генерацией событий и подписчиками. При этом, принцип разделения интерфейса и принцип подстановки наоборот почти никогда не нарушается по причинам явно наблюдаемых интерфейсов и повсеместном использовании моков этих самых интерфейсов для тестирования. К сожалению тут никакого прямолинейного решения не наблюдается и действовать можно только опосредованно через ревью и небольшие обзоры правильных практик разработки ПО.

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

15 Comments Два года с Go

  1. Yauheni Akhotnikau

    > горутины, или скорее злоупотребление оными. Зеленые потоки, дешевые корутины и всеобщее счастье немного одурманивает умы разработчиков и в результате может родиться код с таким количеством мьютексов, что любой C++ проект обзавидуется.

    У меня в докладе, который предстоит сделать буквально послезавтра, есть слайд, посвященный модели CSP, вот с таким текстом:

    Много “процессов” ≠ решение.
    Зачастую много “процессов” ⇒ еще одна проблема.

    Отрадно осознавать, что это я не из пальца высосал.

    Reply
    1. Alexander Stavonin

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

      Reply
      1. Yauheni Akhotnikau

        Я вот только не понял, вы дедлоки на каналах получаете или кроме каналов еще и разделяемые данные, защищенные mutex-ами, используете?

        Reply
        1. Alexander Stavonin

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

          Reply
          1. Yauheni Akhotnikau

            Так акторы вам не помогут. Суть что акторов, что CSP-шных процессов в том, чтобы у каждого было свое приватное состояние и внешний мир на это приватное состояние повлиять не мог.

            Такой подход в принципе противоречит наличию разделяемых данных за которые возможна конкурентная борьба.

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

            ЗЫ. На каналах запросто можно ловить дедлоки, особенно когда взаимодействующих CSP-процессов много. Два процесса ждут друг от друга сообщений и вот тебе аналог дедлока.

          2. Alexander Stavonin

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

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

          3. Yauheni Akhotnikau

            > У нас есть несколько коллекций внешних соединений на которых выполняются команды, при этом команды имеют разное время выполнения и они асинхронные.

            В такой ситуации можно попробовать применить шардинг. Т.е. держать не одну коллекцию, а N коллекций. За каждую коллекцию отвечает своя гороутина со своим каналом. Нужно только иметь функцию, которая по ID соединения (или каким-то другим параметрам) будет выдавать номер “шарды”.

  2. ivan

    Для вэб разработки Go посоветовали бы? Для относительно несложных проэктов типа бложика или REST API.

    Reply
  3. NN

    Года два назад решили у нас взять Go для небольшого сервиса с REST API и БД.

    Сам язык Go немного скудноват, этого повторять смысла нет.
    Строковые тэги идея интересная, но иметь нормальные атрибуты/декораторы/аннотации было бы гораздо проще в поддержке, тем более когда нужно совместить несколько тэгов. (Я про http://gorm.io/docs/models.html )
    Пустной интерфейс аналог void*, и было бы лучше без него. (Может есть какое-то правило, чтобы не писать так код?)
    Наконец в мире Go полно неоднозначностей как лучше работать.
    Вот для зависимостей нужно go dep , go vendor (вроде так) или другое ?
    Одно не совсем работает как надо, другое экспериментальное.
    И так для многого в разработке.

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

    Reply
    1. Alexander Stavonin

      С пустыми интерфейсами мы довольно просто вопрос решили – их не должно быть, если они нужны, то стоит объяснить зачем и почему. Пока что ни одно “зачем и почему” в их адрес коллективный разум не пропустил. Но тут надо отметить что мы библиотек не пишем и обобщенные штуки для которых этот интерфейс обычно используется не нужны.

      В плане управления пакетами мы начинали во времена, когда еще ничего встроенного небыло и взяли govendor, чему до сих пор очень довольны. Отличная штука просто, особенно если хочется точно знать что и когда обновляешь. Более современные не смотрели за ненадобностью.

      У нас на Java точно быстрее не вышло бы, он сильно сложнее, одни система сбоки чего стоит. Плюс вхождение новичков в Go проект занимает неделю, с Java все сложнее.

      Reply
      1. NN

        А чем пользуешься для кэша пакетов Go ? Artifactory или что-то другое ?
        Или ничем и плачете когда GitHub падает ?

        Reply
        1. Alexander Stavonin

          Так у нас все сторонние пакеты имеют зафиксированную через govendor версию. Втроенные никак не кэшерыем. Ни разу проблшем небыло из за недоступности пакетов пока, хотя активно работаем в друх часовых поясах дла года.

          Reply
  4. Егор Синькевич

    Что бы не было дедлоков не должно быть блокировок 🙂

    Странно было не увидеть ни стова о исключениях.
    Для меня их отсуствие – максимальное препятствие вхожлдения в го

    Reply

Leave a Reply