Node.js против Python против Go: выбор serverless-рантайма

Node.js 22, Python 3.12 и Go 1.22 работают на контейнерах за одним шлюзом. Различия, которые реально важны при выборе: экосистема, стриминг, холодные старты и нюанс компиляции Go в плагин.

Node.js против Python против Go: выбор serverless-рантайма

«На каком языке писать 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-шлюзом с авторизацией на уровне маршрута, читают одни и те же секреты на функцию как переменные окружения, эмитят одни и те же трассы и логи, работают в изолированных контейнерах с одними лимитами памяти и таймаута и выигрывают от одних горячих/тёплых пулов, скрывающих холодные старты. Язык — деталь реализации одной функции, а не развилка в вашей архитектуре.

Так что честный ответ на «какой язык?»: выбирайте под каждую функцию, по тому, что ей нужно, и позвольте им сосуществовать. Тот, что вы уже знаете, — хороший дефолт; а возможность взяться за другой, когда конкретная задача этого требует, не поднимая вторую платформу, — и есть настоящее преимущество.