Python и Go отличаются по свойствам, и поэтому могут дополнять друг друга.
Существует распространённое заблуждение, будто простой и лёгкий — это одно и то же. В конце концов, если некий инструмент легко использовать, то и его внутреннее устройство должно быть просто понять, разве не так? И обратное тоже верно, да? На самом деле, всё как раз наоборот. В то время, как по духу оба понятия указывают на одно и то же (итог со стороны кажется лёгким), на практике такая поверхностная лёгкость достигается огромной подкапотной сложностью.
Рассмотрим Python. Известно, как низок порог вхождения в этот язык; именно поэтому Python — излюбленный вариант в качестве первого языка программирования. В школах, университетах, НИИ и многочисленных компаниях по всему миру Python предпочитают именно потому, что разобраться в нём может каждый, независимо от уровня образования, причём академический бэкграунд здесь обычно вообще не требуется. Для работы с Python редко приходится прибегать к теории типов или понимать, где, что и как именно хранится в памяти, в каких потоках выполняется тот или иной фрагмент кода, т.д. Более того, через Python можно влиться в работу с некоторыми наиболее обширными библиотеками, предназначенными для научных вычислений и системного программирования. Когда ты оказываешься в силах ворочать такими мощностями, даже одна строка кода убедительно демонстрирует, почему Python стал одним из самых популярных языков программирования на планете.
Здесь и начинаются нюансы — оказывается, та лёгкость, с которой на Python удаётся выразить что угодно, обходится отнюдь не даром. Под капотом Python лежит тяжеленный интерпретатор, и даже для выполнения единственной строки кода в нём должно произойти множество операций. Если вам доводилось слышать, что Python — «медленный» язык, то знайте, что эта явственная «медлительность» связана с рядом решений, которые интерпретатору приходится принимать во время выполнения. Но, как мне кажется, это даже не основная проблема. Сложность всей среды выполнения Python и её экосистемы, наряду с некоторыми произвольно принятыми решениями по поводу управления пакетами в этом языке — вот причины крайней хрупкости этой среды. Из‑за хрупкости зачастую возникают случаи несовместимости и отказы во время выполнения. Вполне обычны ситуации, в которых ты на какое‑то время отходишь от работы с приложением на Python, затем возвращаешься к нему через несколько месяцев — и обнаруживаешь: экосистема изменилась настолько, что твоё старое приложение уже даже не запустить.
Разумеется, это грубое, даже чрезмерное упрощение: сегодня даже дети знают, что такие проблемы решаются при помощи контейнеров. Действительно, благодаря Docker и другим подобным инструментам можно навсегда «зафиксировать» зависимости в базе кода Python так, что эта база кода будет работать практически вечно. Но, фактически, это просто перекладывание ответственности и сброс всей сложности на уровень инфраструктуры ОС. Не конец света, но нельзя смотреть на эту проблему сквозь пальцы и недооценивать её.
От лёгкости к простоте
Если бы мы взялись решать проблемы, существующие в Python, то получили бы на выходе нечто вроде Rust — язык крайне производительный, но печально известный своим высоким порогом вхождения. На мой взгляд, Rust совсем не лёгок в использовании и, более того, он совсем не прост. В то время, как сегодня Rust на самой волне хайпа, я со всем моим опытом (программирую лет 20, первые шаги делал на C и C++) не могу взглянуть на сниппет кода Rust и с уверенностью прочитать его с листа.
Лет пять назад я открыл для себя Go, как раз, когда работал с системой, основанной на Python. Притом, что с синтаксисом Go я освоился не с первого захода, мне сразу же стало ясно, насколько просты идеи, лежащие в его основе. Язык Go сделан именно так, чтобы его было просто понять любому человеку в организации — как джуну, который только что со студенческой скамьи, так и старшему менеджеру по инженерии, который мельком взглянул на код. Скажу больше, при всей простоте языка Go синтаксис его обновляется очень редко. Последним крупным изменением были дженерики, добавленные в версии 1.18, и то после серьёзного обсуждения, продлившегося целое десятилетие. В большинстве случаев, если взглянуть на код Go, написанный хоть пять дней, хоть пять лет назад, он будет выглядеть очень узнаваемо и должен просто работать.
Но простота требует дисциплины. На первый взгляд язык Go может показаться сковывающим и даже слегка ретроградным. В особенности если сравнить код Go с таким лаконичным выражением, как списковое или словарное включение в Python:
temperatures = [
{"city": "City1", "temp": 19},
{"city": "City2", "temp": 22},
{"city": "City3", "temp": 21},
]
filtered_temps = {
entry["city"]: entry["temp"] for entry in temperatures if entry["temp"] > 20
}
Чтобы написать такой же код на Go, требуется значительно больше постучать по клавиатуре, но в идеале в нём будет на один уровень абстракций меньше, чем в Python, который зависит от своего подкапотного интерпретатора:
type CityTemperature struct {
City string
Temp float64
}
// ...
temperatures := []CityTemperature{
{"City1", 19},
{"City2", 22},
{"City3", 21},
}
filteredTemps := make(map[string]float64)
for _, ct := range temperatures {
if ct.Temp > 20 {
filteredTemps[ct.City] = ct.Temp
}
}
Притом, что аналогичный код можно написать и на Python, в программировании действует негласное правило: если в языке предоставляется более лёгкий (читай, более лаконичный, более элегантный) вариант, то программисты будут склоняться именно к нему. Но «лёгкость» — понятие субъективное, а «простота» должна быть простой для кого угодно. Когда одно и то же действие можно совершить несколькими способами, это приводит к развитию разных стилей программирования, причём зачастую множество стилей может сочетаться в одной и той же базе кода.
Да, Go можно назвать многословным и «скучным», но при работе с ним запросто ставится ещё один плюсик: компилятору Go приходится выполнять совсем немного работы, при сборке исполняемого файла. Компиляция и запуск приложений на Go зачастую не уступает и даже выигрывает в скорости у Python (там ведь ещё нужно завести интерпретатор) или у Java (там нужно запустить виртуальную машину). Неудивительно, что самый быстрый исполняемый файл — это нативный исполняемый файл. Он не так быстр, как аналоги на C/C++ или Rust, зато в несколько раз проще на уровне исходного кода. Этим небольшим «недостатком» Go я готов пренебречь. И на закуску: бинарники Go связываются статически. Это значит, что их можно собрать где угодно, а запускать на той машине, на которой нужно. Не будет никаких зависимостей, связанных со средой выполнения или библиотеками. Ради удобства мы всё равно обёртываем в контейнеры Docker наши приложения, написанные на Go. Тем не менее, эти приложения получаются миниатюрными и потребляют лишь малую толику памяти и процессорного времени по сравнению с аналогичными приложениями, написанными на Python или Java.
Как сочетать Python и Go себе на пользу
Наиболее прагматичное решение, к которому мы пришли в нашей работе — комбинировать лучшие черты лёгкости Python и простоты Go. Мы считаем, что Python — отличный полигон для прототипирования. Именно здесь рождаются идеи, принимаются или отвергаются научные гипотезы. Python просто создан для исследования данных и для машинного обучения, а поскольку нам по работе приходится постоянно иметь дело с этими областями, едва ли имеет смысл заново изобретать велосипед на каких‑нибудь других языках. Кроме того, Python лежит в основе фреймворка Django, девиз которого — стремительная разработка приложений (вряд ли ему в этом найдутся равные, но для полноты картины, конечно, упомяну Ruby on Rails и Phoenix для Elixir).
Допустим, в проекте нужно обойтись минимальным управлением со стороны пользователя, а администрирование данных требуется организовать внутри приложения (у нас таких проектов большинство). В данном случае мы делаем скелетное приложение на Django, так как в нём есть встроенный Admin, фантастически полезная вещь. Как только грубо сработанная на Django пробная модель начинает напоминать продукт, мы смотрим, насколько серьёзная часть этой модели поддаётся переписыванию на Go. Поскольку в приложении на Django уже определена структура базы данных и понятно, как выглядят модели данных, довольно легко написать на Go код, заступающий на смену Django. Через несколько итераций достигаем симбиоза, где обе половинки мирно сосуществуют поверх одной и той же базы данных, а коммуникация между ними осуществляется через самую простую систему сообщений. В конечном итоге «оболочка» Django превращается в оркестратор — то есть отвечает за администрирование и запускает те задачи, которые затем поступают на обработку в область приложения, написанную на Go. Та часть, что написана на Go, отвечает за всё прочее, от клиентских API и конечных точек до бизнес‑логики и обработки заданий, возникающих на машинном интерфейсе.
Такой симбиоз нас пока не подводил и, надеюсь, далее тоже не подведёт. Как‑нибудь напишу пост, в котором более подробно разберу обрисованную здесь архитектуру.
Спасибо за внимание!