Я профессионально занимаюсь разработкой вот уже почти 23 года, а программы пишу в общей сложности почти 38 лет.
За это время мне приходилось пользоваться множеством языков программирования. Я их очень люблю, люблю узнавать об их новых функциях и изменениях, которые они претерпели по сравнению с более старыми языками.
Если оглянуться на 10 лет назад, можно увидеть, насколько сильно развились конкретные языки. У C++, Java, Python и JavaScript появился новый функционал, и даже совсем молодые языки Rust и Swift очень быстро продвинулись со времен своего появления. Всё это, безусловно, интересно, но иногда кажется, что за этой новизной просто не угнаться.
Есть ещё и Go
Чтобы понять, что собой представляет Go, проще перечислить то, чего у него нет:
- нет виртуальной машины и LLVM-компилятора;
- нет исключений;
- нет наследования пользовательской реализации;
- нет переопределения функций, методов, операторов;
- нет неизменяемых строк;
- нет перечислений;
- нет дженериков;
- нет крупных обновлений функционала с момента выхода Go 1 в 2012 году.
Единственное, чем интересен Go, — это встроенная поддержка многопоточного программирования через горутины, каналы и оператор select
. Однако эта технология основана на появившемся ещё в 1978 году принципе взаимодействующих последовательных процессов (CSP). Go мало похож на язык программирования XXI века, не так ли?
И, тем не менее, согласно исследованиям Stack Overflow, Go стоит на 3 месте по востребованности и (вполне возможно, неслучайно) на 3 месте по уровню заработной платы специалистов. Каждый стартап в Кремниевой долине использует Go для своей инфраструктуры. Docker, Kubernetes, etcd, Terraform, Vault, Consul, Traefik и много других суперсовременных проектов написаны на Go. Так что же происходит? Почему всем так интересен этот скучный язык?
Почему разработка — это новое мостостроение?
Прежде чем ответить на этот вопрос, давайте немного отвлечёмся.
На снимке изображён мост Аркадико в Арголисе, Греция. Ему чуть больше 3 тысяч лет. Это старейший в мире мост, сохранившийся до наших дней. Удивительно, но им всё ещё пользуются.
Зачем нам тут какой-то древний мост? А затем, что в разработке есть кое-какая универсальная истина, о которой не особо любят распространяться.
Мы очень плохи в разработке.
И я сейчас говорю не только о том коллеге, которого в период аврала принято отправлять за кофе, чтобы он не плодил баги. Я говорю о каждом: о себе, о вас, о любом известном разработчике, чьё имя только может прийти в голову.
А вот те, кто проектирует и строит мосты, действительно знают свое дело. Мосты всегда возводятся в соответствии с бюджетом и графиком и служат десятки, сотни, а то и тысячи лет. Если вдуматься, мостостроение — это круто. Мы встречаем мосты настолько часто, что уже не видим в них ничего интересного. При этом никого не удивляет, что мост работает как надо, но почему-то все удивляются, когда так работает код.
К сожалению, мир очень зависим от программ — возможно, даже больше, чем от мостов. Поэтому нам придётся учиться писать хороший код гораздо быстрее, чем мы учились строить мосты.
Всё, что мы знаем о разработке ПО
За последние 60 лет мы успели прийти к кое-каким общим договорённостям в сфере программирования:
- проблему лучше обнаружить как можно скорее;
- люди ужасно обращаются с ресурсами памяти в программах;
- инспектирования кода помогают выявлять баги;
- если в проекте задействовано более одного человека, коммуникация выходит на первый план.
Железо не спасает
Ко всему вышесказанному можно добавить ещё одну уже прижившуюся истину — компьютеры сейчас не становятся быстрее. А если и становятся, то не так, как раньше. В 1980-х и 1990-х годах процессоры ускорялись в два раза каждые 1–2 года. Теперь всё иначе.
Если взглянуть на производительность одноядерных процессоров, то можно заметить, что самый быстрый в 2019-м году Core i9 менее чем в два раза быстрее Core i7, который был самым быстрым в 2011-м. Процессоры не становятся быстрее — мы просто добавляем ядра. Теперь посмотрите на производительность многоядерных: она получше — новый процессор чуть более чем в два раза быстрее старого.
Нас ограничивает не только производительность процессора. Вот что писал Форрест Смит, разработчик из Facebook, о влиянии памяти ОЗУ и схемы доступа к ней на производительность:
- ОЗУ намного медленнее процессоров, и эта разница не становится меньше, даже несмотря на то, что процессоры особо не ускоряются.
- К ОЗУ можно получить прямой доступ, но в этом случае она будет работать медленно. Если данные организованы последовательно, то на современном процессоре Intel можно считывать с ОЗУ до 40 гигабайт в секунду. При использовании прямого доступа удастся считывать чуть меньше половины гигабайта в секунду.
- Код с обилием указателей особенно медленный. Вот что писал об этом Смит: “Производительность последовательного суммирования значений через указатель не превышает 1 Гб в секунду. Прямой доступ дважды не проходит кэширование, и его скорость составляет всего 0,1 Гб в секунду. Таким образом, переходы по указателям работают в 10–20 раз медленнее. Пожалейте друзей — не делитесь с ними списками с указателями”.
Скучное — это новое увлекательное
Итак, учитывая то важное, что мы теперь знаем о разработке ПО и о доступном нам железе, давайте снова взглянем на Go.
Go и ПО
Как можно более раннее обнаружение проблем
Пусть у Go и небольшой функционал, но инструменты у него отличные. У Go быстрый компилятор, и такая скорость, по мнению команды Go, является преимуществом языка. Он позволяет быстро компилировать код, а если это не удаётся, указывает, где ошибка. Тестирование встроено в стандартную библиотеку, чтобы мотивировать разработчиков проверять код и искать проблемы. Бенчмаркинг, профилирование и проверка на состояние гонки данных также поставляются “из коробки”. Очень немногие языки могут похвастаться такими встроенными инструментами, хотя они позволяют просто и быстро обнаруживать проблемы.
Управление памятью
Как нам с вами известно, у Go есть сборщик мусора. Не нужно постоянно следить за памятью — и это просто замечательно. Сборка мусора редко встречается у компилируемых языков. У Rust есть контроль владения (borrow checker) — он отлично помогает добиться высокой производительности и эффективного управления памятью, но фактически перекладывает обязанности сборщика на самого разработчика, чем создаёт сложности в использовании. Утечки памяти могут происходить и с ARC в Swift, если наделать ошибок и забыть объявить какие-то ссылки слабыми. Сборщик мусора в Go уступает в производительности этим полуавтоматическим системам, и, безусловно, бывают ситуации, когда нужна более высокая скорость, но в большинстве случаев он прекрасно справляется.
Инспектирование кода
Инспекции важны, если проводить их хорошо. Чтобы получить эффективный анализ кода, нужно убедиться, что проверяющие акцентируют внимание на правильных вещах. В низкокачественных инспекциях много времени тратится, например, на форматирование. И здесь Go приходит на помощь — при анализе кода на Go не может быть разночтений, так как он форматируется утилитой fmt.
Инспектирование кода — это палка о двух концах. Если вам нужен хороший анализ, убедитесь, что другие люди понимают ваш код. Программы на Go должны быть простыми и содержать несколько хорошо известных конструкций, которые не изменились с момента появления языка. Поскольку в Go нет исключений, аспектно-ориентированного программирования, наследования, переопределения и перезаписи методов, то всегда ясно, какой код что вызывает и где возвращаются значения. Если вы не меняете переменные на уровне всего пакета, то понять, как модифицируются данные, очень просто. Так как Go почти не изменился, он позволяет избежать возникновения антипаттерна “поток лавы”, когда можно точно определить, насколько стар тот или иной блок кода, учитывая время ввода в язык применяемых в нём функций.
Роль коммуникации
Чем Go полезен в коммуникации? Мы уже говорили, что простота, стабильность и стандартное форматирование Go упрощают понимание того, как работает код. Но есть и кое-что ещё. Неявные интерфейсы Go позволяют командам разработчиков писать несвязный код: вызывающий код точно описывает необходимую функциональность и позволяет выяснить, что код выполняет.
Go и железо
Решение сделать Go компилируемым языком оправдало себя. Создание интерпретируемых языков, работающих в виртуальных машинах, казалось хорошей идеей, когда процессоры становились быстрее день ото дня. Если программа оказывалась недостаточно быстрой, стоило подождать год, чтобы всё исправилось само собой. Такой подход больше не работает. Компиляция в нативный код далеко не так интересна, как свежие фишки виртуальных машин, но она даёт преимущество в производительности.
Давайте с помощью микро-бенчмарков из The Benchmark Game сравним производительность Go и языков, которые работают в виртуальных машинах. Сначала взглянем на Python и Ruby (значение меньше 100% означает, что Go медленнее, больше 100% — Go быстрее):
Как много здесь красного. Python оказался быстрее (странно, но в этом тесте он быстрее не только Go, но и всех остальных языков) только в одном бенчмарке, а Ruby не опередил Go ни в одном. Кроме этого единственного случая, код обоих языков работает медленнее кода Go в промежутке от 17% до 60 раз.
Теперь посмотрим на Java и JavaScript:
Эти языки гораздо ближе к Go по производительности. JavaScript быстрее Go в одном бенчмарке и медленнее во всех остальных, но худший показатель JavaScript уступает Go в 3 раза.
Java и Go близки по производительности. Java быстрее Go в четырёх случаях, медленнее — так же в четырёх, примерно равен Go — в двух случаях. Худший показатель Go уступает Java примерно в 3 раза, лучший показатель — примерно на 50% быстрее Java.
Здесь мы видим, что единственная виртуальная машина, которая может соперничать с Go — виртуальная машина Java. HotSpot — замечательная технология, но сам факт того, что компилятору, который жертвует оптимизацией ради скорости, приходится противопоставлять один из лучших в мире образцов ПО, о чём-то да говорит. А ещё за эту технологию приходится платить: приложения на Java потребляют гораздо больше памяти, чем на Go.
У Go есть ещё одно преимущество. Мусор, который собирают сборщики, — это не что иное, как неиспользуемые указатели. В отличие от языков, которые их просто скрывают, Go передаёт власть над указателями разработчику. Он позволяет избегать указателей и располагать структуры данных так, чтобы обеспечивать к ним быстрый доступ ОЗУ. Сборщик мусора у Go проще, так как программы на Go просто образуют меньше мусора. Быть скучным — значит просто делать меньше работы.
Как мы знаем, процессоры компенсируют отсутствие роста скорости большим количеством ядер. В таких условиях к месту приходится язык, который использует эту особенность. И тут в игру вступает поддержка многопоточности в Go. То, что в язык встроены поддержка многопоточности и библиотека времени выполнения, которая разбивает горутины на множество потоков, означает, что при наличии нескольких ядер процессора потоки распределяются по этим ядрам.
Чего нет, того и не надо
Мы уже видели, что Go сосредоточен на функциях и инструментах, которые упрощают разработку и в большей степени соответствуют памяти и архитектуре процессора современных компьютеров. Но как насчёт того функционала, которого нет у Go, но есть у других языков? Может, разработчики Go упускают что-то, что позволило бы писать легко поддерживаемый код с меньшим количеством багов? Если верить исследователям, то нет.
В 2017 году вышла работа “A Large Scale Study of Programming Languages and Code Quality in Github” (“Широкомасштабное исследование языков программирования и качества кода на Github”). В попытке ответить на вопрос “Каково влияние языков программирования на качество кода?” было изучено 729 проектов, 80 миллионов строк кода, 29 тысяч авторов, 1,5 миллиона коммитов на 17 языках программирования. В результате оказалось, что влияние небольшое:
“Следует отметить, что незначительное влияние, оказываемое языком, практически полностью подавляется такими факторами разработки, как размер проекта, команды, коммита”.
Другая группа исследователей изучила те же данные и в 2019 году выпустила обновлённый труд “On the Impact of Programming Languages on Code Quality” (“О влиянии языков программирования на качество кода”). Выводы оказались ещё более удивительными:
“Основываясь на имеющихся данных, установить причинно-следственную связь между языком программирования и качеством кода не просто невозможно — само наличие этой взаимосвязи вызывает сомнения”.
Если выбор языка не играет роли, зачем выбирать Go? Что показывают эти исследования, так это то, что важен сам процесс разработки. Инструменты, тестирование, производительность, простота долговременной поддержки — вот что гораздо важнее модных функций. Инструментарий Go, если его правильно использовать, позволяет лучше организовать процесс, предоставляя проверенный временем функционал.
Это не значит, что новые функции — зло. За века и тысячелетия технологии строительства мостов шагнули далеко вперёд. Но хотели бы вы первым пройти по мосту, который был построен по новой задумке, но с непроверенной технологией? Вам бы захотелось немного подождать, пока мост проверят, а уж потом идти по нему самому.
То же самое касается и программ. Если мы хотим создавать надёжные, как мосты, программы, нам так же нужно использовать хорошо известные и протестированные технологии. Именно по этой причине в Go используется функционал, разработанный ещё в 1970-х годах, — мы точно знаем, что он работает.
Go скучный, и это здорово. Давайте с его помощью создавать потрясающие приложения для будущего.
Читайте также:
- Как работает функция Defer в Golang
- Полиморфизм с интерфейсами в Golang
- Объектно-ориентированное программирование в Golang
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Jon Bodner: Go is Boring… And That’s Fantastic!