Pull to refresh

Постулаты Go

Go *
В игре Go очень простые правила, но стать мастером в ней непросто, нужно научиться думать паттернами, территориями и прочими стратегическими концепциями. Язык Go не совсем случайно назван аналогично названию этой игры. В своем выступлении на недавнем митапе в Сан-Франциско, Роб Пайк упомянул книгу «Постулаты Go» (Go Proverbs), которые описывают сложные стратегии простыми поэтичными фразами. Эти постулаты несут глубинный смысл для тех, кто знаком с игрой Go, и могут казаться пустыми фразами тем, кто не знаком. И в этом нетехническом докладе он предложил придумать аналогичным способом «постулаты» и для языка Go.

Вот некоторые из них:
  • Don't communicate by sharing memory, share memory by communicating.
  • Concurrency is not parallelism.
  • Channels orchestrate; mutexes serialize.
  • The bigger the interface, the weaker the abstraction.
  • interface{} says nothing.
  • Gofmt's style is no one's favorite, yet gofmt is everyone's favorite.
  • A little copying is better than a little dependency.
  • Cgo is not Go.
  • Clear is better than clever.
  • Reflection is never clear.
  • Errors are values.


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


«Не общайтесь разделением памяти. Разделяйте память через общение.» (Don't communicate by sharing memory, share memory by communicating)
Этот постулат знают все, он звучал неоднократно в докладах и постах посвященным паттернам конкурентного программирования в Go. Этот постулат призван дать понимание сути механизма работы каналов и горутин — ваши функции не делят одну и ту же память, они безопасно общаются, передавая данные по каналам.

«Конкурентность — это не параллелизм» (Concurrency is not parallelism)
Не уверен, что слово concurrency сколько-нибудь удачно переводится на русский, но суть в том, что параллелизм — это просто параллельное выполнение одного и того же кода, а софт, полноценно использующий преимущества многоядерности — конкурентный софт — это конкурентность, тоесть способ структурироания логики программы. Новички очень часто путают эти два понятия. Есть отличное выступление Пайка с одноименным названием, обязательно посмотрите, кто ещё не.

Каналы — оркестрируют, мьютексы — сериализируют (Channels orchestrate; mutexes serialize)
Хороший постулат в тему предыдущей статье («Танцы с мьютексами в Go»), о том, когда использовать мьютексы, а когда каналы. Мьютексы просто сериализируют исполнение доступа к чему-либо, но горутины и каналы дают вам инструмент создавать программы со сложной архитектурой и структурой обмена данными между частями программы.

Чем больше интерфейс, тем слабее абстракция (The bigger the interface, the weaker the abstraction)
Новички в Go, особенно пришедшие с Java, часто считают, что интерфейсы должны быть большими и содержать много методов. Также часто их смущает неявное удовлетворение интерфейсов. Но самое важное в интерфейсах не это, а культура вокруг них, которая отображена в этом постулате. Чем меньше интерфейс, тем более он полезен. Пайк шутит, что три самых полезных интерфейса, которые он написал — io.Reader, io.Writer и interface{} — на троих в среднем имеют 0.666 метода.

Делайте нулевое значение полезным (Make the zero value useful)
Речь о том, что лучше делать нулевое значение типов полезным безо всяких конструкторов. Конечно, иногда функции-конструкторы необходимы, но если есть возможность сделать переменную вашего типа рабочей «прямо из коробки» — это всегда идёт на пользу. Пример — тип bytes.Buffer или sync.Mytex из стандартной библиотеки. Пустое значение — это готовый к использованию буфер, хотя и существуют конструкторы вроде bytes.NewBuffer и bytes.NewBufferString().

Пустой интерфейс ни о чём не говорит (interface{} says nothing)
Этот постулат говорит о том, что интерфейсы — «поведенческие типы» — должны что-то означать. Если вы создаёте интерфейс, это что-то означает и служит конкретной цели. Пустой же интерфейс (interface{}) ничего не означает и ни о чём не говорит. Есть ситуации, когда его нужно использовать, но они чаще исключение — не используйте interface{} без повода. Новички часто переиспользуют пустые интерфейсы, и масса вопросов на Stack Overflow именно о них.

Стиль Gofmt — это не чье-то предпочтение, но gofmt предпочитают все (Gofmt's style is no one's favorite, yet gofmt is everyone's favorite)
Многие новички, борясь с привычками, жалуются на то, что код в Go отформатирован не так, как им нравится. Gofmt — это единый стиль, он не является чьим-то личным предпочтением. Даже сам Грисмайер, автор утилиты go fmt, сам предпочёл бы другой стиль форматирования. Но с того момента, как формат был утверждён и gofmt используется везде и всегда — просто примите это как должное. Опытные программисты считают go fmt спасением, так как он очень упрощает работу с чужим кодом — ад с разными стилями форматирования в других языках ушёл в прошлое.
Примечательно, что новые языки перенимают этот подход (к примеру, rustfmt) — это то, о чём говорил Dave Cheney в своем выступлении «Наследие Go» — новые языки больше не будут считаться полноценными без единого стиля форматирования и утилиты, обеспечивающей это.

Небольшое копирование лучше небольшой зависимости (A little copying is better than a little dependency)
Это уже относится к программированию в целом, но в Go это видно и по стандартной библиотеке, и в культуре в целом. Отчасти это перекликается с известной установкой «Дупликация дешевле плохой абстракции». Пайк рассказывает, как в первые дни в Google ему сказали, что больше всего пекутся о реиспользовании кода. Если можно избежать написания одной повторяющейся строчки, сделав импорт — нужно так и поступать. По мнению Пайка, Google до сих пор разгребает проблемы с кодом из-за этого подхода. Очень часто функционал может быть похож (до первого рефакторинга), или необходима лишь часть функционала из зависимости, и зачастую бывает эффективней написать 5 строчек, чем импортировать 150кб зависимостей. Как пример — функция IsPrint из пакета strconv — она дублирует функционал unicode.IsPrint(), при этом тесты проверяют, что функции работают одинаково. Резюмируя — не бойтесь копирования кода, там где это оправдано.

Код с syscall-вызовы должен содержать build-теги (Syscall must always be guarded by build tags)
В тему недавно переведённой статьи «Как писать Go код, который легко портируется». Некоторые жалуются на пакет syscall в Go — нужно его изменить, он не портируется. Но syscall-ы это по определению платформозависимые вызовы, поэтому если вы хотите делать качественный кроссплатформенный код — всегда добавляйте build-теги в файлы, в которых используются syscall.

Cgo также должны содержать build-теги (Cgo must always be guarded by build tags)
По абсолютно той же причине — всегда добавляйте build-теги, при использовании cgo. Если вы вызываете С — никто не знает, что там на самом деле происходит, это почти наверняка непортируемо.

Cgo это не Go (Cgo is not go)
Многие радуются тому, как легко в Go использовать C-код. Иногда, это, конечно, необходимо, но Пайк признается, что сам никогда не использовал Cgo, и в мире Go есть четкое понимание памяти, стабильность, безопасность, сборка мусора… и с Cgo всё это идёт в топку. В 90% случаев в Google, когда кто-то говорит «моя программа крашнулась, помогите разобраться» — дело оказывается в Cgo.

С пакетом unsafe нет гарантий (With the unsafe package there are no guarantees)
Тут делается акцент на том, что код написанный с использованием пакета unsafe также не портируемый, и на него не распространяются гарантии обратной совместимости в пределах Go 1 (это написано в первой же строке документации к пакету). Пока что в нём ничего не меняли, но если вы собираетесь использовать зачем-то unsafe — будьте готовы, что в будущих версиях, придётся подстраиваться под изменения.

Ясно лучше, чем заумно (Clear is better than clever)
Постулат о том, что в Go ясность и читабельность кода стоит на первом месте. И вы должны также стремиться писать ясный и понятный код, вместо заумного. Ваш код рано или поздно будут читать, улучшать и рефакторить другие программисты, и чем яснее он будет, тем проще и быстрее будет результат. Go будет всеми способами и силами вам в этом помогать и способствовать, весь дизайн языка пропитан этой целью.

Рефлексия никогда не понятна (Reflection is never clear)
Рефлексия (способность программы менять собственную структуру) в Go реализована пакетом reflect, и люди нередко пытаются использовать её не к месту. Рефлексия — это очень-очень мощная вещь, но очень сложная для понимания, и на самом деле нужна очень малому количеству людей и задач. Пайк говорит, что написал, вероятно, больше всех кода с рефлексией в Go, и до сих пор ненавидит это делать. И считает, что нужно стимулировать людей не использовать рефлексию, поскольку она, как правило, не нужна так, как им это кажется.

Ошибки это значения (Errors are values)
Ещё один важный постулат, который уже неоднократно освещался на хабре, и которому, в частности, посвящена статья в официальном блоге Go. Новички часто не понимают этого и спрашивают — «почему я должен везде писать эти if err != nil». Да потому что вы не программируете, вы клепаете код. Многие считают, что if err != nil — это такая альтернатива try..catch… — специальная конструкция языка для работы с ошибками. Но это не так, вы не можете программировать с try… catch… — это управляющая конструкция. С ошибками же в Go можно и нужно делать то, что требует логика программы.

Не просто проверяйте ошибки, обрабатывайте их красиво (Dont’ just check errors, handle them gracefully)
Ошибки очень важно не просто проверять, но и думать, что с ними делать, чтобы ошибочная ситуация была обработана корректно. Возможно нужно дополнить ошибку какой-то информацией, возможно запомнить для обработки позднее, что-нибудь ещё. Огромная и очень важная часть программирования — это как вы обрабатываете ошибки. Не просто проверять и пробрасывать наверх, а думать о том, как правильно обработать эту ошибку. Это справедливо для всех языков, но поскольку в Go ошибки это просто значения, то это проще сделать и проще реализовать красивую и правильную обработку ошибок в коде.

Продумывайте архитектуру, называйте компоненты, документируйте детали (Design the architecture, name the components, document the details)
Тут речь о важности правильного именования компонентов вашего кода, методов, функций и переменных. Эти имена будут потом находится в коде пользователей вашего пакета. И если имена удачны и понятны, дизайн программы будет более ясен и прост для понимания. Но, конечно, всегда есть детали, которые нужно объяснить — и вот их нужно помещать в документацию.

Документация — для пользователей (Documentation is for users)
О том, что документация к функции должна описывать не что эта функция делает, а для чего она нужна. Это очень важное различие, которое зачастую упускается из виду. Когда пишете документацию, посмотрите на неё глазами пользователя вашего пакета.

Ну, и если вы можете придумать ещё какие-то постулаты, которые удовлетворяют критериям краткости, поэтичности и концептуальности — смело предлагайте.

go-proverbs.github.io

Слайды (не оригинальные, правда): speakerdeck.com/ajstarks/go-proverbs

Видео:
Tags:
Hubs:
Total votes 24: ↑16 and ↓8 +8
Views 22K
Comments Comments 77