Как мы себя обманываем, только бы продолжать пользоваться Golang

в 13:04, , рубрики: Go, Rust, высокая производительность, Исследования и прогнозы в IT, легаси, Программирование, Разработка под Linux, языки

За два года с тех пор, как я опубликовал статью I want off Mr Golang's Wild Ride, она вновь и вновь всплывала на Reddit, Lobste.rs, на HackerNews и в других местах.

Всякий раз дискуссия выходит к одним и тем же ответам:

  • Вы говорите о Windows: а ведь как раз с Windows язык Go не слишком хорош! (Опять же, кого это волнует?)

  • Вы однобоки: о сильных сторонах Go вы не говорите!

  • Вы не понимаете тех компромиссов, на которые мы идем в Go.

  • Крупные компании используют Go, значит, не так уж он и плох!

  • «Правильно» моделировать задачи очень затратно, поэтому забота о корректности – спорный довод.

  • Корректность всегда бывает в каком-то диапазоне, Go позволяет частично пожертвовать ею ради ускорения разработки.

  • Вы сразу ссылаетесь на Rust, но и у него есть свои недостатки, так что ваш аргумент никуда не годится.

  • Т.д.

Среди комментаторов есть и громогласная фракция тех, кто всем сердцем соглашаются с моей филиппикой, но давайте сосредоточимся на разборе того явного конфликта, который здесь просматривается.

Сначала я потрачу немного времени и обрисую откровенно слабые аргументы, чтобы сразу отмести их, а затем перейду к более честным комментариям и отвечу на них настолько качественно, насколько смогу.

Автор - утконос

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

Например, на момент написания статьи верхний комментарий на HackerNews начинается со слов:

«Автор в корне не понимает, как построены языки программирования».

Я люблю синдром самозванца, поэтому, как правило, мне близки такие комментарии. Но такая реакция на какую бы ни было обратную связь – ленивая и нигилистская.

Не нужно большого умения, чтобы заметить проблему.

Фактически, по мере того, как разработчик приобретает профессиональную зрелость, он становится склонен игнорировать все больше и больше проблем, поскольку просто привыкает к ним. Так делалось всегда, и люди просто сживаются с некоторыми проблемами, поэтому перестают задавать о них вопросы.

Но разработчики-джуниоры всякий раз всё рассматривают с широко распахнутыми глазами: они еще не научились игнорировать всех причуд, поэтому им от них неуютно, и они склонны их оспаривать (если почувствуют себя в достаточной безопасности и осмелятся озвучить, что их не устраивает).

Одна только эта причина уже достаточно веская, чтобы нанимать джуниоров, и мне бы хотелось, чтобы как можно больше компаний брали их на работу, а не упирали на факт, что «сеньор быстро войдет в темп и сориентируется в той неразберихе, что у нас сейчас творится».

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

На самом деле, я спроектировал язык еще в 2009 году (по меркам программирования, тогда я был еще плаксивым грудничком), суть которого сводилась в обваливании С в синтаксическом сахаре. На тот момент этот язык сочли достаточно интересным и даже пригласили меня на OSCON (тогда я впервые оказался в Портленде, штат Орегон, столице кофе, гранжа, ненастья и белых людей), где мне довелось встретиться с другими юными и не столь юными молокососами (работавшими над Io, Ioke, Wren, JRuby, Clojure, D, Go, т.д.)

Это была очень интересная конференция: я до сих пор глубоко стыжусь той презентации, с которой выступил, но ностальгически вспоминаю, как кто-то из слушателей задал вопрос команде Go: «Почему вы решили проигнорировать все исследования в области систем типов, проведенные с 1970-х»? В тот момент я не вполне понимал, что следует из этого вопроса, но уверен, что понимаю сейчас.

С тех пор я совершенно забросил мой авторский язык, поскольку стал присматриваться к семантике гораздо внимательнее, чем к синтаксису – по этим же причинам я также не интересовался Zig, Nim, Odin, т.д.: меня более не интересует проект «напишем еще один C, только лучше».

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

Мама курит, так может, это нормально?

Среди наименее эффективных способов затариваться технологиями (а приобретением технологий приходится регулярно заниматься техническим директорам (CTO), вице-президентам по инженерии, начальникам, руководящему звену и просто старшим программистам) – посмотреть: а что там используют в других компаниях.  

Это отличный способ открывать для себя новые технологии для последующей оценки (как вариант – можно читать Tech Radar от ThoughtWorks), но этого далеко не достаточно.

Опус компании X о том, "как мы использовали технологию Y" лишь очень редко отражает, какой ценой в компании удалось освоить данную технологию. До того, как программистов (авторов поста) силой заставят написать такой текст в корпоративный блог, успела закончиться многомесячная неравная битва, было принято техническое решение, и пути назад уже нет.

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

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

Есть, конечно, и некоторые исключения. Например, блог Tailscale настолько искренний, что кажется глотком воздуха. Но, прочитав у них такие статьи как netaddr.IP: a new IP address type for Go или Hey linker, can you spare a meg?, можно отреагировать по-разному.

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

Или вы можете ужаснуться, осознав, что все эти сложные проблемы существуют только потому, что используется Go. Такие сложные проблемы нехарактерны для других языков, ни даже для C, который определенно нельзя обвинить в саморекламе (в частности, его не рекомендуют в качестве замены Go).

Разнообразные проблемы, упомянутые в статье о netaddr.IP, обусловлены следующими причинами:

  • В Go нет тип-сумм — и поэтому там реально неудобно написать тип, который представлял бы «либо адрес IPv4, либо адрес IPv6»

  • Выбор Go по поводу того, какие структуры данных вам понадобятся – в данном случае, речь идет о срезе «один на все случаи жизни», каждый из которых на 64-разрядной машине обойдется вам в 24 байта

  • Go не допускает перегрузки операторов, напоминая о той давней истории Java, когда a == b не было идентично a.equals(b)

  • В Go отсутствует поддержка неизменяемых данных. Единственный способ предотвратить любые изменения каких-либо данных – вручную раздавать их копии, проявляя при этом особую осторожность, чтобы не изменить их в том коде, который фактически обладает доступом к внутренним разрядам

  • Go не слишком способствует вам, если вы хотите написать на нем непрозрачный "newtype". Единственный способ это сделать – написать отдельный пакет и обеспечить опосредованность при помощи интерфейсов, что дорого и неудобно

Если вы только не решили заранее, что вас не переубедить, то вся эта статья – один весьма убедительный довод против использования Go для решения конкретной задачи, описанной в посте.

Тем не менее, Tailscale пользуется Go. Это ошибочное решение? Совсем не обязательно! Ведь их команда состоит из экспертов по Go. В чем можно убедиться, прочитав другую их статью, о линковщике Go.

Поскольку эти люди – эксперты по Go, они заранее знают, чего им будет стоить работа с Go, поэтому обладают необходимой компетенцией, чтобы обоснованно решить, стоит или нет прибегать к Go. Они глубоко разбираются в том, как работает Go (именно те аспекты, о которых маркетинг Go клянется и божится «да вам же этим никогда не придется заморачиваться, зачем же вы спрашиваете?), поэтому, столкнувшись с пограничными случаями, они смогут углубленно изучить каждый случай, исправить проблему и дождаться, пока их фикс будет передан наверх (если вообще будет передан).

Но весьма вероятно, что вы не из таких экспертов. Это не ваша организация. Также у вас не Google, поэтому вы не можете позволить себе выстроить целую новую систему типов поверх Go только для того, чтобы ваш проект (такой как Kubernetes) вообще заработал.

Но есть и хорошие новости

Ну ладно, но ведь Tailscale продолжает пользоваться Go до сих пор. Можно вспомнить случай, когда в 2020 году мой пост о Windows вызвал рой скептических замечаний «но ведь Go для этого не слишком годится» — точно так можно отмахнуться и от постов Tailscale как «ну, они же сами захотели сдавать такой код для iOS / заниматься низкоуровневыми сетевыми операциями».

Весьма честно! Окей. Давайте поговорим о тех вещах, в которых Go блистает.

Go обладает довольно хорошей асинхронной средой выполнения, в которой предусмотрены жесткие (opinionated) умолчания, ультрасовременный сборщик мусора с двумя переключателями и инструментарий, которому позавидовали бы специалисты по C, если бы удосужились выглянуть за пределы своего мирка.

Все это также характерно для Node.js с момента его появления (в сущности, он представляет собой libuv + V8), и, как я полагаю, также касается «современной Java» с такими API как NIO. Хотя я и не проверял в деталях, что творится на территории Java, вы можете вообще не читать эту статью, если хотите попридираться к мелким неточностям: она же бесплатная.

Поскольку асинхронная среда выполнения образует ядро языка, в нагрузку к ней предлагается и инструментарий, которому в самом деле завидуют Rust-разработчики! Я рассказываю об этом, например, в статье Request coalescing in async Rust.

В настоящее время в Go не составляет труда сделать дамп обратной трассировки (вывести все стектрейсы) для всех действующих горутин, так, как tokio пока не делает. В Go также заложена возможность обнаружения взаимных блокировок, встроен собственный профилировщик, по-видимому, в этом языке вам не придется беспокоиться по поводу цвета функций, т.д.

Легко подхватить и полюбить инструментарий Go, обеспечивающий управление пакетами, рефакторинг, кросс-компиляцию; на первый взгляд этот арсенал определенно воспринимается как серьезный шаг вперед на фоне многочисленных человеко-часов, потраченных на борьбу с капризами pkg-config, autotools, CMake, т.д. До тех пор, пока вы не столкнетесь с какими-то как ниоткуда возникшими ограничениями, просто не удостоившимися внимания со стороны команды Go — и вы остаетесь с этими проблемами один на один.

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

Но один реально хороший компонент погоды на платформе еще не делает.

По-настоящему удобная асинхронная среда выполнения не единственная вещь, которую вам придется принять. Также понадобится освоиться с очень своеобразным тулчейном, системой сборки, соглашением об именованиях, единственным сборщиком мусора (независимо от того, подходит он вам или нет), а также с набором «готовых батареек», притом, что некоторые из них МОЖНО поменять на другие, а оставшуюся часть экосистемы – нет. Самое важное, что вы беретесь за работу на языке, который возник случайно.

Соглашусь с вами, что чрезмерная озабоченность чем-либо оставляет почву для подозрений. Не секрет, что значительная часть современного академического багажа безнадежно неприменима в индустрии; легко углубиться в аннотацию и получить на выходе натянутые схемы решения задач, которые на самом деле не существуют ни для кого, кроме автора статьи.

Думаю, именно такие ощущения у кого-то возникают от Rust.

Но излишне поверхностное отношение к чему-либо также опасно.

Очевидно, команда Go не собиралась проектировать язык. Им по-настоящему нравилась лишь их асинхронная среда выполнения. Они хотели добиться, чтобы у них получилось реализовать поверх нее TCP, HTTP, TLS, HTTP/2, DNS, etc., т.д. А над всем этим – ещё и веб-сервисы.

Но они этого не сделали. Вместо этого они спроектировали язык. Просто из разряда «он взял и получился» didn't design a language. It sorta just "happened".

Поскольку этот язык должен был казаться знакомым «недавно выпустившимся из университета гуглерам, уже в какой-то степени изучавшим Java/C/C++/Python» (Роб Пайк, Lang NEXT 2014), в Go оказались заимствования из всех этих языков.

Точно как и C, Go вообще не утруждает себя обработкой ошибок. Весь код – большой пушистый ком изменяемых состояний, и именно вы отвечаете за то, чтобы ОЧЕНЬ ОСТОРОЖНО (и только вручную) добавлять в него if-ы и else-ы, а также заботитесь о том, чтобы недействительные данные никуда не просачивались.

Go, точно как и Java, пытается стереть различия между «значениями» и «ссылками», поэтому (при рассмотрении с места вызова) невозможно судить о том, будет некоторая сущность меняться или нет.

import "fmt"

type A struct {
	Value int
}

func main() {
	a := A{Value: 1}
	a.Change()
	fmt.Printf("a.Value = %dn", a.Value)
}

В зависимости от того, какова сигнатура изменения – такая:

func (a A) Change() {
	a.Value = 2
}

или такая:

func (a *A) Change() {
	a.Value = 2
}

...локальное a в main либо будет меняться, либо не будет.

Соответственно, точно, как в C и Java, у вас не будет возможности решать, что изменяемо, а что неизменяемо (ключевое слово const в C, по сути, является рекомендательным, вроде того), и передавать ссылку чему-либо (например, во избежание затратного копирования) всегда рискованно, так как изменение может произойти без вашего ведома, либо структура данных будет где-то удерживаться вечно, так, что вы не сможете ее высвободить (меньшая, но весьма неиллюзорная проблема).

Go не в силах предотвратить и многие другие классы ошибок; в этом языке ничего не стоит случайно скопировать мьютекс, из-за чего он станет совершенно неэффективным, либо оставить поля структур неинициализированными (или же инициализированными в нулевое значение), что приведет к бессчетным логическим ошибкам.

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

Что совершенно логично, ведь слезть с Go по-настоящему тяжело.

Go - это остров

Если только вы не используете cgo, (но cgo не Go), то ваш мир похож на киновселенную из фильма «План-9».  

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

Go ближе к герметичным языкам, чем к C или C++. Даже Node.js, Python и Ruby не столь враждебны к FFI (интерфейсам внешних функций).

В значительной степени это фича, а не баг – ведь выделяться так круто. Такой подход не лишен достоинств. Когда есть возможность профилировать нутрянку стеков TLS и HTTP с той же легкостью, что и бизнес-логику приложения – это просто фантастика (тогда как в динамических языках стектрейс обрывается на OpenSSL). Ещё такой код в полной мере пользуется удобствами, связанными с отсутствием окрашивания функций: он может переложить на среду выполнения все заботы, связанные с неблокирующим вводом/выводом и планированием.

Но за это приходится ужасно дорого платить. Для многих вещей имеется отличный инструментарий, который с Go неприменим (но применим для работы с теми фрагментами, которые написаны на cgo, но опять же, не следует использовать cgo, если вы хотите Испытать Все Прелести Go). Здесь теряются все «институциональные знания», и приходится переучиваться с чистого листа.

Кроме того, именно по этой причине Go настолько сложно интегрировать с чем-либо еще, будь то в восходящем направлении (вызов C из Go) или в нисходящем (вызов Go из Ruby). В обоих этих сценариях требуется прибегать к cgo, или, если вы отважны до слабоумия, к ужасающим хакам.

Замечание: по состоянию на Go 1.13 сугубо двоичные пакеты больше не поддерживаются

Обеспечение красивого взаимодействия Go с (любым) другим языком – задача действительно сложная. Для вызова C из Go, не говоря уже об издержках на преодоление границы FFI, требуется вручную отслеживать дескрипторы, только бы не сломать сборщик мусора. (В WebAssembly была точно такая же проблема до появления ссылочных типов!)

При вызове Go откуда угодно требуется запихать всю среду исполнения Go (в том числе, сборщик мусора) в любой объект, который вы эксплуатируете: рассчитывайте, что у вас получится очень большая статическая библиотека, и на вас лягут все операционные издержки, возникающие при обращении с Go как с обычным исполняемым файлом.  

Потратив годы на эти пляски с FFI (туда-сюда), я пришел к выводу, что единственная хорошая граница с Go – сетевая.

Интеграция с Go получается относительно безболезненной, если вы можете мириться с задержками, возникающими при вызове удаленных процедур (RPC) по TCP (будь то HTTP/1 API в стиле REST, либо что-нибудь вроде JSON-RPC или более сложная схема, такая, как GRPC, т.д.). Кроме того, только так можно обеспечить, чтобы Go не «заразил» всю вашу базу кода.

Но даже это недешево: вам придется поддерживать инварианты по обе стороны границы. В Rust в таком случае можно было бы прибегнуть к чему-нибудь вроде serde, которая, вкупе с тип-суммами и отсутствием нулевых значений, обеспечивает достаточную уверенность, что у вас в «мешке» действительно «кот»: если число – это ноль, значит, здесь и ожидался ноль, это не какая-нибудь недостача числа.

Все это выходит за рамки, если вы используете некоторый формат сериализации, например, protobuf, обладающий всеми недостатками системы типов Go, но ни одним из достоинств.

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

На этом мы подходим к более крупной общей проблеме – культуре Go.

Все или ничего (так не будем же делать ничего)

Я упомянул о том, что «поля структур остаются неинициализированными». Это легко происходит в случаях, когда мы меняем код примерно со следующего

package main

import "log"

type Params struct {
	a int32
}

func work(p Params) {
	log.Printf("Working with a=%v", p.a)
}

func main() {
	work(Params{
		a: 47,
	})
}

На следующий:

package main

import "log"

type Params struct {
	a int32
	b int32
}

func work(p Params) {
	log.Printf("Working with a=%v, b=%v", p.a, p.b)
}

func main() {
	work(Params{
		a: 47,
	})
}

Вторая программа выводит на экран:

2009/11/10 23:00:00 Working with a=47, b=0

В сущности, мы изменили сигнатуру функции, но забыли обновить то место, где её вызывают. Компилятор это совершенно не волнует.

Довольно странно, но, если бы наша функция была структурирована вот так:

package main

import "log"

func work(a int32, b int32) {
	log.Printf("Working with a=%v, b=%v", p.a, p.b)
}

func main() {
	work(47)
}

Мы получили бы ошибку компиляции:

./prog.go:6:40: undefined: p
./prog.go:10:7: not enough arguments in call to work
	have (number)
	want (int32, int32)

Go build failed.

Почему компилятору Go вдруг стало важно, предоставляем ли мы теперь явные значения? Если бы язык был самосогласован, он позволил бы опустить оба параметра и просто по умолчанию поставил бы вместо них нули.

Дело в том, что один из постулатов Go в том, что нулевые значения хороши.

Смотрите-ка, как быстро с ними  работается. Если вы имели в виду, что b должно равняться нулю, то просто можете не указывать этого.

Причем иногда такой подход в самом деле работает, ведь нулевые значения в самом деле что-то значат:

package main

import "log"

type Container struct {
	Items []int32
}

func (c *Container) Inspect() {
	log.Printf("We have %v items", len(c.Items))
}

func main() {
	var c Container
	c.Inspect()
}
2009/11/10 23:00:00 We have 0 items

Program exited.

Это хорошо! Ведь срез []int32 в самом деле является ссылочным типом, и его нулевое значение есть nil, а len(nil) просто возвращает ноль, потому что, «очевидно», нулевой срез пуст.

А иногда и не хорошо, поскольку нулевые значения могут означать совсем не то, что вам кажется:

package main

type Container struct {
	Items map[string]int32
}

func (c *Container) Insert(key string, value int32) {
	c.Items[key] = value
}

func main() {
	var c Container
	c.Insert("number", 32)
}
panic: assignment to entry in nil map

goroutine 1 [running]:
main.(*Container).Insert(...)
	/tmp/sandbox115204525/prog.go:8
main.main()
	/tmp/sandbox115204525/prog.go:13 +0x2e

Program exited.

В данном случае вы должны были бы сначала инициализировать словарь (кстати, также являющийся ссылочным типом) при помощи make или словарного литерала.

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

  • Операция отправки в канал nil блокируется навечно

  • Операция получения из канала nil блокируется навечно

  • Операция отправки в закрытый канал вызывает панику

  • Операция получения из закрытого канала немедленно возвращает ноль

Поскольку у нулевых каналов должен быть какой-то смысл, именно этот вариант и был выбран. Хорошо, что есть такая штука pprof, помогающая находить взаимные блокировки!

А поскольку нет никакого способа «выйти» за пределы значений, должен быть смысл и в получении из закрытых каналов, и в отправке в них, поскольку даже после того, как вы такие каналы закроете, вы по-прежнему можете с ними взаимодействовать.

(В то же время, в таких языках как Rust канал закрывается, как только его Отправитель отбрасывается, а это бывает лишь в случае, когда больше никто не может снова к нему прикоснуться — совсем. То же, вероятно, применимо и к C++, и к ряду других языков, это не новое явление).

Формулировка «нулевые значения имеют смысл» упрощенная и определенно неверная, если рассмотреть ввод… практически любого рода. Существует так много ситуаций, в которых значения должны быть «одним из этих известных вариантов и ничем иным», что ради таких случаев и появились тип-суммы (в Rust это перечисления).

А в Go ответ на эту проблему – «просто будьте аккуратнее». Так же и в С на это отвечали.

Просто не обращайтесь к возвращенному значению, если не проверили, не является ли оно ошибкой. Держите полдюжины людей, которые будут тщательно отсматривать каждое, даже самое тривиальное изменение кода, чтобы удостовериться, что из кода не станет повсюду (и до самых глубин вашей системы) просачиваться «ничто», нуль или пустая строка.

Всего-то ещё одна вещь, за которой нужно тщательно следить.

Которая, в любом случае, не избавит вас от всех проблем.

Это так! Всегда есть уйма вещей, за которыми следует следить – и даже самые простые операции, такие, как скачивание файла на диск... совсем не простые! Вообще!

Причем практически на любом языке можно написать код с логическими ошибками. А если постараться — то, будьте уверены, можно устремить локомотив прямо в дерево. Тем более — машину.

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

Это еще одна тактика самозащиты: отказываться рассматривать любые варианты тезиса кроме доведенного до крайности, указывая при этом, насколько он абсурден (игнорируя факт, что никто и не отстаивает эту нелепую крайность).

Давайте же обсудим этот тезис.

Rust идеален, а вы все дураки

Как бы я хотел испытывать именно такие чувства, поскольку объяснять их было бы настолько проще.

Эту искусственную версию также очень легко разбить. «Как же вы тогда дошли до работы с Linux? Он же написан на C». «Rust небезопасен, поэтому писать на нем правильно невероятно сложно, как вам этот факт?»

Успех Go большей частью обусловлен тем, что в нем «все включено», а также есть жесткие умолчания.

Успех Rust – в том, что его легко принимать по кусочкам и в том, как хорошо он уживается с другими языками.

Обе эти истории рассказывают об успехе, но эти истории успеха очень разные.

Если верить в конспирологическую версию «Rust – это замануха», то почему всем сразу не отбросить все старое, выбрав «Единственный Хороший Язык Из Всех Существующих»?

Такая позиция настолько далека от реально происходящих событий, что даже трагична.

База кода Firefox написана в основном на C++, но несколько критически важных компонентов в ней написаны на Rust. В проекте Android недавно повторно реализовали на Rust весь стек Bluetooth. Криптографический код Rust смог закрепиться в Python, коду Rust для HTTP нашлось место в curl (как в одном из многих бекендов, имеющихся в наличии), а патчи на Rust для ядра Linux с каждой итерацией смотрятся все лучше.

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

Мы очень далеки от подхода «выплеснуть ребенка с водой». Бекенд для кодогенерации под Rust, которым пользуются буквально все – это гора кода, написанного на C++ (LLVM). Любые альтернативы невозможно признать его конкурентами, сколько ни напрягай фантазию – исключением может быть, пожалуй, только еще одна гора кода на C++.

Самые закоренелые пользователи Rust громче всех кричат о проблемах, например, о том, сколько длится сборка, об отсутствии в языке определенных фич (просто дайте мне GAT!) и обо всех других недостатках, о которых говорят почти все остальные.

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

Но точно как позиция «а не перепроверить ли паспорт», все это не важно. Нынешние тенденции могут быть подобны пропаганде опасных фуфломицинов, а никакой достойной альтернативы у нас буквально нет. И неважно, кто именно поднимает вопрос!

Выдумывая ложные дихотомии, мы ничуть не приблизимся к решению ни одной из обозначенных проблем.

Люди, у которых развивается аллергия на «огромные комья изменяемых состояний без тип-сумм» обычно тяготеют к языкам, в которых удобно контролировать изменяемость, сроки жизни, а также строить абстракции. Не суть, что таким языком зачастую оказывается  Go и Rust. Иногда на их место заступают C и Haskell. Еще в некоторых случаях — ECMAScript и Elixir. Не скажу за них с уверенностью, но такие языки встречаются.

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

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

А ещё вы можете легко и непринужденно не заморачиваться по поводу многих вещей, когда пишете код на Rust. Например, если вы не пишете такую низкоуровневую утилиту командной строки как ls, то можете ограничиться присмотром только за путями, которые представляют собой полноценные строки UTF-8, для этого используется camino.

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

«Правильный» способ предположить, что установлено опциональное значение – сказать, что это так, а в противном случае не использовать его. В этом и есть разница между вызвать json.Unmarshal и скрестить пальцы – и вызвать unwrap() применительно к Option<T>.

Причем сделать все это правильно гораздо проще, если система типов позволяет вам «огласить весь список» опций – даже если они настолько просты, как «ок» и «не ок».

Что подводит нас к следующему аргументу, который (с запасом) является самым резонным во всем букете.

Go - это прототипный/затравочный язык

Вот мы и добрались до пятой стадии: принятия.

Хорошо. Готов согласиться, что Go не подходит для продакшен-сервисов, если только ваша гильдия не состоит из одних только экспертов по Go (Tailscale) или если вы не можете потратить сколько угодно денег на инженерные расходы (Google).

Но у Go, определенно, остается своя ниша.

В конце концов, Go – язык, на который легко перейти (потому что он такой маленький, верно)? Сейчас уже много тех, кто его выучил, поэтому набрать Go-разработчиков легко, так почему бы не запастись ими за сходную цену и – уххх! – напрототипировать парочку систем?

А затем, попозже, когда дела усложнятся (при масштабировании так всегда бывает), мы либо перепишем код на других языках, либо привлечем экспертов, либо придумаем что-нибудь.

Все бы хорошо, только не существует такой штуки как «бросовый код».

Все технические организации, которые мне известны, ИСКЛЮЧИТЕЛЬНО не любят ничего переписывать, и на то есть причины! Требуется время, чтобы организовать гладкий переход, в суете теряются детали, сдача новых фич тормозится, а еще приходится переучивать сотрудников, чтобы они шли по новому треку так же эффективно, как и по старому, т.д.

Масса хороших, веских причин.

Поэтому переписываются в конечном итоге лишь очень немногие вещи. По мере того, как все больше и больше компонентов пишется на Go, появляется все больше причин продолжать в том же духе: не потому, что вам это хорошо подходит, а потому, что всегда так сложно взаимодействовать с имеющимися базами кода буквально откуда угодно извне (кроме как по сети, но даже тогда – см. выше раздел «Go – это остров»).

Итак, в сущности, никогда ничего не меняется к лучшему. Все западни Go, все те вещи, которые вам не помогут предотвратить язык и компилятор – это общие проблемы, они касаются и новичков, и бывалых. В какой-то степени помогает линтер, но он никогда не сделает столько, сколько делается компилятором в языках, где к этим проблемам подходят серьезно. В результате такие языки только замедляют разработку, хотя, распиарены именно как средства для «быстрой разработки».

Вся сложность, которая не живет в языке, теперь живет у вас в базе кода. Все инварианты, которые вы не обязаны проговаривать, когда используются типы, теперь приходится проговаривать на уровне кода. Соотношение «шум-сигнал» в ваших (очень больших) базах кода будет очень неприглядным.

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

Поскольку сигнатуры функций вам практически ни о чем не говорят (она изменяет данные? Она удерживает их? Нормально ли здесь нулевое значение? Запускает ли этот код горутину? Может ли этот канал быть нулевым? Какие типы в самом деле можно передать для этого параметра interface{}?), приходится полагаться на документацию, обновлять которую дорого, а не обновлять еще дороже – ведь тогда будут возникать новые и новые баги.

Причина, по которой я не считаю Go «языком для новичков», именно в этом: компилятор принимает настолько много кода, что почти гарантированно допускает ошибки.

Требуется набрать большой опыт по всем аспектам вокруг языка, по всему, что Go оставляет программисту «попрактиковаться», чтобы научиться писать хотя бы условно неплохой код на Go, и даже в таком случае, как мне кажется, этот результат не стоит потребовавшихся усилий.

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

Как раз наоборот, это признание, что людям плохо удается поддерживать инварианты. Всем. Но мы умеем делать инструменты, которые в этом помогают. А фокусируясь на таком подходе к разработке, требуется заранее хорошо в нее вкладываться, но такие расходы очень хорошо окупаются.

Я думал, что мы давно изжили убеждение, что «программирование – это битье по клавиатуре», но, когда я снова и снова читаю: «так можно же быстро написать много Go!» — такая уверенность пропадает.  

Неотъемлемая сложность никуда не денется, если просто зажмуриться.

Решив для себя не убавлять сложность, вы просто перекладываете её на плечи других разработчиков, на вашего начальника, админов, клиентов, на кого-то другого. Теперь им придется огибать ваши допущения, чтобы обеспечить гладкую работу всех элементов системы.

В настоящее время я часто оказываюсь таким кем-то и уже устал от этого.

Потому что на первый взгляд Go так легко может понравиться, потому что так легко на него перейти – но сложно слезть. И потому, что цена, заплаченная за выбор Go, осознается медленно, накапливается и становится непереносимой, только когда уже слишком поздно. Наша отрасль такова, что мы просто не можем позволить себе замалчивать эту проблему.  

Пока мы не начнем требовать себе самых лучших инструментов для работы, нас снова и снова ждут ночные побудки, поскольку какое-то nil-значение заползло туда,  где его быть не должно.

Это Ошибка на миллиард долларов, очередная.

Подведём итоги

Так вот о чем мы упорно себе лжем, продолжая использовать Golang:

  • Другие им пользуются, значит, и для нас он будет хорош

  • Все, кто скептически относятся к Go – это выскочки-элитарии

  • У Go такая привлекательная асинхронная среда выполнения и сборщик мусора, что они компенсируют все остальное

  • В отдельности любой языковой изъян – не страшен, и в совокупности они тоже не страшны

  • Все недостатки преодолимы, нужно «просто быть аккуратнее» или добавить лишних линтеров/внимательных глаз

  • Поскольку на Go легко писать, на нем легко разрабатывать софт для продакшена

  • Поскольку язык прост, все остальные связанные с ним вещи тоже просты

  • Go можно взять лишь немножечко, либо только для начала, а потом мы легко перейдем с него на другой

  • Потом всегда можно переписать.

Автор:
Sivchenko_translate

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js