Node.js против Python против Go: выбор serverless-рантайма
Node.js 22, Python 3.12 и Go 1.22 работают на контейнерах за одним шлюзом. Различия, которые реально важны при выборе: экосистема, стриминг, холодные старты и нюанс компиляции Go в плагин.
«На каком языке писать serverless-функции?» — вопрос со скучным правильным ответом (на том, который ваша команда уже знает) и более интересным настоящим ответом: выбранный рантайм меняет то, что функции могут делать, как они холодно стартуют и как стримят. На Inquir доступны Node.js 22, Python 3.12 и Go 1.22 — их можно свободно смешивать за одним шлюзом и выбирать под каждую функцию. Эта статья — про различия, которые реально важны при выборе.
Сначала — то, что у них общее, потому что это фундамент для всего остального.
Они на контейнерах, а не на edge-изолятах
Каждая функция работает в своём контейнере — один контейнер на функцию, изолированный от остальных. Это осознанный выбор с реальными последствиями, и именно на этой оси serverless-на-контейнерах отличается от edge/isolate-платформ.
Edge-рантаймы (изоляты V8 и им подобные) прекрасны для крошечных быстрых stateless-обработчиков, но они накладывают жёсткие ограничения: урезанное подмножество стандартной библиотеки, никаких нативных модулей, тесные потолки по CPU/памяти, никаких произвольных бинарников. Как только вам нужен sharp для обработки картинок, нативная крипто-библиотека, headless-браузер, ONNX-модель или пакет, дергающий бинарник, — вы переросли модель изолята.
У функций на контейнерах такого потолка нет. Вы получаете полный рантайм языка и стандартную библиотеку, нативные модули и возможность подключать общие слои зависимостей для Node, Python или Go. Плата — холодный старт, измеряемый временем запуска контейнера, а не инстанцирования изолята, — ровно то, что скрывают горячие/тёплые пулы. Если ваша работа — «маленький чистый JS-трансформ на edge», изолят хорош. Если это «настоящая бэкенд-работа с настоящими зависимостями» — вам нужны контейнеры.
Всё ниже предполагает этот общий фундамент: та же маршрутизация шлюза, те же секреты на функцию, та же наблюдаемость, те же горячие пулы, те же лимиты (256 МБ памяти по умолчанию до 2 ГБ; таймаут 5 с по умолчанию до 15 мин). Меняется — язык.
Node.js 22 — дефолт для большинства бэкендов
Node — путь наименьшего сопротивления, и для большинства API-и-склеивающей работы это правильный выбор. Экосистема огромна, асинхронная модель ложится на request/response-бэкенды, а хендлер — ровно такой, как вы ожидаете:
export async function handler(event, context) {
const body = JSON.parse(event.body ?? '{}');
return { statusCode: 200, body: JSON.stringify({ ok: true }) };
}
Несколько особенностей Node, которые стоит знать:
- Работают и ESM, и CommonJS с автоопределением — можно отдавать
export async function handlerилиexports.handler = …, и оно разрешится в любом случае, включая фолбэк, спасающий неверно помеченный.mjs/.cjs. - Стриминг — первоклассный через async-генераторы (
export async function* handler) — полный паттерн см. в посте про SSE. Это делает Node самым гладким выбором для стриминга токенов LLM и прогресса агентов. - Зависимости берутся из бандла или из общих слоёв, разрешаемых по module path, так что тяжёлые
node_modulesне нужно перезаливать под каждую функцию.
Берите Node, когда функция — это HTTP-склейка, обработчик вебхуков, tool-эндпоинт AI-агента или что угодно, где решающий фактор — «нужная библиотека есть в npm». Это большинство функций.
Python 3.12 — данные, ML-склейка и AI-тулчейн
Python оправдывает себя везде, где суть — в библиотеках. Обработка данных, численные задачи, ML-инференс и вся экосистема AI/LLM-тулинга комфортнее всего живут в Python, а контейнерный рантайм даёт пользоваться ими без борьбы с ограничениями изолята.
def handler(event, context):
body = json.loads(event.get("body") or "{}")
return {"statusCode": 200, "body": json.dumps({"ok": True})}
Особенности Python:
- Синхронный или асинхронный хендлер — работают и обычный
def handler(event, context), иasync def, а async/генераторы дают стриминг так же, как в Node. - Слои монтируются в
sys.path, так что общие зависимости разрешаются как обычные импорты, с опциональной прекомпиляцией байткода для сокращения времени импорта. Подключайте нужные библиотеки слоем, а не раздувайте каждую функцию. - Это естественный дом для CPU-bound численных задач и инференса моделей — загрузите модель один раз на уровне модуля, чтобы она была тёплой для последующих вызовов, и держите тёплый пул, чтобы не платить стоимость холодного импорта на каждом вызове.
Берите Python, когда ценность функции — это библиотека (модель, парсер, научный пакет) или когда она склеивает LLM с вашими данными.
Go 1.22 — производительность, мало памяти и один реальный нюанс
Go — выбор, когда важна стоимость исполнения: CPU-bound работа, высокий объём вызовов, тесные бюджеты памяти или задержка, которую хочется срезать до пола. Он работает экономно и быстро. У него же — самая своеобразная из трёх модель и один нюанс, который стоит знать до того, как коммититься.
func Handler(event map[string]any, ctx obs.Context) (any, error) {
return map[string]any{"ok": true}, nil
}
Особенности Go:
- Отдельный контракт
(result, error). Хендлер возвращает значение и ошибку вместо объекта-ответа; паники восстанавливаются, так что один плохой запрос не роняет контейнер. - Он компилируется в плагин. Ваш Go-исходник при первой загрузке собирается в плагин, кэшируется по хешу исходника и требует
CGO_ENABLED=1. Эта компиляция при первой загрузке — стоимость, которую интерпретируемые рантаймы не платят; последующие загрузки бьют в кэш. Заложите чуть более тяжёлый первый деплой в обмен на быстрое исполнение в устоявшемся режиме. - Стриминг работает иначе. Go стримит, возвращая канал, который рантайм сливает в SSE-ответ, — это лучше ложится на модель конкурентности Go, чем генератор. Нюанс: стриминг в Go требует тёплого/горячего пути; холодный путь стриминг отклоняет. Для латентно-чувствительного стриминг-эндпоинта вы и так держите тёплый пул, так что обычно это не проблема — но знайте это до выбора Go для стриминг-сервиса.
Берите Go, когда функция горячая, численная или критична по производительности, и вы предпочтёте один шаг компиляции заранее, чем CPU-циклы навсегда.
Быстрый гид по выбору
- По умолчанию — Node.js для HTTP-хендлеров, вебхуков, tool-ов AI-агентов и всего, где решает «пакет есть в npm». У него же самый гладкий стриминг.
- Выбирайте Python, когда причина существования функции — библиотека: обработка данных, ML-инференс или LLM/данные-склейка.
- Выбирайте Go для CPU-bound, высоконагруженной или тесной по памяти работы, принимая компиляцию при первой загрузке и нюанс стриминга на холодном пути.
И тихая суперсила запуска всех трёх на одной платформе: вам не нужно выбирать один раз. Python-функция может делать инференс модели, Go-функция — горячий численный путь, а Node-функция — обращённый к шлюзу оркестрирующий инструмент: те же маршруты, те же секреты, те же трассы, разные языки под задачу. Смешение рантаймов — не компромисс, а смысл.
Что не меняется, какой бы язык вы ни выбрали
Какой бы рантайм вы ни взяли, контракт платформы одинаков: функции стоят за одним API-шлюзом с авторизацией на уровне маршрута, читают одни и те же секреты на функцию как переменные окружения, эмитят одни и те же трассы и логи, работают в изолированных контейнерах с одними лимитами памяти и таймаута и выигрывают от одних горячих/тёплых пулов, скрывающих холодные старты. Язык — деталь реализации одной функции, а не развилка в вашей архитектуре.
Так что честный ответ на «какой язык?»: выбирайте под каждую функцию, по тому, что ей нужно, и позвольте им сосуществовать. Тот, что вы уже знаете, — хороший дефолт; а возможность взяться за другой, когда конкретная задача этого требует, не поднимая вторую платформу, — и есть настоящее преимущество.