Тоталитарный Golang
- вторник, 24 марта 2026 г. в 00:00:07
Всем привет! Сегодня я попробую вас убедить, что уникальным торговым предложением языка Go являются не горутины, скорость, минималистичность или современность, а "тоталитарность".
Преимуществами Go называют скорость, производительность, многопоточность, современность и удобность в микросервисах (чтобы это ни значило), и часто эти свойства выделяются как что-то уникальное.
Но разве в Java нет горутин? Разве Python какой-то старый? Разве Rust не быстрее? Мои коллеги не правы, потому что Go уникален не горутинами, а тоталитарностью.
В Go вы не будете писать элегантный код, не будете писать красивый код, не будете делать магию.
В Go нет наследования, поэтому мы будем использовать композицию.
# В питоне есть наследование class MapCache: def get(self, k: str) -> str: ... def set(self, k: str, v: str) -> None: ... class RedisCache(MapCache): def get(self, k: str) -> str: ... def set(self, k: str, v: str) -> None: ...
// А в Go - только композиция и интерфейсы type UserService struct{ cache Cache } type Cache struct{ v map[string]string } func (r *Cache) Get(k string) (string, error) {/* ... */} func (r *Cache) Set(k, v string) error {/* ... */}
В Go нет классов, перегрузок и конструкторов, поэтому мы пишем глупенькие структуры.
// Будь в Go конструкторы, выглядели бы они, наверно, так type User struct{ ID uuid.UUID Name string CreatedAt time.Time constructor(name string){ this.Name = name this.CreatedAt = time.Now() this.ID = uuid.New() } } // Но их нет, поэтому делаем так type User struct{ ID uuid.UUID Name string CreatedAt time.Time } func NewUser(name string) User{ return User{ Name: name, CreatedAt: time.Now(), ID: uuid.New(), } }
В Go нет макросов и метапрограммирования (почти), поэтому мы пишем if err != nil через каждую строчку

В Go вы будете писать одинаковый код, глупый код, скучный код, читаемый код.
И это будет прекрасно.
Горутины, производительность и минималистичность важны, но именно тоталитарность делает Go уникальным.
Под тоталитарностью в данной статье имеется в виду ограниченность Go, которая, например, запрещает разработчикам реализовать одну и ту же фичу 2 разными способами (ну, или наказывает их за это, но об этом позже). Автор, конечно, осуждает любой политический тоталитаризм и использует этот термин лишь для красного словца.
Давайте разберем каждый пункт поподробнее!
Многопоточка Go прекрасна, она прибита гвоздями в язык, в стандартной библиотеке есть все что нужно для синхронизации потоков: мьютексы, атомики, каналы, вейтгруппы.
Но что такое горутина? По определению из go tour горутина это лайттред, который управляется рантаймом Go, в отличие от платформенных тредов, которые управляются операционкой (linux, mac и тд).

У горутин маленький стек, их дешевле и быстрее переключать и за счет этого создать 10к горутин - дешево и легко.
А есть ли что-то подобное в других языках?

В Java 21 появились виртуальные треды, которые... определяются как лайттреды, которые управляются JVM.
Я не утверждаю, что горутины и виртуальные треды одинаковы, у них есть различия на уровне архитектуры, но для бекендера их использование одинаково - и там и там управление идет рантаймом, и то и то работает быстро, и там и там можно "наспавнить" тысячи лайттредов.
А в Python есть корутины, а в Rust есть async функции.
Очевидно, Python корутины - совсем не то же самое что и горутины. Архитектура и модель совсем другие, но решаемая задача та же: платформенные треды - медленно, поэтому нужно что-то быстрое и легковесное.
Да, горутины-корутины есть у всех — но в Go есть только горутины, и это часть тоталитарности.
Имеется в виду, что в Go не получится параллелить код, например, через обычные треды, корутины или что-то еще, если только не менять компилятор или делать что-то подобное. Да и зачем? Горутины прекрасно покрывают все нужды.
К чему это я? Любой современный язык давно понял, что на OS тредах далеко не уехать. Поэтому не убедим мы джавистов или растовщиков переходить на Go только из-за горутин.
Я отлично понимаю, что могу развести святую войну этой темой, поэтому сразу предлагаю начать спор в комментариях о том, кто быстрее, но давайте по порядку.
Если очень грубо классифицировать языки по производительности, я задал бы 3 тира:

Тир 2 - Языки со сборщиком мусора, динамически типизируемые, интерпретируемые*, single-threaded/с GIL*.
* - не обязательное свойство
Да, у JS есть bun и JIT, у Python есть pypy, а PHP, оказывается, может достигать скоростей Java, но средний код на нативном интерпретаторе языка 2 тира почти всегда будет медленнее, чем код языка 1 тира.
Тир 1 - Языки со сборщиком мусора, но уже компилируемые, строготипизируемые, многопоточные.
Swift использует ARC, что не совсем классический сборщик мусора, но освобождает разработчика от ручной очистки хипа, из-за чего он ближе по произвоидельности и удобству к Go и Java, чем к языкам без GC.
Тир 0 - Языки без сборщика мусора.
Важно отметить, что в данном тире разбираются только популярные языки в бекенд разработке общего назначения. Куда запихнуть SQL и 1C - понятия не имею :)
К чему это, снова, я?
Если мы перепишем средний бек с Python на Go, то, почти всегда, получим буст в ~3-5 раз по RAM, CPU, P50 и P99.
Пример бенчмарка тута под спойлером "бенчмарки"
Если перепишем с Go на Rust - буст будет поменьше, P50 и P99, скорее всего, поменяются слабо, зато загрузки CPU и RAM упадут.
Вы можете мне сказать, что Python можно хорошо оптимизировать и если использовать быстрые библиотеки (написанные на Rust и C) типа pydantic, asyncpg и тд, то результат может быть сравним с Go. Но, конечно, на это можно ответить, что и в Go есть быстрые библиотеки (Fiber и Sonic - частично написан, кстати, на asm). Но даже без таких оптимизаций Go будет все равно быстрее - GIL и динамическая типизация накладывают слишком много оверхеда.
Иными словами переход из одного тира на другой дает прирост производительности.
А что внутри тиров? Джава же медленнее голэнга, потому что...
А почему? Реальность такова, что современная джава такая же, как и Go. И там и там есть GC, и там и там компиляция (только в Java она JIT, а в Go - сразу, то есть AOT). И там и там - строгая типизация, всякие оптимизации типа инлайнинга, и там и там лайттреды.
Хороших полноценных бенчмарков я не нашел, кроме одного - это видео от Антона Путры, классного блогера, что делает полноценные бенчмарки с кубером, промом и AWS.

По бенчмаркам Go и Rust видно, что Go быстрее умирает и в среднем тратит заметно больше CPU.

А вот между Java и Go разницы меньше, вроде P99 у Go меньше, но всего на 100 микро секунд, вроде CPU ниже, но всего на 1.5%.
Важно отметить, что тут нагрузки детские, 400 РПС - далеко не предел для Go и Java. Поэтому если вы найдете хорошие бенчмарки - кидайте в комменты, буду очень благодарен!
Получается, что Java и Go одинаковы в производительности? Не совсем, как мне кажется, есть 3 важных различия:
История: Если взять средний Java бекенд и переписать его на средний Go бекенд, то вы получите огроменный буст, потому что средний Java бек будет сидеть на Java 8 (простите), которой уже больше 10 лет, а также использовать тяжелый Spring Boot. В то время как в Go приняты легковесные фреймворки типа Gin или Fiber, а также редко используются ORM. Конечно, со временем это меняется, но динамика, все-таки, осталась.
JIT vs AOT. Благодаря AOT (и не только) гошный сервис быстрее запуститься, меньше скушает ОЗУ на старте. Но джавовский JIT позволяет в рантайме оптимизировать узкие места и делает он это крайне успешно.
Идентичность с точностью до фреймворков. Разница между Go и Java будет крайне заметной, с точностью до фреймворков и библиотек. Используете не настроенный Java Spring Boot, против Go Fiber - Java проиграет.
ОЧЕНЬ грубо говоря, представим, что у вас есть эндпоинт, и гошный бек выдает на нем P99 - X ms. Тогда джавовый холодный такой же функционально бекенд будет выдавать 1.01*X ms, но при нагрузке задержка упадет до 0.99*X ms.
Опять же, если у вас есть бенчмарки и дополнения - скиньте! Обязательно добавлю в статью
Подводя итог - хороший быстрый бекенд можно написать как на Java, так и на Go, так что наших любимых джавистов мы не переубедим, что Go круто только из-за производительности
Потому что преимущества Go перед Python и JS очевидны - производительность и строгая типизация, что уменьшает потенциальные баги в продакшене. А вот сравениние с Java уже не такое очевидное :)
Go прекрасно чувствует себя в куберах и докерах. Он компилируется в маленький standalone binary, не требует зависимостей (почти), запускается быстро, кушает мало ОЗУ.
Как-то я проводил анализ в своих проектах, изуч��л размеры докер образов одинаковых бекендов. Получилось, что Python, в среднем, кушает под 500 МБ, Java - 300, а Go - 100.
Это лишь мои наблюдения в разных командах и проектах. Хорошей статистики по этому вопросу я, к сожалению, не находил
Кстати, да, у Go можно срезать еще 70 МБ, если использовать образ distroless, у джавы под 100МБ, но динамика останется той же - Go занимает меньше всего места.
Получается очень удобно, но... Так ли это нужно? Нужно ли вообще сокращать расходы на container registry, где хранятся наши образы? Да в целом нет, редко найдется комманда в которой расходы на него будут превышать 1% от общих расходов на инфраструктуру. А если и найдется, то вы точно что-то делаете не так :)
Важно отметить, что Go сияет в разработке самой инфрастуктуры, Виктория Метрикс, Докер, Кубер, Бек графаны - все написано на Go. Малый бинарь, быстрый старт - идеально для таких задач.
Но если мы делаем обычный продуктовый бекенд, то так ли нам нужен Go?
Скажем, делаем мы 3 микросервиса на 3 подах, каждый кушает по 100МБ RAM и нагружен на 1-3 РПС. Java бы тут скушала намного больше, но... Но зачем вам тогда микросервисы? Не проще ли пихнуть все в монолит и получить еще меньший сумарный след?
Но тогда и Java будет выдавать тот же результат, потому что прогретая джава сравнима по RAM с Go. Пилить микросервисы на каждый чих - это скорее антипаттерн, лучше делать микросервисы побольше. Важно отметить, что Go, как мне кажется, самый удобный из всех популярных ЯП для бекенда, но это лишь вопрос удобства и процессов.
Иными словами не убедим мы джавистов, что надо переходить на Go :)
Но тоталитарность Go и тут прослеживается. Go компилируется только в standalone-binary, никакой интерпретируемости или JIT, никаких подключаемых в runtime зависимостей и тд.
Ну, кроме как Сишных зависимостей. Гошные же библиотеки вкомпилируются в бинарь сразу, стабильного ABI или каких-либо еще систем подключения зависимостей после компиляции нет
Плагины? Чисто технически в Go есть режим компиляции в плагины (что-то вроде .so из мира C++), но они урезаны, не очень стабильны, не работают на Windows (и хорошо), а также могут вызывать краши, если вдруг плагин был собрал в одной версии Go, а код запускается через другую. Да и я ни разу не видел, чтобы их кто-то использовал :)
Go - супер современный язык, в нем встроенно все что душе угодно:
Package manager
Система тестов
Линтеры
Унифицированный стиль написания кода
Горутины
Встроенная библиотека большая, тут и все возможные сериализации, встроенный http сервер и клиент, работа с массивами и маппами и тд
В Go легко сделать бекенд только на STD либе.

Но это лишь необходимое условие для популярности языка. Мы, ленивые разработчики, не хотим писать CMakeList как на 1 курсе ВУЗа, мы хотим сразу все готовенькое и если завтра выйдет язык без менеджера пакетов, он будет при��оден только для сессий развлекательного программирования на ютубе, не для энтерпрайза.
При этом тоталитаризм и тут не потерялся. Единый package manager и система тестов уменьшают разнообразие подходов, зачем использовать кастомные тесты, если есть стабильные стандартные?
Аналогичная ситуация прослеживается со встроенной библиотекой, например, с net/http клиентом и сервером. Зачем искать third-party решения, если есть стандартные, которые и так все используют :)
Да, есть Fasthttp и Fiber, но они достаточно не популярны. Например, Traefik, Caddy и Victoria Metrics все используют net/http
Еще интересный факт - Fasthttp был создан создателем Victoria Metrics, но в ней он не используется, хотя казалось бы :)
Go минималистичен, Go строг, Go однообразен, в Go всего 25 ключевых слов (против 35 в Python, 50 в Java, 90 в C++)

В Go нет наследования, поэтому вы не сможете сделать мультинаследование и гадать какие атрибуты есть у класса.

В Go не обязательно все оборачивать в static public final class
В Go не пописать красивых макросов
В Go не обернуть обьект в Box<Cow<Arc<RefCell<'static str>>>>
Вы скажете "Но это же минус!"
А давайте разберем конкретный пример
Скажем, есть у нас класс UserService, которому нужен кеш. Вначале сделаем кеш простой, на одной мапе без мьютексов.
type UserService struct{ cache Cache } type Cache struct{ v map[string]string } func (r *Cache) Get(k string) (string, error) {/* ... */} func (r *Cache) Set(k, v string) error {/* ... */}
Проходит время, мы понимаем, что разным классам нужен разный кеш, где-то тредобезопасный, где-то на редисе. В Go мы можем решить нашу задачу только 1 путем - интерфейсы и композиция: создаем Cache interface с методами Get и Set и реализуем наши кеши.
type Cache interface{ Get(k string) (string, error) Set(k, v string) error } type MapCache struct {/* ... */} type MapCacheThreadSafe struct {/* ... */} type RedisCache struct {/* ... */}
В реальном кеше, конечно, стоит добавить context.Context, TTL, инициализацию и закрытие
Как же это можно в Python?
Можно, например, сделать на протоколах, по сути, тех же неявных интерфейсах, что и в Go.
@dataclass class UserService: cache: Cache class Cache(Protocol): def get(self, k: str) -> str: ... def set(self, k: str, v: str) -> None: ... class MapCache: def get(self, k: str) -> str: ... def set(self, k: str, v: str) -> None: ... class RedisCache: def get(self, k: str) -> str: ... def set(self, k: str, v: str) -> None: ...
Можно на абстрактных классах, говоря на гоферском - явных интерфейсах.
class Cache(abc.ABC): @abc.abstractmethod def get(self, k: str) -> str: ... @abc.abstractmethod def set(self, k: str, v: str) -> None: ... class MapCache(Cache): def get(self, k: str) -> str: ... def set(self, k: str, v: str) -> None: ... class RedisCache(Cache): def get(self, k: str) -> str: ... def set(self, k: str, v: str) -> None: ...
Можно на наследовании 2 способами: создать BaseCache (де-факто абстрактный класс), либо от MapCache отнаследовать RedisCache
class MapCache: def get(self, k: str) -> str: ... def set(self, k: str, v: str) -> None: ... class RedisCache(MapCache): def get(self, k: str) -> str: ... def set(self, k: str, v: str) -> None: ...
Либо мы можем просто проигнорировать типизацию, это же Python :)
Он ограничивает. Мы, гоферы, на код ревью будем спорить про названия переменных, расположение модулей, инвалидацию кеша, но не про наследование против композиции. Golang уменьшает количество конфликтов между разными разработчиками. В свою очередь это позволяет проще перекатиться из одного проекта в другой, проще поконтрибьютить в опенсорс, посаппортить чужой старый код.
Go однообразный, одинаковый, простой, тоталитарный.
Тут вы можете сказать, что в Go есть embedding и данную задачу можно решить через него. Но... Нельзя. Embedding не позволит нам переопределить методы уже существующего класса, только зашедуить их. Поэтому нам в любом случае придется делать интерфейс, реализовывать его и использовать в UserService.
Тот же Python тоже минималистичен, но Go идет еще дальше - он ограничивает.
В Python есть прекрасный дзен с 2 хорошими правилами.
$ python3.14 -c "import this" | head -n 5 | tail -n 2 Explicit is better than implicit. # Явное лучше неявного Simple is better than complex. # Простое лучше сложного
Go возводит их в абсолют.
Хотите распаралелить код? Только горутины. Вам не нужно проверять, что нужный вам коннектор к новой базе данных работает на горутинах, потому что в Go есть только горутины.
Интересное следование. Не будь в Go сразу горутин, а только треды (как в Python и Java изначально), со временем пришлось бы их (или что-то аналогичное) добавить, что привело бы к зоопарку решений, прям как в Java и Python. А это в свою очередь нарушило бы тоталитарность и единство.
Хотите заимпортить пакет? Значит указываете весь путь до репозитория и импортите сам модуль. Вы не можете заимпортить только конкретную функцию или обьект.
# Python from pydantic import BaseModel from pydantic import * # Можно заимпортить все из модуля, только модуль, что хотите!
// Go import "github.com/pkg/errors" // Нельзя импортунуть только метод "New" errors.New("new error!")
Опять же, меньше споров на тему стиля!
Имеется в виду, что часть питонистов выступают против import *, а Go решает эту проблему - просто нельзя заимпортить через звездочку!
Все языки умеют говорить на C и Go тут не исключение, через CGO можно запускать сишный код, но сам CGO имеет оверхед, поэтому в узких местах, возможно, сишный код будет лучше даже переписать на Go
CGO не стоит использовать, например, если вы очень часто вызываете очень маленькую функцию. CGO лучше подойдет, скажем, с батчами, когда делается сразу много операций в пространстве C
А если неосторожно влезть в подкапотную Golang, то можете оказаться на публичной доске дураков (в оригинале "hall of shame")! В стандартной библиотеке есть такая функция - runtime.throw, она доступна только из пакета runtime. Сама функция является чем-то вроде суперпаники, останавливает процесс, пишет ошибку, отловить нельзя. Но чисто технически с ней можно слинковаться и использовать в сторонних библиотеках.

И это плохо! Мейнтейнерам языка Golang это не нравится, поэтому в самих исходниках Go есть список библиотек, которые находятся в официальном зале позора из-за использования указанной функции.
Например, туда попали:
xray-core - VPN протокол
simdjson-go - minio JSON парсер
pebble - хранилище, используемое в CockroachDB
Как мы видим, Golang явно дестимулирует пользователей лезть во что-то сложное и внутреннее
Как я понял, проблема заключается в том, что разработчики Go могут хотеть поменять сигнатуру или логику работы throw, но так как на нее уже кто-то завязался - изменение сломает их проекты. И хоть это не будет нарушением обратной совместимости, потому что это не публичная функция, все равно не хочется так делать :)
Хотите перегрузок операторов? А их нет... Вообще ни перегрузок, ни перегрузок операторов, ни оверрайдов нет.
package main import "fmt" // Пример класса `вектор`, // в котором можно было перегрузить оператор +, будь мы в C++. // Но в Go это сделать невозможно. type Vec3[T int64 | float64] [3]T func (l Vec3[T]) Add(r Vec3[T]) Vec3[T] { return Vec3[T]{l[0] + r[0], l[1] + r[1], l[2] + r[2]} } func main() { vec1 := Vec3[int64]{1, 2, 3} vec2 := Vec3[int64]{4, 5, 6} fmt.Printf("%+v\n", vec1.Add(vec2)) // not vec1 + vec2 }
Хотите выполнить какой-то код на compile time? Неа, не получится, максимум что есть - кодогенерация.
Хотите перечисления (enum)? Нет не хотите. В го нет ни только алгебраических енамов, в го нет даже обычных енамов. Идеоматичный (то есть верный) способ - создать свой alias-тип и константами обьявить перечисления, но такой подход не может дать гарантию, что кто-то случайно не положен что-то не то в ваш enum.
Можно, конечно, использовать кодогенерацию для автоматического создания сериализации/десериализации, но, опять же, гарантии валидности енама достичь невозможно. Максимум - частично закрыть через линтеры.
Управление потоком в го происходит максимально явно. Исключений нет, только явные ошибки, поэтому всегда видно когда функция может преждевременно завершиться. В го нет аннотаций, макросов или декораторов, которые меняют поведение функции, поэтому если вы видете тело функции, то всегда можете быть убеждены, что именно оно будет выполняться.
import ( "github.com/gofrs/uuid" "github.com/pkg/errors" ) func (r *Service) getUser(id uuid.UUID) (User, error) { userDB, err := r.userRepo.getUser(id) if err != nil { return User{}, errors.Wrap(err, "get user") // Ошибка может быть тут } user := userDB.toDTO() if len(userDB.subscriptions) != 0 { subscriptions, err := r.subscriptionRepo.getSubscription(userDB.subscriptions) if err != nil { // Ошибка может быть и тут return User{}, errors.Wrap(err, "get subscriptions") } user.subscriptions = subscriptions if user.balance < 0 { // А тут может быть паника panic("user invalid state") } } return user, nil // А тут все ок! }
Да, в Go есть panic, который работает похоже на исключения и чисто технически можно его так и использовать. Я даже находил библиотеки, что panic так и используют.
Но есть важный ньюанс - в самом комьюнити Go не принято использовать панику, даже если очевидно, что какая-то ошибка невозможна. Например, зачастую можно гарантировать, что сериализация JSON будет всегда успешной, но ошибка из json.Marshal разработчиками, в основном, все равно обрабатывается, а не паникуется.
Гоферы очень любят слово "идиоматично". Что же это значит? Это правильно. Не быстро, эффективно, адекватно, просто правильно. Почему правильно? Потому что так принято.
Что же дает тоталитарность гоферам?
Легкость перекатываний. Вас заставили поддерживать чужой старый монолит? Хотите исправить в баг в опенсорс либе? Нужно заонбордить джуна? С Go это проще, он тоталитарный.
Минимизация конфликтов. Меньше споров на ревью про паттерны! Теперь спорим только про переменные и инвалидацию кеша.
Производительность. Как это странно бы ни звучало, но из-за соблюдения обратной совместимости, обновить версию вашего гошного проекта с 1.18 до 1.26 - крайне просто. А новые версии часто ведут за собой оптимизации, например, новый GC. Также еще одно странное явление гоферов - крайняя нелюбовь к ORM, из-за чего разработчики частенько самостоятельно пишут SQL запросы, что тоже, скорее, позитивно сказывается на производительности.
Кстати. Вайбкодинг с Golang, наверно, самый простой из всех существующих языков. Это связанно с той же тоталитарностью, а также с крайне высокой стабильностью языка, обратная совместимость в Go религиозно защищается, из-за чего даже если ЛЛМ обучалась на старенькой 1.18 версии Go, она и в 1.26 версии будет писать корректный код
Тоталитарность Go не случайна. Go, де-факто, создавался Гуглом для решения проблем Гугла - тысячи разработчиков, проекты постоянно закрываются и открываются, нужен язык, который будет быстрым, простым и разрабов будет легко перекидывать между коммандами.
Собственно так и получилось :)
Очевидно, такая тоталитарность создает массу проблем, бесконечные if err != nil, могут легко захламить код и замазать глаз, отсутствие дженериков до 1.18 версии (я даже не буду объяснять в чем тут проблема) и другое. Отсутствие enum иногда тоже выстреливает неожиданными багами, да и многопоточка могла бы быть побезопаснее
Например, как в Rust, где мьютексы закрывают доступ к конкретному объекту и не позволяют к нему обратиться без Lock, а не просто существуют в вакууме как в Go.
Да и отстувие stack trace в стандартных ошибках - крайне странное решение, которое приводит к third-party библиотекам типа pkg/errors или cockroachdb/errors и небольшому, но все же зоопарку обработки ошибок.
Написанием этой статьи я не хотел вам "продать" Go, я лишь хотел обратить ваше внимание на особенность Go, которую я считаю самой важной.
Спасибо за внимание
Обязательно оставляйте комментарии, буду рад с вами поспорить!