Как стать автором
Обновить
165.2
2ГИС
Главные по городской навигации

Как функциональщик в Go ушёл… и не вернулся

Уровень сложностиСредний
Время на прочтение7 мин
Количество просмотров9K

Хороших технических статей про Go было написано немало, и эта — не одна из них. Эта статья — графомания о моём субъективном и эмоциональном опыте перехода со Scala на Go.

Руководитель: Хочешь техлидить новый проект?

Я: Да, конечно. А что за проект?

Руководитель: Распределённые бэкенды на Go.

Я: Go? Но я же скалист-функциональщик...

Чуть позже.

Коллега: Слышал, что ты будешь техлидить другой проект — вы там тоже Scala завозить будете?

Я: Нет, будем писать на Go.

Коллега: Ты что, бросаешь Scala?!

Этот момент мне запомнился очень хорошо. Когда ты долго работаешь с каким-то языком, накапливаешь экспертность, нюансы, грабли, привыкаешь жить в его экосистеме — смена стека кажется чем-то болезненным. Будто ты уезжаешь в другой город и оставляешь старых друзей.

Да, конечно же, язык — просто инструмент, а реальная компетенция — в теории, паттернах и опыте решения определённого рода задач, которые копятся за годы работы. Тяжело менять классы задач, но менять инструментарий гораздо легче.

Go и моя первая реакция: раздражение

Go смог зарекомендовать себя как удобный и эргономичный инструмент, ориентированный на разработку веб-сервисов и инфраструктурных штук. На Go написаны инструменты, без которых сложно представить мир распределённых систем: Kubernetes, Docker, Istio, CockroachDB, Badger, Prometheus, VictoriaMetrics, Temporal, NATS. Меня всегда соблазняла мысль выкачать их репозитории к себе на машину и забуриться в их внутреннее устройство. Ведь что может быть интереснее распределённых систем и баз данных?

Но всё равно долгое время меня отталкивала простота Go. Я несколько раз пробовал писать на нём, но каждый раз чувствовал, что меня держат за идиота. В мире, где языки развиваются стремительно, предлагают мощные системы типов и элегантные абстракции, Go выглядел упрямым и снисходительным к разработчику. Будто он не уважает разработчика и игнорирует весь прогресс за последние 30 лет. Дженериков на тот момент не было, обработка ошибок через какие-то if err != nil, ручками надо писать простейшие вещи, а язык явно поощряет максимально приземлённый императивный подход.

Мне казалось, что будущее — в Haskell, Erlang, Smalltalk, Lisp. Там, где бóльшая выразительность и лучший контроль над абстракциями.

… и что-то поменялось

Однако чем гибче инструмент, тем больше ты тратишь времени и сил, чтобы придумать себе рамки. Без этого сложно управлять растущей сложностью решений.

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

Поэтому вскоре я понял простую вещь: сила в ограничениях.

Раньше казалось, что если ты умеешь строить технически сложные системы, значит, ты хороший инженер. Но чем дальше, тем больше я видел другой тренд: хорошие инженеры не те, кто способен разобраться в хитросплетении моноидов и функторов, а те, кто может написать систему так, чтобы через полгода в ней было легко разбираться и адаптировать её под новые обстоятельства.

И Go про это.

Первые недели работы с Go были странными. Я написал простой императивный код, без монад и фреймворков, и он просто сработал. Без архитектурной выверенности и слоев абстракции, но с нужным результатом.

Ровно в этот момент я ощутил свободу и удовольствие от того, что делаю. Я могу просто писать код и решать свою задачу, не думая о том, как работают 20 слоёв абстракций между мной и отправкой запроса в базу данных или в соседний сервис. Я больше не ломаю голову, какие абстракции использовать. Нет ощущения выбора «из десяти правильных способов». В Go код будто сам говорит, каким ему быть.

Go: философия прагматизма

Есть отличная статья (перевод), в которой сравниваются два принципиально разных инженерных подхода:

  1. The right thing — о том, что простота интерфейсов важнее простоты реализации, корректность поведения обязательна во всех возможных сценариях, усложнение интерфейса допустимо для сохранения консистентности дизайна. Один из эталонных примеров — Haskell, девиз которого — фраза-мем “Avoid success at all costs”. Нет, тут не про то, чтобы всеми силами избегать успеха, а про то, чтобы не идти на компромисс с тем, как правильно.

  2. Worse is better делает ставку на простоту реализации. Интерфейс может быть неидеальным — главное, чтобы код был прост в поддержке. Корректность — желательна, но допустимы компромиссы. Консистентность — хорошо, но не всегда обязательна. Главное — закрыть большинство ситуаций минимальными усилиями. Worse-is-better сильно пересекается с принципом Парето и предпочитает простую 20% реализацию 80% релевантных кейсов. И Go — целиком и полностью язык про это.

По большому счёту, Go вообще не претендует на умность и красоту. Он про: «Сделай дело. Просто напиши код. Возможно, потом исправишь, но не надо превращать его в искусство ради искусства».

Несколько примеров:

  • Обработка ошибок. В других языках у тебя исключения, try/catch, иногда монады (Either или Try). В Go у тебя if err != nil. И всё — максимально простая концепция с точки зрения реализации языка, закрывающая 80% потребностей. Сначала ты думаешь: «Да вы шутите?», потом смиряешься, а потом вообще перестаёшь об этом думать — потому что это просто работает.

  • Асинхронщина и планировщик. В Go не стали тащить концепцию «окрашивания функций» (подробнее — статья и перевод), когда функции делятся на синхронные и асинхронные, и первые могут вызывать только себе подобных. В других языках это часто приводит к «заражению» кода асинхронностью и дублированию API для синхронных и асинхронных вариантов. В Go же любая функция может быть вызвана как синхронно, так и асинхронно, что упрощает понимание кода и избавляет архитектуру от лишней сложности, связанной с async/await-логикой. Взамен разработчикам нужно разбираться в нюансах работы горутин, каналов и примитивов синхронизации, но эти знания требуются не так часто и не усложняют основную часть кода.

А как я приспособился?

Я всегда считал себя человеком, который ценит функциональный стиль. Иммутабельность, безопасная работа с состоянием, никаких побочных эффектов, чистые функции, простота — вот это вот всё. И мне казалось, что Go — это чистый императив, и ради него мне придётся отказаться от всего, что я любил в Scala.

Неожиданно для себя я нашёл в Go знакомые подходы, во многом похожие на Rust.

Во-первых — иммутабельность.

В Rust есть концепция владения (ownership), которая заставляет разработчика принять явное решение: разрешать функции менять переданный параметр или нет, передать право владения значением или не передавать. Но Rust — это про the right thing. В Go мы же про worse-is-better. Здесь нет жёстких гарантий, но можно придумать практику: если структура передаётся по значению, то это негласное обещание — не менять её внутри функции. Если функция возвращает значение, а не указатель, то она отдаёт свой результат целиком и полностью во владение получателя, гарантируя, что никто другой не будет менять это значение.

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

func PureFunc(value T) U

Сомнительно, но окей. Мы получили способ сказать другим разработчикам: «Эти данные неизменны, не трогайте их». Чистые функции по договорённости на месте, и это уже замечательно.

Вторая идея касается «окраски функций».

Да, её отсутствие — это плюс. Но всё же иногда хочется явно отделять функции, которые делают IO и работают с внешним миром. Это помогает держать в голове архитектурные границы. И это тоже можно получить через договорённость и конвенцию. В стандартной библиотеке Go есть замечательный context.Context, который как раз и нужен для обработки таймаутов и отмены IO-операций.

func IOFunc(ctx context.Context, val T) (U, error)

Я это осознал только спустя некоторое время. Но когда понял, у меня щёлкнуло: в Go я всё ещё могу писать код, который легко тестируется, которым просто оперировать, с чётким разграничением между чистыми и функциями с побочными эффектами — и всё это делать без монад и tagless final (со всем уважением к ним).

Вот тогда Go окончательно меня убедил.

Но есть нюанс ...

Go действительно простой и понятный — на первый взгляд. Но есть тонкости, в которых интуиция новичка легко подведёт. Например, слайсы, мапы и каналы в Go — ссылочные типы. В их объявлении это не отображается, и об этом нужно просто помнить. Как следствие: из nil-мапы можно получить значение, но попытка записать в неё вызовет панику.

Да, такие вещи прописаны в документации, но это часто не помогает. Всё-таки мы лучше всего учимся через взаимодействие и ошибки.

Другой пример боли: это если тебе нужно построить хоть сколько-нибудь нетривиальный асинхронный пайплайн обработки данных на каналах. Горутины и каналы — мощный инструмент. Но вместе с ними приходят сложности: утечки, гонки, непредсказуемая синхронизация. А если добавить WaitGroup, Mutex и context — становится совсем непросто. Всё это требует дисциплины, иначе легко наступить на грабли.

Go даёт простые примитивы конкурентности, но готовить их правильно — задачка не из лёгких. Редко когда можно увидеть действительно полностью корректную реализацию алгоритма, задействующего context.Context, sync.WaitGroup, sync.Mutex и несколько каналов для обработки данных и агрегации ошибок.

Главная сила Go — в отсутствии магии и абстракций. Но от разработчика требуется зрелость.

Просто попробуй (возможно, ещё раз)

Недавно я попробовал снова запустить проект на Java и Spring. Зашёл на start.spring.io, создал скелет, поднял всё у себя на машине. JVM там уже давно не было, пришлось повозиться. И уже в процессе я понял — продолжать не хочется. Go успел меня избаловать быстрой обратной связью. От идеи до работающего результата — всего пара шагов: простой запуск, моментальная компиляция, отсутствие лишних абстракций.

Надо ли срочно всё бросать и уходить в Go? Конечно, нет, любая смена инструмента должна быть осмысленной. Но если ты работаешь с Java или .NET — просто попробуй. Не подходи к этому с позиции «Что здесь не так?», а просто дай себе время привыкнуть, почувствовать, как меняется твой стиль работы.

Для меня Go стал языком, который освободил голову от лишнего. Он помог мне сосредоточиться на архитектуре, продуктах, бизнес-логике, не тратя ресурс на борьбу с языком.

В Go я перестал усложнять. Ушли лишние слои, мысли стали яснее. Я всё ещё люблю функциональное программирование. Но Go помогает сосредоточиться на главном: создавать ценность и не мешать себе это делать.

Может быть, тебе тоже зайдёт?

Теги:
Хабы:
+39
Комментарии68

Публикации

Информация

Сайт
2gis.ru
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Россия
Представитель
Наталья Акберова