...

пятница, 30 октября 2015 г.

Почему Go это хорошо продуманный язык программирования

В недавнем посте с критикой Go, который был выдан за перевод, пользователь tucnak, помимо избыточной фамильярности в адрес Роба Пайка, поднял несколько интересных моментов языка. Поскольку формат статьи предполагал, увы не желание разобраться в теме, а разжечь холивары, предлагаю в этой статье пройтись по озвученным «проблемам» и понять, о чём же речь на самом деле, и что же заставляет современные компании выбирать Go.

image

Причина №1. Манипуляции со слайсами просто отвратительны


Автор начинает свой разнос дизайна языка с утверждения о том, что отрицательные индексы, как в Python, не работают.

// numbers[:-1] из Python не прокатит.


Здесь ответ прост — Go это не Python, это разные языки. Некоторым людям сложно даётся факт, что никакие языки, кроме Питона, не являются Питоном, но давайте изучим вопрос детальнее. Вот официальный ответ Роберта Пайка о том, почему отрицательных индексов нет в Go:

Это было убрано намеренно, потому что выражение s[i:j] может молча дать неверный результат, если j станет меньше нуля. Именно это было причиной ужасного бага в Rietveld, кстати говоря. Индексы должны быть не отрицательны.

В issues на Github есть похожий вопрос и ответ, который дополняет следующим утверждением:

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

Кроме того, от статус кво, выигрывает читабельность. Сейчас очевидно, что выражение s[:i] создает слайс от s длиной i байт. Если же i может быть отрицательным, то нужно больше контекста, чтобы понимать всё выражение слайса.

Это решение отталкивается от общей философии Go избегать тонких синтаксических трюков.


Можно согласиться, что кому-то для его случаев отрицательный индекс может быть удобен, и чуть короче в записи. Но авторы Go, исходя из своего, более чем, полувекового опыта разработки, видят, что эта экономия нескольких байт в одном случае, ведущая к сложным багам и ухудшению читабельности в другом — не стоит того, и выбирают решение, более подходящее концепции языка. Решение, требующее программистам на Python немного перепривыкнуть, но уменьшающее риск хитрых ошибок в long term. Понятно, что не все мыслят на long term, отсюда и конфьюз.

Идем далее, следующий rant автора:

/ Хочется вставить какое-то число? Ничего страшного,
// в Go есть общепринятая best practice!
numbers = append(numbers[:2], append([]int{3}, numbers[2:]...)...)


Во-первых, это не best practice а сниппет-однострочник, найденный на странице Go Slice Tricks. В реальном коде, если уж кто это и делает, то более читабельно. И тут мы, для начала, вспомним, что дизайн слайсов и массивов в Go обсуждался больше года, прежде чем прийти к окончательному дизайну. И основными факторами были «практическая необходимость» и «скорость». Да, это не очень привычный подход, во многих других языках наличие фич в большем приоритете, но в Go вот так.

А теперь давайте спросим автора два вопроса:

  1. насколько частый случай — вставка элемента в середину массива?
  2. насколько эффективна вставка в середину массива?

Думаю, не стоит пояснять, что при текущей реализации (массивы в Go очень похожи на массивы в C, и, в основном, служат хранилищем для слайсов) эта вставка — довольно дорогая операция, заставляющая (пере)выделять память, а реальный кейс использования этого не настолько частый, как, скажем «добавление» элемента к слайсу.
Это и вправду совсем не частый кейс, по крайней мере в той нише, на которую рассчитан Go. Безусловно, такие кейсы есть — к примеру операции с бинарными протоколами, но это не то, что вы будете писать хотя бы раз в день или даже раз в месяц. В комментариях к посту автора я попросил хотя бы одного человека привести три примера из своего реального кода, где ему нужно было бы вставлять элемент в середину массива — не написал никто. Зато писали придуманные кейсы, в которых гораздо правильнее использовать не массивы, а более подходящие структуры данных.

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

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

Причина №2. Нулевые интерфейсы не всегда нулевые :)


Это классический пример cherry-picking — автор находит в интернете пример кода, который может запутывать новичка, не знакомого с интерфейсами в Go (полагаю, большинство читателей предыдущей статьи), и выставляет этот пример, как «плохой дизайн». За кадром, конечно, остается то, что этому моменту посвящена запись в FAQ на странице Go, что в реальном коде с этой проблемой вы никогда не встретитесь (разве что, если вам совсем несмышленый джуниор попадется), и что дизайн системы типов и интерфейсов по своей простоте и целостности позволяет создавать сложнейшие абстракции без применения классов и наследования. Все это проходит мимо автора и его читателей, с восторгом пишущих «наконец-то пост ненависти про Go».

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

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

Причина №3. Забавное сокрытие переменных


Тут автор, обманывает и читателей и самого себя. Во-первых, shadowing всегда был источником различных конфьюзов и непоняток, и в Go, в отличие от, скажем С++, нет undefined behavior при сокрытии переменных. Винить можно только невнимательность программиста, что, безусловно, не снимает с языка ответственности в том, чтобы помогать программисту не совершать такие ошибки.
Во-вторых, критика «дизайна сокрытия переменных» должна подразумевать предложение альтернативного, более удачного дизайна — тогда дискуссия имеет шанс быть предметной.
В-третьих, штатная утилита go vet умеет показывать сокрытые декларации переменных, но, поскольку сокрытие это не ошибка, то, по умолчанию этот флаг go vet выключен, чтобы не засорять вывод на правильном коде.
Используйте
go tool vet -shadow=true
и не вводите в заблуждение читателей.

Например на код автора оригинальной статьи, go vet скажет:


$ go tool vet -shadow=true main.go
main.go:16: declaration of number shadows declaration at main.go:10:


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

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

Причина №4. Ты не можешь передать []struct как []interface


На эту ситуацию я, в начале знакомства с Go, наткнулся сам. Пытался делать нечто замудренное, по незрелости, конвертировать интерфейсы туда-обратно, и возникла вот точно такая же задача.
Разумеется, я наткнулся на ошибку компилятора, но, в отличие от автора статьи, не бросился писать статью с проклятиями в адрес Пайка и утверждениями о том, что «в Go нет полноценной поддержки интерфейсов» (хаха), а нашел объяснение почему такая операция явно не поддерживается.
Первое, что нужно знать — это отличия «структуры» от «интерфейса» в Go. Это просто, и этого достаточно, чтобы понять, почему вы просто так не можете «скастить» слайс структур в слайс интерфейсов. Это база, причем очень простая, и оправданий не знать её нет — вся документация на официальном сайте и читается за вечер.
Второе — и созвучное с выше обсужденными слайсами — это то, что операция конвертирования слайсов — дорогая операция. По времени это O(n) и компилятор Go подобные дорогие вещи не будет делать, чтобы не давать нерадивым программистам писать медленный код. Хотите делать потенциально дорогую операцию — будьте добры, сделайте это явно, вся ответственность на вас, а не на компиляторе. Очень правильный подход, который нацелен на качество кода на Go в long term. Ну, ясно, почему для автора поста это так непонятно.

Причина №5. Неочевидные циклы «по значению»


Тут автор возмущается тем, что ключевое слово range передает значения, а не ссылки, и что, цитирую,

range — это тебе не foreach из С++

Потом, правда, поправляется и говорит, что проблема в том, что в документации явно это не указано.

Что тут можно сказать? Go — это другой язык, это не C++. Зачем пытаться сделать из Go то Python, то C++ — загадка.
Любой, кто прошел go-tour увидит в этой строке оператор ":=", который означает создание новой переменной и присвоение ей значения:

for number := range numbers {}

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

Причина №6. Сомнительная строгость компилятора


Сомнительная строгость, по мнению автора заключается в том, что в Go код не компилируется если есть неиспользуемый импорт. Это, помимо того, что является квинтессенцией opinionated подхода Go к вопросу качества кода, ещё и демонстриует, что автор таки не читал FAQ, в котором этот вопрос подробно объяснен. Любой, кто писал на С/C++, знает во что превращаются проекты, после того, как кодом поработают 5-10 программистов, и код пройдет несколько стадий рефакторинга. Помимо этого, каждый импорт — это замедление процесса компиляции, и там где Go-программисты не успевают моргать глазом, С++-программисты идут пить кофе и ждать, и это тоже вина/заслуга «неиспользуемых импортов». Понимание правильности такого решение могло прийти только с большим опытом.
Согласен, что это непривычно, но это приводит к быстрой компиляции и чистому аккуратному коду в любом проекте на Go. И это бесценно. У программистов без опыта, конечно, приоритеты другие.

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

Причина №7. Кодогенерация в Go это просто костыль


Недавно был ещё один подобный наброс от товарища из Mozilla (который ещё говорил «Питон сравним по потреблению памяти с Го, если посчитать лики горутин»), и он тоже пользовался этим приемом. Вот что он писал:

Строго говоря, утилита для анализа покрытия кода в Go — это хак


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

go generate отлично выполняет свою функцию, позволяя обходиться безо всяких Make-файлов даже тогда, когда вам нужно сгенерировать какой-то код с помощью сторонних утилит вроде yacc, thrift, protobuf или swig. Это простой, удобный и рабочий инструмент, который решает реальную задачу, практически ничего не добавляя в язык, кроме одной команды go generate и одного соглашения о формат комментария (что-то вроде hashbang). Это работает и работает хорошо.

Но нет, для теоретиков это не важно. Теоретикам важно повесить нужный ярлык, и судить о вещах издалека, глядя на ярлыки.
Далее автор мягко уходит в «мне лично кажется, что это плохо», но усиленное фамильярничание и надменность в адрес Пайка выдает искусственность такого «аргумента» о «плохом дизайне языка».

Эпилог


Go безусловно является языком строгим и упрямым — часто используют слово opinionated. У Go есть своё видение хороших и плохих практик, и он вас стимулирует следовать хорошим практикам и не дает следовать плохим. Хотите сотни неиспользуемых импортов в коде — ищите другой язык, хотите покрывать тестами код и смотреть степень покрытия — вот вам все карты в руки, хотите забивать на ошибки и прятать код обработки ошибок с глаз подальше — ищите другой язык, хотите стрелять себе в ногу с адресной арифметикой — туда же.
Go — это ваш друг, более опытный и мудрый, который помогает вам делать более быстро более качественный код. Упрямый друг, у которого огромный опыт за плечами, и если вы захотите научиться у него — вы получите меньше проблем в будущем, больше свободного времени и больше удовольствия от работы. Но для этого нужно поверить ему, как другу, и понять, что long-term цели всегда важнее short-term хотелок.

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

Статья же автора о том, что дизайн языка «плохой» — это, к сожалению, не более, чем попытка привлечь к себе внимание. В подростковом возрасте это нормально, но хочется донести автору, что искусственное разжигание хейтерства и холиваров никогда не бывает конструктивным. Подобными статьями, заведомо предвзятыми и не объективными, безусловно, автор наносит вред. К примеру, есть компании, где Go в процессе внедрения, и, зачастую, решение о том «писать несколько месяцев REST-бекенд на C++ и нанять для этого ещё 2 rock-star C++ программиста» или «перейти на Go и написать все быстро и качественно» принимает менеджер сверху, который может понимать области применимости языков, а может и не понимать. И достаточно такого вброса, чтобы человек, даже не читая и не вникая, решил для себя, что «Го не стоит использовать, раз такие статьи про него пишут». И это реальные ситуации.

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

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Комментариев нет:

Отправить комментарий