Go глазами Rust-программиста: первые впечатления

Автор оригинала: Nick Cameron
  • Перевод
В преддверии старта курса «Разработчик Golang» подготовили перевод интересного материала. А каким было ваше первое впечатление от Go?




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

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

Общие впечатления


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

Я отметил намного меньше фрустрации и намного больше продуктивности, чем при использовании C/C++, Java, Python и т. д. Однако, Go все еще ощущается частью этого поколения языков. Он извлек из них уроки, и я думаю, что это, вероятно, лучший язык этого поколения; но он определенно часть этого поколения. Он скорее представляет инкрементное улучшение, нежели нечто принципиально новое (следует отметить, что это не оценочное суждение — инкрементность зачастую на пользу в мире разработки программного обеспечения). Хорошим примером этого является nil: такие языки как Rust и Swift избавились от null парадигмы, тем самым устранив целый класс ошибок. Go делает ее менее опасной: нет нулевых значений; разграничение nil и 0. Но основная идея все еще присутствует, как и распространенная рантайм ошибка разыменования нулевого указателя.

Легкость освоения


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

Несколько факторов, в пользу обучаемости:

  • Go небольшой. Многие языки стараются быть небольшими, Go же на самом деле является таковым. (В основном это хорошо, и я впечатлен дисциплиной, которая для этого потребовалась).
  • Стандартная библиотека хороша (и опять же тоже небольшая). Находить и использовать библиотеки в экосистеме очень легко.
  • В языке очень мало того, чего нет в других языках. Go наследует много битов из других устоявшихся языков, полирует их и аккуратно соединяет. Он старательно избегает новизны.


Рутинность кода


Код Go очень быстро становится повторяющимся. В нем отсутствует какой-либо механизм типа макросов или дженериков для сокращения повторений, (интерфейсы хороши для абстракций, но не так хорошо работают для уменьшения дублирования кода). У меня часто накапливается большое количество функций, идентичных за исключением типов.

Обработка ошибок также способствует повторению. Многие функции имеют больше шаблонов if err != nil { return err }, чем оригинального кода.

Использование дженериков или макросов для сокращения рутинности кода иногда критикуют за компромисс с его читаемостью. Но в случае с Go, я бы с ними не согласился. Копировать и вставлять код легко и быстро, но чтение Go кода может приводить к фрустрации, потому что вы должны игнорировать большую его часть или выискивать тонкие различия.

То, что мне понравилось


  • Время компиляции. Определенно быстро; определенно намного быстрее чем Rust. Но на самом деле не так быстро, как я ожидал (мне кажется, что оно на уровне C/C ++ или даже немного быстрее для средних и крупных проектов, а я ожидал чего-то практически мгновенного).
  • Горутины и каналы. Облегченный синтаксис для запуска подпрограмм Go и использования каналов — это действительно хорошо. Такая небольшая деталь, делающая параллельное программирование намного приятнее, чем в других языках, действительно показывает силу синтаксиса.
  • Интерфейсы. Они лишены изыска, но их легко понять и использовать, и они полезны во многих местах.
  • if ...; ... { } синтаксис. Возможность ограничивать область видимости переменных телом if операторов — это хорошо. Это нечто сродни if let в Swift и Rust, но более общего назначения (Go не имеет паттерн матчинга как Swift и Rust, поэтому он не может использовать if let).
  • Тест и док комментарии просты в использовании.
  • Инструмент Go приятен — все сразу в одном месте без необходимости подключения множества инструментов через командную строку
  • В наличии сборщик мусора (GC)! Отсутствие необходимости заботиться об управлении памятью действительно делает программирование проще
  • Varargs.


То, что мне не понравилось


Порядок значения не имеет.

  • nil слайсы — nil, nil слайс и пустой слайс — все это разные вещи. Я более чем уверен, что вам нужны только двое из них, а не все три.
  • Никаких первичных классов. Использование констант ощущается непривычно.
  • Запрет на циклы импорта. Это действительно ограничивает полезность пакетов предназначенных для модуляризации проекта, поскольку это поощряет набивать большое количество файлов в один пакет (или множество небольших пакетов, что может быть так же плохо, если файлы, которые должны быть вместе, распределены в разные пакеты).
  • switch может быть не исчерпывающим
  • for ... range возвращает пару индекс/значение. Получить только индекс легко (просто проигнорируйте значение), но получение только значений требует явного указания. Для меня это шиворот-навыворот, так как в большинстве случаев мне нужно значение, а не индекс.
  • Синтаксис:
    • Несоответствие между определениями и использованиями.
    • Избирательность компилятора (требующего или запрещающего, например, висящие запятые); в основном это облегчается хорошим набором инструментов, но существует несколько случаев, когда это создает раздражающий дополнительный шаг.
    • При использовании возвращаемых типов с несколькими значениями скобки требуются для типа, но не для return.
    • Для объявления структуры требуется два ключевых слова (type и struct).
    • Использование заглавных букв для обозначения переменных public или private. Это как венгерская нотация, только хуже.
  • Неявные интерфейсы. Я знаю, что это так же в списке того, что мне нравится, но иногда это действительно раздражает, например, при необходимости найти все типы, которые реализуют интерфейс, или интерфейсы реализованные для данного типа.
  • Вы не можете писать функции с получателем в другом пакете, поэтому, даже если интерфейсы типизированы неявно, они не могут быть реализованы для вышестоящих типов, что делает их гораздо менее полезными.

Я уже упоминал об отсутствии дженериков и макросов выше.

Согласованность


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

Некоторые моменты, которые особенно выделяются:

  • Есть хороший синтаксис для возврата нескольких значений и для каналов, но их нельзя использовать вместе, потому что нет типов кортежей.
  • Существует оператор for ... range для итерации по массивам и слайсам, но вы не можете перебирать другие коллекции, потому что концепция итераторов отсутствует.
  • Такие функции, как len и append, являются глобальными, но невозможно сделать ваши собственные функции глобальными. Эти глобальные функции работают только со встроенными типами. Они также могут быть универсальными, даже если в Go нет дженериков!
  • Нет перегрузки операторов. Это особенно раздражает при использовании ==, потому что это означает, что вы не можете использовать пользовательские типы в качестве ключей для map, если они не сопоставимы. Это свойство является производным от структуры типа и не может быть переопределено программистом.


Заключение


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

По сравнению с Rust, Go — это совсем другой язык. Хотя оба они могут быть грубо описаны как системные языки или «замена» для C, они имеют разные цели и приложения, стили языкового дизайна и приоритеты. Сборка мусора — это действительно большая разница. Наличие GC в Go делает язык намного проще и меньше, и его легче понимать. Отсутствие GC в Rust делает его очень быстрым (особенно если вам нужна четкая задержка, а не просто высокая пропускная способность) и обеспечивает возможности и шаблоны программирования, которые невозможны в Go (по крайней мере, без ущерба для производительности).

Go — это компилируемый язык с хорошо реализованной средой выполнения. Он быстр. Rust также компилируемый, но имеет намного меньшую среду выполнения. Он очень быстр. Предполагая, что никаких других ограничений нет, я думаю, что выбор между использованием Go и Rust — это компромисс между гораздо более короткой кривой обучения и более простыми программами (что означает более быструю разработку) и, со стороны Rust, большой скоростью и более выразительной системой типов (что делает ваши программы более безопасными и ускоряет отладку и поиск ошибок).



На этом перевод подошел к концу, а мы приглашаем вас на бесплатное практическое онлайн-занятие, где вы узнаете, как за 60 минут создать полностью протестированный http-сервис с нуля без 3rd-party зависимостей.


OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Комментарии 37

    +7

    Лучше читать оригинал. Перевод никакой.

      –21
      Я после Go пробовал посмотреть Rust. По большому счету, он мне понравился. Но его защита памяти делает его несколько медленнее в среднем чем Go. Хотя, конечно, работа сборщика мусора в GC все еще оставляет желать лучшего.
        +2
        Я, конечно, не знаю как вы это меряли, но это звучит как-то очень странно. Да и бенчмарки не согласны benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/rust-go.html
          +2

          Поделитесь замерами? Просто ваше утверждение несколько спорное. Возможно что-то не так было в раст имплементации.

            +6

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

              +1

              Речь не только про сложные алгоритмы, см. блогпост. Достаточно в цикле создавать мульёнами кучу короткоживущих объектов и аллокатор будет висеть гирей на растовом решении, тогда как для гц это практически no-op. Впрочем, даже в такой ситуации кастомный jemalloc неплохо себя показывает.

                +4
                Ради справедливости замечу, что GC в Go тоже далеко не no-op при использовании короткоживущих объектов с указателями. Я уже натыкался на это. Вот пример от более опытного человека: syslog.ravelin.com/further-dangers-of-large-heaps-in-go-7a267b57d487?gi=2dd24cd91629
                  +4

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


                  Впрочем, я даже не про гошный гц, а про гц в целом скорее. В JVM/Haskell, таких проблем нет, и часто бывает, что человек который приходит оттуда в раст и тащит с собой старые привычки обмазывается боксами, а потом всё тормозит как чёрт. У нас даже были внутри компании сравнения практически идентичного алгоритма, один мы написали сами идеоматично, а другой взяли у третьей стороны, написанный в стиле "а мы портировали с Java". Разница была порядка 400%.


                  Так что аллокации без ГЦ действительно удовольствие недешёвое, лучше им не злоупотреблять.

                    –7
                    В Го весьма продвинутый GC из существующих.
                      +7

                      Бггггг, нет. Там используется three-colored mark and sweep, а сборщик мусора не сжимающий и даже не поколенченский (generational).

                        –2
                        Зато он с минимальными паузами, многопоточный и умеет работать, не останавливая выделение памяти.

                        Т.е трейдофф у него в сторону скорости, а не размера «грязной» памяти.
                          +1

                          Угу, хорошая latency в ущерб troughput и общему потреблению памяти. И не настраивается толком.

                            +5

                            Если вы не знали, у гц не 2 параметра которые настраиваются, а с десяток. Latency/Troughput/MaxMemoryConsumption/AverageMemoryConsumption/Cross-generations references/… И все они настраиваются в приличных ГЦ, хотя там офк выбраны некоторые умолчания. Оптимизировать один парметр, забив на все остальные — много ума не надо. Стоит ли считать после этого такой гц "лучшим" или хотя бы "хорошим" — большой вопрос.

                              –2
                              Это скорее теоретически «настраивается». Четвертое колесо настройками не прикрутишь.

                              Так что от настроек, Cross-generations references (и барьеры) в Го не появятся, и можно ли их отключить например в дНете, где они есть — тоже вопрос.

                              Либо ГЦ работает и всех в общем устраивает, как он работает. Либо нужно отказываться от ГЦ. Настройки — мертвому припарки.

                              В Го большинство опрошенных ГЦ критичной проблемой не считают, значит он хорош.
                              image
                                +3
                                Это скорее теоретически «настраивается». Четвертое колесо настройками не прикрутишь.

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

                                  –5
                                  Нет сомнений, что подпирание костылями — весьма выгодное занятие, когда дом построен на песке =)
                    0

                    Да, такое и правда из за деструкторов будет не очень работать, как минимум. Хорошее замечание, но ведь для таких случаев можно в принципе наколеночный "gc" применить в духе такого.
                    https://github.com/servo/string-cache

                      +1

                      Можно. Можно юзать heapless/smallvec и аллоцировать не в куче, можно играть с jemalloc или вообще свой сделать, можно идти по пути всяких кэшей или просто аллоцировать большой объём и руками его использовать… Варианты всегда есть, просто в момент когда вы в программу пытаетесь встроить медленный, плохо протестированный и страшненький псевдо-ГЦ это верный признак, что технология выбрана немного не верно.


                      Точно так же, как я вижу когда в Java/С#-приложении в начале выделяют массивчик на 2 гигабайти и MyFooIntPtr из MyFooMemoryAllocator туда-сюда развлекаться.


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

                        +2

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

                  +2
                  Но его защита памяти делает его несколько медленнее в среднем чем Go.

                  Вот это странное заявление, которое ничем не подкреплено. Читаем статью "Go быстрее Rust, Mail.Ru Group сделала замеры".

                    –5
                    Что вы хотите от Goшника? Golang изначально создавали чтобы дурачкам в гугле было сложнее существующие системы поломать. Пайк об этом прямым текстом говорит
                      0
                      python, как я помню, тоже не для широких масс создавался. И что мы видим?
                  +1
                  Существует оператор for… range для итерации по массивам и слайсам, но вы не можете перебирать другие коллекции, потому что концепция итераторов отсутствует
                  На самом деле, еще можно сделать range по каналам
                    0

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

                      0
                      Может канал не закрыли? Тогда он будет бесконечно на range висеть. Если же канал закрывать нельзя, то да, кроме for с конкретным количеством больше придумать нечего, range тут не поможет.

                      Edit: хотя, если один поток решил не отвечать или запаниковал, то for с количеством повиснет точно так же, как и range.
                      0
                      А также по map
                      +4

                      Читал перевод, но после того как Map перевели как Карту просто закрыл статью. Статья — хороша, перевод — гугл транслейт. Поставил плюс за контент, но надо было хотя бы 10 минут на вычитку потратить. Выглядит как перевод "за зачёт", без особого интереса и попытки понять, что же могло означать "Никаких первичных классов" или "вы не можете использовать пользовательские типы в качестве ключей для карте". Вангую что речь-то шла про first class citizens (functions/types/...) и хэшмапы.




                      Глянул оригинал — почти: No first class enums. Using constants feels backwards.. То есть речь конкретно про энумы, а не какие-то абстрактные "классы".

                        +2
                        Время компиляции быстро? Возможно, но вот время линковки ужасно, многогигабайтный результирующий бинарник — это про Го.

                        По этой же причине и из-за многих других накладных расходов Го не может быть системным ЯП, да и позиционируется исключительно для Веб-прикладных.
                          0
                          В современном Go (1.2-1.4 уж простите не застал) полная сборка (с линковкой) большинства проектов мгновенна.

                          многогигабайтный результирующий бинарник — это про Го

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

                          позиционируется исключительно для Веб-прикладных

                          в чятах плюсовиков разве что, ну или тех, кто полностью игнорирует облака в 2020
                            +2
                            Ой, ну приврал на 3 порядка =) Многомегабайтный конечно, 88Мб в статье.

                            Минимум примерно такой — берем из статьи исходник на Go и на Rust.
                            Ubuntu 18, go 1.14.2, rust 1.40
                            cargo clean
                            cargo build --release
                            Finished release [optimized] target(s) in 1.05s
                            размер после strip 244'056 byte
                            время выполнения программы для 20000 — 3.288 с
                            rm distance
                            time go build distance.go — real 0.564s
                            размер после strip 1'534'040 byte
                            время выполнения программы для 20000 — 6.554 с

                            Время почему то варьируется от конфигураций — лучше посмотреть у себя. Я запускал с дефолтными, т.е -О2 для обоих.
                              –1
                              вы простите, я опять не понял…
                              я был немного удивлён критикой линковки в Go ("время линковки ужасно"), но даже в вашем примере видно, что Go собирает бинарь в 2 раза быстрее чем Rust (и кстати меньше секунды, т.е. мгновенно), линковка входит в сборку

                              что до размера в 88мб: по дефолту Go собирает статический full debug бинарь, кому не нравится, надо как раз линкеру желаемые флаги передавать. upx кстати хорошо помогает и официально поддерживается, а вот strip делать нельзя, можно потом странные сегфолты поймать.

                              время выполнения вполне типичное (ну +-), я где не тестировал, Go примерно вдвое и отставал от Rust (что вполне логично: авторы Go считают производительность результирующего кода делом десятым, ведь компьютеры становятся быстрее с каждым годом, а стоимость вычислений наоборот падает. на первом месте для них производительность программиста — чистый прагматизм, в развитых экономиках человекочасы программистов гораздо дороже обходятся чем железо, отсюда и многие дизайн-решения, например GC, который в большинстве случаев позволяет вообще о памяти не думать). да и Rust чертовски быстр, если сравнить со скриптотой, то Go будет быстрее в 5-10 раз (зависит от задачи)
                                +1
                                Чтобы сравнивать время сборки, стоит сравнивать с чем то быстрым. например с чистым С. Да и пример тут слишком копеечный. Я например наблюдал 40с линковку для небольшой задачки.

                                У Раста медленная компиляция, о чем предпочитают умалчивать. В данном случае это нивелировало. Да и до стрипа здесь был +- сравнимый размер.

                                Вдвое отставание на выч.задачах — выглядит как неплохая оценка.
                                  0
                                  Я например наблюдал 40с линковку для небольшой задачки.

                                  Звучит как баг по мне, наверное даже зарепортил бы. Я не помню даже больших проектов которые не уложились бы в 10с итого (вся сборка).
                          0

                          Банально, но ЯП — лишь инструмент…
                          В основном на Go сижу года три, если не больше…
                          И бывали круто и просто организованные проекты, а также, подобно одному из последних — стопицот обстракций для простого и элементарного круда… Ещё и вызовов множество неявных в перемешку с инициализациями и сетапами пакетов (внутренних, монорепа)… И все это завинчено интерфейсами… — это плата за якобы простоту Го… Ну, так проект был построен, по адски…


                          А по существу: статья какой-то хайп 2х годичной давности, я первый месяц кайфовал с Го, потом плакал, потом научился как ненужно писать — и вроде наладилось… Пока не встретиться дикий проект)


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


                          А ещё там есть select для каналов, контекст, интерфейсы (которыми рекомендуют не злоупотреблять), имутабельность строк, особенности работы с слайсами и много других вещей… Читайте офф доки, раз в месяц, и будет почти счастье.


                          Учимся делать сложные вещи просто… Когда простые вещи получаются сложно — или учите матчасть, или берите другой ЯП, на котором будет проще.


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

                            0
                            Учимся делать сложные вещи просто…
                            Это как познать Дзен… Бесконечен путь совершенствования
                            0
                            Никаких первичных классов.
                            Использование констант ощущается непривычно.
                            Вообще не понял, что тут имеется в виду.

                              0
                              -

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

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