Почему я перешёл с Python на Go: choose your fighter

Привет, Хабр! Меня зовут Саша, я бэкенд-разработчик в Ozon. Пишу платформу для контента, который генерят пользователи: отзывов, комментов, вопросов, ответов. Раньше я писал на Python. Выбрал его изначально из-за лёгкого синтаксиса и большого количества вакансий для Python-разработчиков — изи катка для входа в профессию. 

В один момент мне написали из Ozon: «П̶с̶с̶, ̶ ̶п̶а̶р̶е̶н̶ь̶, ̶ ̶п̶о̶к̶о̶д̶и̶т̶ь̶ ̶н̶а̶ ̶G̶o̶ ̶н̶е̶ ̶х̶о̶ч̶е̶ш̶ь̶? ̶ Предлагаем переход на Golang с текущего стека, обучение за счёт компании».

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

image-loader.svg


Среди разработчиков бытует мнение, что язык вторичен. Мол, главное — уметь в computer science, а на чём писать — не так уж важно. Но так считают хардкорные разработчики, они вертят деревья, смотрят на всех свысока и зарабатывают 300кк в наносекунду. Я же программист-полукровка (без высшего технического образования, а ещё мои родители — маглы) и считаю, что смена языка — важный шаг и нужно хорошенько прикинуть, прежде чем в это вписываться. Будем откровенны, если у вас за плечами десять лет на плюсах, вряд ли вам предложат должность senior iOS-разработчика на Swift. Проблема в том, что каждый язык имеет свои особенности и на их изучение требуется время.
Дисклеймер: сравнивая два языка, я не считаю один плохим, а другой — хорошим. У меня также нет цели ответить на вопрос, какой язык лучший (такая постановка в принципе некорректна, ведь каждый язык — для своих задач). Я лишь рассказываю о своём способе мышления и о том, на что я обращал внимание, делая выбор, переезжать или нет.

Python как первая любовь

Обожаю этот язык. По-прежнему слежу за ним и экспериментирую с машинным обучением. Он занял ряд ниш, где правит почти единолично: машинное обучение, аналитика, DevOps, скриптинг для различных нужд, например, датафикса. И, конечно же, Python широко используется в вебе. Рейтинг языков программирования PYPL ставит его на первое место с огромным отрывом.

image-loader.svg


На Python легко состряпать прототип веб-сервиса и получить proof of concept от рынка. Например, на Django можно за считанные дни собрать MVP — сразу бэкенд и фронтенд. Сильнейшее сообщество Python-разработчиков написало кучу прекрасных библиотек, которые дружат с Django так крепко, что в проект можно добавить какие-то хитрые модули буквально парой строк кода. 

Golang — вечно молодой

В 2005 году перестало работать важное следствие закона Мура: «производительность процессоров должна удваиваться каждые 18 месяцев из-за сочетания роста количества транзисторов и увеличения тактовых частот процессоров». Тактовая частота упёрлась в ограничение, но начало расти количество ядер, используемых в процессоре.

kw1sjfs7i-n6bxklng2sen2u8ys.jpeg


Все основные языки, которые используются в бэкенде, были созданы, когда закон Мура работал и казалось, что так будет всегда: 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 есть три фундаментальные проблемы:

  1. GIL (global interpreter lock) съедает профит от concurrency. GIL нужен для того, чтобы синхронизировать потоки для работы сборщика мусора. При изменении количества ссылок на переменные GIL блочит все потоки и даёт исполняться только одному. Те, кто пишут код на Python, давно просят разработчиков языка убрать GIL, но он пустил свои корни так глубоко, что, скорее всего, останется с нами навсегда.
  2. Перед выполнением любых операций с любыми объектами Python проверяет их тип. Это боль языков с динамической типизацией, которая съедает производительность.
  3. Почти все объекты в Python аллоцируются на куче, а не на стеке. Неиспользуемая память на куче освобождается медленнее.


Тут нужно оговориться, что невысокая скорость Python — это не баг, а фича. Это расплата за лёгкий синтаксис, с которым можно не заботиться о памяти. Возможность создавать резиновые списки, hashmap«ы, set«ы и т. д. не может быть бесплатной.

Golang же идеален для написания хайлоада. Распараллелить флоу программы на несколько ядер и собрать результат воедино можно без танцев с бубном, а с помощью синтаксиса из коробки. Тут стоит упомянуть, что многоядерность — это не панацея и что она работает до определённого предела. 

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

s0jutln_o0d6wuxd6kqj_pyk8ke.jpeg


Ещё одним преимуществом 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:

  1. Во многих технологических компаниях он стал языком номер один. Если всё будет хорошо, то Golang станет стандартом высоконагруженного бэкенда — кажется, к этому всё и идёт.
  2. На Go пишется хайлоад, а это другой уровень задач и их разнообразия. В Ozon в период осенних распродаж держали планку в 5000 заказов в минуту.
  3. У Go прозрачный синтаксис и статическая типизация, что улучшает читаемость кода и уменьшает количество просаженных багов.
  4. Go-разработчикам больше платят. В России они вообще входят в топ-3 по зарплатам. 


Но и Python я забывать не планирую. Я внимательно слежу за ним и продолжаю решать на нём свои задачи.

© Habrahabr.ru