Когда я начинал разработку fxTunnel — утилиты для проброса TCP-туннелей через NAT — у меня был прототип на Python. Asyncio, aiohttp, всё как полагается. Прототип работал, но когда дело дошло до продакшена и дистрибуции — я переписал всё на Go. Не потому что «Go быстрее», а потому что для конкретно этой задачи Go оказался правильным выбором. Вот сравнение из моего опыта.
Конкурентность: goroutines vs asyncio
Первое, что бросается в глаза при написании сетевого кода — модель конкурентности.
Python с asyncio:
import asyncio
async def handle_connection(reader, writer):
while True:
data = await reader.read(4096)
if not data:
break
writer.write(process(data))
await writer.drain()
writer.close()
async def main():
server = await asyncio.start_server(handle_connection, '0.0.0.0', 8080)
async with server:
await server.serve_forever()
Go:
func handleConnection(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if err != nil {
return
}
processed := process(buf[:n])
conn.Write(processed)
}
}
func main() {
ln, _ := net.Listen("tcp", ":8080")
for {
conn, _ := ln.Accept()
go handleConnection(conn)
}
}
Синтаксически оба варианта лаконичны. Но разница проявляется под нагрузкой. В Python asyncio — это однопоточный event loop. Если одна корутина начнёт делать что-то CPU-bound (даже случайно — логирование, сериализация, валидация), она заблокирует весь цикл. В Go каждая goroutine — лёгкий поток, управляемый рантаймом. Scheduler Go распределяет goroutines по системным потокам, и блокирующий вызов в одной не останавливает остальные.
На практике: в Python-версии fxTunnel при 500+ одновременных соединениях я начал замечать периодические задержки. Не из-за самого парсинга заголовков — а из-за того, что GC Python срабатывал в event loop и подвешивал все соединения на 10-30ms. В Go-версии с тем же количеством соединений латентность оставалась стабильной.
Замеры: цифры из реального проекта
Я провёл сравнительное тестирование обеих версий на одном и том же сервере (4 vCPU, 8GB RAM, Ubuntu 22.04). Тест: проксирование TCP-трафика через туннель, эхо-сервер на другом конце. Замеры не были суперстрогими — скорее порядок величин, чтобы понять разницу.
| Метрика | Python (asyncio) | Go 1.22 |
|---|---|---|
| Соединений/сек (новых) | ~2,800 | ~12,000 |
| Память при 1000 соединениях | ~180 MB | ~45 MB |
| Время старта | ~280 ms | ~8 ms |
| Латентность p99 (1000 conn) | ~12 ms | ~2 ms |
| Размер бинарника | ~30 MB (PyInstaller) | ~8.5 MB |
Разница в несколько раз по соединениям/сек — не потому что Python медленный, а потому что asyncio тратит больше ресурсов на управление корутинами, и GC работает агрессивнее при большом количестве объектов.
Память — ключевой фактор. Каждая goroutine стартует с 2KB стека и растёт по мере необходимости. Каждая корутина Python тянет за собой фреймы, словари для локальных переменных и объекты Future — это дороже.
Оговорка: я не профилировал Python-версию глубоко. Возможно, с uvloop и тюнингом GC разрыв был бы меньше. Но для меня решающим фактором была не производительность, а дистрибуция.
Дистрибуция: где Go побеждает безоговорочно
Вот реальная причина, почему fxTunnel написан на Go:
# Собрать для всех платформ
GOOS=linux GOARCH=amd64 go build -o fxtunnel-linux-amd64
GOOS=darwin GOARCH=arm64 go build -o fxtunnel-darwin-arm64
GOOS=windows GOARCH=amd64 go build -o fxtunnel-windows-amd64.exe
GOOS=linux GOARCH=arm64 go build -o fxtunnel-linux-arm64
Четыре команды — четыре бинарника. Пользователь скачивает один файл и запускает. Без Python, без pip, без виртуальных окружений, без requirements.txt, без конфликтов версий.
С Python для дистрибуции CLI-утилиты варианты такие:
- PyInstaller — 30+ MB бинарник, медленный старт, проблемы с антивирусами на Windows
- pip install — требует Python на машине пользователя, конфликты зависимостей
- Docker — для сетевой утилиты, которую пользователь запускает локально, это неудобно
Для fxTunnel, который ставят на VPS, локальные машины, Raspberry Pi — Go с его кросс-компиляцией был единственным разумным выбором.
Когда Python всё-таки лучше
Я не фанатик Go. Вот задачи, где я по-прежнему выбираю Python:
API-клиенты и интеграции. Когда нужно работать с REST API, парсить JSON, трансформировать данные — Python с его экосистемой (httpx, pydantic, rich) делает это быстрее в разработке. Мой python-ozon-api — тому пример.
Прототипирование. Проверить идею за вечер — Python. REPL, Jupyter, динамическая типизация — всё это ускоряет эксперименты.
Data processing. Если сетевая утилита собирает метрики и потом их анализирует — pandas и numpy не имеют аналогов в Go.
Скрипты автоматизации. Для одноразовых задач, cron-скриптов, ботов — Python быстрее в разработке, и накладные расходы рантайма не имеют значения.
Гибридный подход: Go + Python
В Tirebase я пришёл к тому, что разные части системы пишутся на разных языках:
fxtunnel (Go) — ядро, туннелирование, CLI
gRPC
fxtunnel-manager (Python) — управление, мониторинг, API
REST
Dashboard (Vue.js) — веб-интерфейс
Go делает то, что должен делать Go: обрабатывает тысячи соединений, проксирует трафик, работает как демон. Python делает то, что должен делать Python: предоставляет удобное API для управления, собирает и агрегирует метрики, отправляет уведомления.
Связь между ними — gRPC. Protobuf-контракт гарантирует совместимость, а кодогенерация работает для обоих языков. Правда, поддержка protobuf в Python иногда раздражает — генерация кода не такая гладкая, как в Go, и типизация неполная. Но работает.
Чек-лист: что выбрать
Прежде чем начинать проект, я задаю себе эти вопросы:
- Бинарник без зависимостей критичен? Go.
- Тысячи одновременных соединений? Go.
- Кросс-компиляция для ARM/Windows/Mac? Go.
- Интеграция с внешними API, парсинг данных? Python.
- Быстрый прототип за вечер? Python.
- Нужно и то, и другое? Разделите систему на компоненты и используйте каждый язык для своей задачи.
Итог
Нет лучшего языка для сетевых утилит — есть задача и инструмент. fxTunnel как ядро — на Go, потому что дистрибуция, память и латентность критичны. Управление fxTunnel — на Python, потому что скорость разработки и экосистема важнее наносекунд.
Самое вредное, что можно сделать — выбрать язык из идеологии, а не из требований проекта. Пишите на том, что решает конкретную проблему.