Почему я перешёл с Python на Go: choose your fighter
Привет, Хабр! Меня зовут Саша, я бэкенд-разработчик в Ozon. Пишу платформу для контента, который генерят пользователи: отзывов, комментов, вопросов, ответов. Раньше я писал на Python. Выбрал его изначально из-за лёгкого синтаксиса и большого количества вакансий для Python-разработчиков — изи катка для входа в профессию.
В один момент мне написали из Ozon: «П̶с̶с̶, ̶ ̶п̶а̶р̶е̶н̶ь̶, ̶ ̶п̶о̶к̶о̶д̶и̶т̶ь̶ ̶н̶а̶ ̶G̶o̶ ̶н̶е̶ ̶х̶о̶ч̶е̶ш̶ь̶? ̶ Предлагаем переход на Golang с текущего стека, обучение за счёт компании».
Каждый инженер десятки раз в своей карьере сталкивается с выбором: оставаться дальше на той технологии, на которой он работает, или уходить на другую. В статье я расскажу, по каким критериям я сравнивал две технологии, и почему принял решение переехать на другой язык.
Среди разработчиков бытует мнение, что язык вторичен. Мол, главное — уметь в computer science, а на чём писать — не так уж важно. Но так считают хардкорные разработчики, они вертят деревья, смотрят на всех свысока и зарабатывают 300кк в наносекунду. Я же программист-полукровка (без высшего технического образования, а ещё мои родители — маглы) и считаю, что смена языка — важный шаг и нужно хорошенько прикинуть, прежде чем в это вписываться. Будем откровенны, если у вас за плечами десять лет на плюсах, вряд ли вам предложат должность senior iOS-разработчика на Swift. Проблема в том, что каждый язык имеет свои особенности и на их изучение требуется время.
Дисклеймер: сравнивая два языка, я не считаю один плохим, а другой — хорошим. У меня также нет цели ответить на вопрос, какой язык лучший (такая постановка в принципе некорректна, ведь каждый язык — для своих задач). Я лишь рассказываю о своём способе мышления и о том, на что я обращал внимание, делая выбор, переезжать или нет.
Python как первая любовь
Обожаю этот язык. По-прежнему слежу за ним и экспериментирую с машинным обучением. Он занял ряд ниш, где правит почти единолично: машинное обучение, аналитика, DevOps, скриптинг для различных нужд, например, датафикса. И, конечно же, Python широко используется в вебе. Рейтинг языков программирования PYPL ставит его на первое место с огромным отрывом.
На Python легко состряпать прототип веб-сервиса и получить proof of concept от рынка. Например, на Django можно за считанные дни собрать MVP — сразу бэкенд и фронтенд. Сильнейшее сообщество Python-разработчиков написало кучу прекрасных библиотек, которые дружат с Django так крепко, что в проект можно добавить какие-то хитрые модули буквально парой строк кода.
Golang — вечно молодой
В 2005 году перестало работать важное следствие закона Мура: «производительность процессоров должна удваиваться каждые 18 месяцев из-за сочетания роста количества транзисторов и увеличения тактовых частот процессоров». Тактовая частота упёрлась в ограничение, но начало расти количество ядер, используемых в процессоре.
Все основные языки, которые используются в бэкенде, были созданы, когда закон Мура работал и казалось, что так будет всегда: C (1972), C++ (1983), Python (1991), Java (1995), PHP (1995), JavaScript (1995), C# (2000). Дизайн этих языков изначально не подразумевал работу с несколькими ядрами.
Go — достаточно молодой язык. Ему всего 12 лет, и он сразу делался с оглядкой на то, что код будет выполняться на нескольких ядрах процессора. И в этом его невероятная сила.
А ещё Go изначально задумывался Робом Пайком, как язык, который ускоряет всё: процесс написания кода и процесс компиляции. Никаких ожиданий — всё под девизом «Make programming fun again».
Плох тот бэкендер, который не хочет писать хайлоад
Я всегда мечтал писать хайлоад — и это было первым аргументом в пользу Go. Вы можете возразить, что на Python тоже есть хайлоад, и будете правы. Когда говорят про Python, всегда упоминают, что на нём написаны Instagram и Dropbox.
На сегодняшний день Instagram — проект с самым большим трафиком, написанный на Python. Чтобы заскейлить горизонтально бэкенд на Python при миллиарде MAU (monthly active users), нужно вложить очень много денег в железо — на десятки процентов больше, чем если бы он был написан на более производительном языке. При таких объёмах затраты на железо бьют по карману. Но есть хорошая новость: такая задача в принципе решаема. Скорее в Instagram используют Python, потому что они заложники легаси-кода, который долго и дорого переписывать.
Dropbox, изначально написанный на Python, переписали часть бэка именно на Golang. Некоторые микросервисы до сих пор работают на Python, но это периферийная ненагруженная часть, core-функционал переписан на Go. И причина простая — Golang более производительный.
Основной язык бэкенда компании Lyft — Python. Агрегатор вычислил стоимость железа на одну поездку в такси, заказанную через приложение, — 14 центов. Количество поездок, совершённых в 2018 году, — порядка 650 миллионов. Lyft тратил 100 миллионов долларов в год на инфраструктуру, и сейчас эта сумма ещё больше. Если бы это был Golang стоимость железа могла бы быть меньше на несколько десятков миллионов долларов.
Хайлоаду важен производительный бэкенд — это вопрос экономики. Железо дешевле, чем люди, но на объёмах в сотни миллионов MAU затраты на разработку сравнимы по цене с железом, и выбор непроизводительного языка может привести компанию в ловушку, когда на железо будут сливаться сотни миллионов долларов в год. При этом переписывание бэкенда тоже влетит в копеечку, а, возможно, и вовсе будет невыполнимой задачей.
У Python есть три фундаментальные проблемы:
- GIL (global interpreter lock) съедает профит от concurrency. GIL нужен для того, чтобы синхронизировать потоки для работы сборщика мусора. При изменении количества ссылок на переменные GIL блочит все потоки и даёт исполняться только одному. Те, кто пишут код на Python, давно просят разработчиков языка убрать GIL, но он пустил свои корни так глубоко, что, скорее всего, останется с нами навсегда.
- Перед выполнением любых операций с любыми объектами Python проверяет их тип. Это боль языков с динамической типизацией, которая съедает производительность.
- Почти все объекты в Python аллоцируются на куче, а не на стеке. Неиспользуемая память на куче освобождается медленнее.
Тут нужно оговориться, что невысокая скорость Python — это не баг, а фича. Это расплата за лёгкий синтаксис, с которым можно не заботиться о памяти. Возможность создавать резиновые списки, hashmap«ы, set«ы и т. д. не может быть бесплатной.
Golang же идеален для написания хайлоада. Распараллелить флоу программы на несколько ядер и собрать результат воедино можно без танцев с бубном, а с помощью синтаксиса из коробки. Тут стоит упомянуть, что многоядерность — это не панацея и что она работает до определённого предела.
По закону Амдала, при распараллеливании вычислений быстро достигается предел производительности, после чего дополнительные ядра не обеспечивают предельной производительности.
Ещё одним преимуществом Go являются горутины. Это легковесные потоки исполнения программы. В отличие от тредов в операционной системе горутины:
- быстро стартуют;
- имеют расширяемый стек памяти;
- могут эффективно коммуницировать друг с другом через пакет sync и каналы;
- менеджерятся планировщиком от Go и не завязываются на ОС.
Читаемость и предсказуемость кода
На одно написание кода приходится в среднем пять его прочтений. А некоторые критические места любого сервиса перечитываются десятки раз. В Zen of Python есть такие строки:
Должен существовать один — и, желательно, только один — очевидный способ сделать что-то
На деле мне как-то на собеседовании задали вопрос про то, сколько существует способов развернуть список.
lst = [1, 2, 3, 4, 5]
# 1, разворачивает список in-place, возвращает None
lst.reverse()
# 2, возвращает reversed object, который надо завернуть в list()
new_lst = list(reversed(lst))
# 3, разворачивает список с использованием слайса, возвращает новый объект списка
new_lst = lst[::-1]
Очень легко запутаться и забыть, какой способ что возвращает. Это порождает ошибки.
Python пошёл по пути добавления синтаксического сахара. В какой-то момент его стало чересчур много. Например, объединить два хешмапа до версии 3.9 можно было вот таким способом:
new_dict = {**dict1, **dict2}
Но в 3.9 для этой операции появился ещё один вариант:
new_dict = dict1 | dict2
На вопрос: «А как же Zen of Python?» — в стандарте дан ответ: «Ну мы же говорим про один очевидный способ». А какой из них очевидный-то?
Там же в стандарте кодеры ругаются, что синтаксический сахар затрудняет чтение.
В Golang нет built-in-способа объединить два хешмапа. Приходится делать так:
for k, v := range b {
a[k] = v
}
Ужасно? Но я сразу понимаю замысел программиста, который писал этот код.
Синтаксический сахар порой становится миной замедленного действия. Например, в Python есть прекрасное средство для быстрого создания списков — list comprehension.
lst = [i for i in range(5)] # [0, 1, 2, 3, 4]
Но если такое средство есть, то в один момент ты встречаешь в чужом коде вот такую конструкцию.
init_data_struct = [{'a': 10, 'b': 20}, {'p': 10, 'u': 100}]
# нам нужно объединить два словаря внутри списка, выкинув оттуда ключи 'b','u'
new_data_struct = [[(k,v) for k,v in d.items() if k not in ['b','u']] for d in init_data_struct]
# [[('a', 10)], [('p', 10)]]
И чип начинает искриться. А что делает эта конструкция? Что хотел сказать автор? Сложность кода и так растёт экспоненциально. А если это часть какой-нибудь функции-портянки строк на 50, то восприятие теряется, ты снова и снова утыкаешься в это место и не понимаешь, что происходит. Я наблюдал, как среди разработчиков иногда начинается гонка, как уложить код в как можно меньшее количество строк. В итоге получается огромная цепочка вызовов в рамках строки — и никто не знает, как она работает.
В Golang реально есть только один очевидный способ сделать что-то. Без дзенов. Там просто нет синтаксического сахара. На написание любой конструкции требуется больше кода, чем в Python, но зато паттерны видны сразу, постоянно встречаются одни и те же конструкции, которые упрощают коммуникацию разработчиков через код.
Статическая vs динамическая типизация
У Python динамическая типизация — это значит, что тип переменной определяется в рантайме. У Go тип переменной определяется при компиляции.
a := "UpdateReviewStatus"
b := 1
c := a + b
Этот код в Golang просто не скомпилируется. Возможно, дело даже не дойдёт до компиляции, так как IDE начнёт ругаться и разработчик заметит это. Проект на Python с подобным кодом запустится, но выдаст ошибку в рантайме (хотя и тут IDE поругается и укажет на ошибки). За это Go-разработчики расплачиваются тем, что приходится писать похожие функции для разных типов. Кажется, небольшая плата за баги, отловленные на моменте компиляции. Если код на Go скомпилировался, то он рабочий в подавляющем большинстве случаев.
В версии Python 3.6 появились аннотации типов. Они подсказывают разработчику, какой тип заходит в функцию и что возвращается. Аннотации типов сделаны для того, чтобы подсказать разработчику, как функция работает. Даже если передать в функцию с аннотированными типами значения с другими типами, код запустится и будет работать.
def add(x: int, y: int) -> int:
return x + y
add("a", "b")
# ab
На аннотацию типов немного поругается IDE, а в остальном интерпретатор будет безразличен. Многие разработчики считают, что аннотация типов — это не Pythonic way. Хотя лично я считаю аннотации обязательными: с ними код читается гораздо легче, хоть и нет никаких гарантий, что в функцию зайдёт нужный тип.
Где больше денег, Лебовски?
По данным исследования Stack Overflow, медианная зарплата Go-разработчиков выше, чем у разработчиков на Python, на 27%: $75 669 против $59 454. В России, по данным Хабр Карьеры, разрыв ещё больше — 38%: 180 000 рублей против 130 000 рублей.
Здесь стоит упомянуть, что на Python, например, пишутся парсеры и прочие скриптовые истории, — они тянут медиану зарплат вниз. Но если взять 90-ый процентиль, то есть 10% самых дорогих специалистов, то разница между самыми дорогими разработчиками на Go и на Python составит примерно 10%. Для меня это был приятный бонус при переезде.
Тут стоит упомянуть, что переход на другую технологию почти всегда сопровождается падением по зарплате. Рынок верит в то, что опыт работы на конкретном языке важнее, чем опыт в целом. Но когда тебя хантят, то тебе компенсируют упущенную выгоду на твоём языке. Ты можешь учиться за счёт работодателя, что является приятным бенефитом.
Вывод
Отрефлексировав свой опыт, я убедился в правильности своего решения о переезде на Go:
- Во многих технологических компаниях он стал языком номер один. Если всё будет хорошо, то Golang станет стандартом высоконагруженного бэкенда — кажется, к этому всё и идёт.
- На Go пишется хайлоад, а это другой уровень задач и их разнообразия. В Ozon в период осенних распродаж держали планку в 5000 заказов в минуту.
- У Go прозрачный синтаксис и статическая типизация, что улучшает читаемость кода и уменьшает количество просаженных багов.
- Go-разработчикам больше платят. В России они вообще входят в топ-3 по зарплатам.
Но и Python я забывать не планирую. Я внимательно слежу за ним и продолжаю решать на нём свои задачи.