[Перевод] Дзен Go
Оценивая свою работу, я недавно много размышлял о том, как мне писать хороший код. Учитывая, что никто не интересуется тем, как писать плохой код, возникает вопрос: как узнать, что ты написал на Go хороший код? Если есть какая-то шкала между хорошо и плохо, то как понять, какие части шкалы относятся к хорошему? Каковы его свойства, атрибуты, отличительные признаки, паттерны и идиомы?
Идиоматический Go
Эти рассуждения привели меня к идиоматическому Go. Если мы называем что-то «идиоматическим», то это что-то соответствует определённому стилю какого-то времени. Если что-то не является идиоматическим, то он не соответствует главенствующему стилю. То есть не модное.
Что ещё важнее, когда мы говорим, что чей-то код не идиоматический, то это никак не объясняет причины. Почему не идиоматический? Ответ даёт словарь.
Идиома (сущ.): оборот речи, употребляющийся как некоторое целое, не подлежащий дальнейшему разложению и обычно не допускающий внутри себя перестановки.
Идиомы — это отличительные признаки общих значений. Книги не научат вас идиоматическому Go, он познаётся только тогда, когда становишься частью сообщества.
Я обеспокоен мантрой идиоматического Go, потому что зачастую она является ограничительной. Она говорит: «ты не можешь сидеть с нами». Разве не это мы имеем в виду, когда критикуем чью-то работу как «не идиоматическую»? Они сделали это неправильно. Это выглядит неправильно. Это не соответствует стилю времени.
Я считаю, что идиоматический Go не подходит для того, чтобы учить написанию хорошего кода, потому что, по сути, это означает говорить людям, что они сделали что-то неправильно. Лучше давать такой совет, который не оттолкнёт человека в тот момент, когда он больше всего хочет получить этот совет.
Поговорки
Отвлечёмся от идиоматических проблем. Какие ещё культурологические артефакты присущи программистам на Go? Обратимся к прекрасной странице Go Proverbs. Эти поговорки — подходящий инструмент для обучения? Говорят ли они новичкам, как писать хороший код на Go?
Не думаю. Я не хочу принизить труд автора. Составленные им поговорки — лишь наблюдения, а не определения значений. Снова приходит на помощь словарь:
Поговорка (сущ.): краткое высказывание, имеющее буквальное или образное значение.
Задача Go Proverbs заключается в том, чтобы показать глубинную сущность архитектуры языка. Вот только будет ли полезен совет вроде »Пустой интерфейс ни о чём не говорит» новичку, пришедшему из языка без структурной типизации?
В растущем сообществе важно осознавать, что количество изучающих Go намного превосходит количество тех, кто прекрасно владеет этим языком. То есть поговорки, вероятно, не лучший способ обучения в подобной ситуации.
Ценности проектирования
Дэн Лю нашёл старую презентацию Марка Луковски о культуре проектирования в команде разработчиков Windows времён Windows NT-Windows 2000. Я упомянул об этом, потому что Луковски описывает культуру как обычный способ оценки архитектур и выбора компромиссов.
Основная идея — принятие в рамках неизвестной архитектуры решений на основе ценностей. У команды NT были такие ценности: портируемость, надёжность, безопасность и расширяемость. Попросту говоря, ценности проектирования — это способ решения задач.
Ценности Go
Что относится к явным ценностям Go? Каковы ключевые представления или философия, определяющие способ интерпретации мира Go-программистами? Как они провозглашаются? Как их преподают? Как они соблюдаются? Как они меняются со временем?
Как вам, новообращённому Go-программисту, прививаются ценности проектирования Go? Или как вы, опытный Go-профессионал, провозглашаете свои ценности будущим поколениям? И чтобы вы понимали, этот процесс передачи знаний не является опциональным? Без притока новых участников и новых идей, наше сообщество становится близоруким и зачахнет.
Ценности других языков
Чтобы подготовить почву к тому, о чём я хочу сказать, мы можем обратить внимание на другие языки, на их ценности проектирования.
Например, в С++ и Rust считается, что программист не должен платить за фичу, которой не пользуется. Если программа не использует какую-то ресурсоёмкую возможность языка, то нельзя заставлять программу нести расходы на поддержание этой возможности. Эта ценность проецируется из языка в стандартную библиотеку и используется в качестве критерия оценки архитектуры всех программ, написанных на С++.
Главная ценность в Java, Ruby и Smalltalk — всё является объектом. Этот принцип лежит в основе проектирования программ с точки зрения передачи сообщения, сокрытия информации и полиморфизма. Архитектуры, которые соответствуют процедурной или функциональной парадигме считаются в этих языках ошибочными. Или, как сказал бы Go-программист, не идиоматическими.
Вернёмся к нашему сообществу. Какие ценности проектирования исповедуют Go-программисты? Дискуссии на эту тему часто фрагментарны, поэтому непросто сформулировать набор значений. Крайне важно прийти к согласию, но трудность его достижения растёт экспоненциально вместе с ростом количества участников дискуссии. А что если бы кто-то выполнил за нас эту трудную работу?
Дзен Python«а Go
Несколько десятилетий назад Тим Петерс сел и написал PEP-20 — The Zen of Python. Он попытался задокументировать ценности проектирования, которых Гвидо Ван Россум придерживался в роли Великодушного Пожизненного Диктатора Python.
Давайте обратимся к The Zen of Python и посмотрим, можно ли почерпнуть оттуда что-нибудь о ценностях проектирования Go-программистов.
Хороший пакет начинается с хорошего имени
Начнём с остренького:
Пространства имен — это отличная идея, давайте делать их больше!The Zen of Python, запись 19.
Достаточно однозначно: Python-программистам следует использовать пространства имён. Много пространств.
В терминологии Go пространство имён — это пакет. Несомненно, что объединение в пакеты благоприятствует проектированию и многократному использованию. Но может возникать путаница в том, как это правильно делать, особенно если у вас есть многолетний опыт программирования на другом языке.
В Go каждый пакет должен быть для чего-то предназначен. И наименование — лучший способ понять это предназначение. Переформулируя мысль Петереса, каждый пакет в Go должен быть предназначен для чего-то одного.
Идея не нова, я уже говорил об этом. Но почему следует использовать этот подход, а не другой, при котором пакеты используются для нужд подробной классификации? Всё дело в изменениях.
Проектирование — это искусство делать так, чтобы код работал сегодня и всегда годился для изменений.Сэнди Метц
Изменения — название игры, в которой мы участвуем. Мы, как программисты, управляем изменениями. Если мы делаем это хорошо, то называем это архитектурой. А если плохо, то называем это техническим долгом или legacy-кодом.
Если вы напишете программу, которая прекрасно работает один раз с одним фиксированным набором входных данных, то никого не будет интересовать, хороший ли у неё код, потому что бизнесу важен только результат её работы.
Но так не бывает. В программах есть баги, меняются требования и входные данные, и крайне мало программ пишется с расчётом на однократное исполнение. То есть ваша программа будет со временем меняться. Возможно, такое задание дадут вам, но скорее всего этим займётся кто-то другой. Кому-то нужно сопровождать этот код.
Как нам облегчить изменение программ? Добавлять везде интерфейсы? Делать всё пригодным для создания заглушек? Жёстко внедрять зависимости? Возможно, для каких-то видов программ эти методики и подойдут, но не для многих. Однако для большинства программ создание гибкой архитектуры является чем-то большим, чем проектирование.
А если вместо расширения компонентов мы будем их заменять? Если компонент не делает то, что указано в инструкции, то его пора менять.
Хороший пакет начинается с выбора хорошего имени. Считайте его краткой презентацией, которая с помощью одного слова описывает функцию пакета. И когда имя больше не соответствует требованию, найдите замену.
Простота важна
Простое лучше сложного.The Zen of Python, запись 3.
PEP-20 утверждает, что простое лучше сложного, и я полностью согласен. Несколько лет назад я написал:
Most programming languages start out aiming to be simple, but end up just settling for being powerful.
— Dave Cheney (@davecheney) December 2, 2014
Большинство языков программирования сначала стараются быть простыми, но позднее решают быть мощными.
По моим наблюдениям, по крайне мере в то время, я не мог вспомнить известный мне язык, который не замышлялся бы простым. В качестве обоснования и соблазна авторы каждого нового языка объявляли простоту. Но я обнаружил, что простота не была основной ценностью многих языков, ровесников Go (Ruby, Swift, Elm, Go, NodeJS, Python, Rust). Возможно, это ударит по больному месту, но, быть может причина в том, что ни один из этих языков и не является простым. Или их авторы не считали их простыми. Простота не входила в список основных ценностей.
Можете считать меня старомодным, но когда это простота вышла из моды? Почему индустрия разработки коммерческого ПО постоянно и радостно забывает эту фундаментальную истину?
Есть два способа создания программной архитектуры: сделать её настолько простой, чтобы отсутствие недостатков было очевидным, и сделать её такой сложной, чтобы у неё не было очевидных недостатков. Первый способ гораздо труднее.Чарльз Хоар, The Emperor«s Old Clothes, Лекция на награждении премией Тьюринга, 1980
Простое не значит лёгкое, это мы знаем. Часто приходится потратить больше сил, чтобы обеспечить простоту использования, а не лёгкость создания.
Простота — залог надёжности.Эдсгер Дейкстра, EWD498, 18 июня 1975
Зачем стремиться к простоте? Почему для программ на Go важно быть простыми? Простая на значит сырая, это значит читабельная и удобная для сопровождения. Простая не значит безыскусная, это значит надёжная, доходчивая и понятная.
Суть программирования заключается в управлении сложностью.Брайан Керниган, Software Tools (1976)
Следует ли Python своей мантре о простоте — вопрос дискуссионный. Однако в Go простота является основной ценностью. Думаю, все мы согласимся, что в Go простой код предпочтительней умного кода.
Избегайте состояний на уровне пакетов
Явное лучше неявного.The Zen of Python, запись 2
Здесь Петерс, на мой взгляд, скорее мечтает, чем придерживается фактов. В Python многое не является явным: декораторы, dunder-методы и т.д. Несомненно, это мощные инструменты, и они существуют не просто так. Над внедрением каждой фичи, особенно сложной, кто-то работал. Но активное использование таких фич мешает при чтении кода оценить стоимость операции.
К счастью, мы, Go-программисты, можем по своему желанию делать код явным. Возможно, для вас явность может быть синонимом бюрократии и многословности, но это поверхностная интерпретация. Будет ошибкой сосредотачиваться лишь на синтаксисе, заботиться о длине строк и применении к выражениям принципов DRY. Мне кажется, важнее обеспечивать явность с точки зрения связанность и состояний.
Связанность — мера зависимости одного от другого. Если одно тесно связано с другим, то оба движутся вместе. Действие, влияющее на одно, прямо отражается и на другом. Представим себе поезд, в котором все вагоны соединены — точнее, связаны — вместе. Куда едет паровоз, туда и вагоны.
Связанность также можно описать термином cohesion — спаянность. Это мера того, насколько одно принадлежит другому. В спаянной команде все участники настолько подходят друг другу, словно они специально так были созданы.
Почему важна связанность? Как и в случае с поездом, когда вам нужно изменить кусок кода, придётся менять и весь остальной тесно связанный с ним код. Например, кто-то выпустил новую версию своего API, и теперь ваш код не компилируется.
API — неизбежный источник связывания. Но оно может представать и в более коварных формах. Все знают о том, что если изменилась сигнатура API, то меняются и данные, передаваемые в API и из него. Всё дело в сигнатуре функции: я беру значения одних типов и возвращаю значения других типов. А если API начинает передавать данные по-другому? Что если результат каждого вызова API зависит от предыдущего вызова, даже если вы не меняли свои параметры?
Это называется состоянием, и управление состояниями является ПРОБЛЕМОЙ в информатике.
package counter
var count int
func Increment(n int) int {
count += n
return count
}
Вот у нас есть простой пакет counter
. Для изменения счётчика можно вызывать Increment
, можно даже получать обратно значение, если инкрементируете с нулевым значением.
Допустим, что вам нужно протестировать этот код. Как сбрасывать счётчик после каждого теста? А если вы хотите запускать тесты параллельно, как это можно сделать? И предположим, что вы хотите использовать в программе несколько счётчиков, вам это удастся?
Конечно нет. Очевидно, решением является инкапсулирование переменной variable
в тип.
package counter
type Counter struct {
count int
}
func (c *Counter) Increment(n int) int {
c.count += n
return c.count
}
Теперь представим, что описанная проблема не ограничивается счётчиками, она затрагивает и основную бизнес-логику ваших приложений. Вы можете изолированно тестировать её? Можете тестировать параллельно? Можете использовать одновременно несколько экземпляров? Если на все вопросы ответ «нет», то причиной является состояние на уровне пакетов.
Избегайте таких состояний. Уменьшайте связанность и количество кошмарных дистанционных действий, предоставляя типам необходимые им зависимости в виде полей, а не используйте переменные пакетов.
Стройте планы на случай неудачи, а не успеха
Никогда не передавайте ошибки по-тихому.The Zen of Python, запись 10
Это сказано про языки, которые поощряют обработку исключений в самурайском стиле: возвращайся с победой или не возвращайся совсем. В языках, в основе которых лежат исключения, функции возвращают только корректные результаты. Если функция не может этого сделать, то поток управления идёт совсем по другому пути.
Очевидно, что непроверенные исключения представляют собой небезопасную модель программирования. Как вы можете писать надёжный код при наличии ошибок, если не знаете, какие выражения могут кинуть исключение? Java пытается уменьшить риски с помощью концепции проверенных исключений. И насколько я знаю, в других популярных языках не существует аналогов этого решения. Исключения есть во многих языках, и везде, кроме Java, они не проверяются.
Очевидно, что Go пошёл по другому пути. Программисты на Go считают, что надёжные программы создаются из частей, которые обрабатывают сбои до обработки успешных путей прохождения. Учитывая, что язык создавался для серверной разработки, создания многопоточных программ, а также программ, обрабатывающих данные, входящие по сети, программисты должны во главу угла ставить работу с неожиданными и повреждёнными данными, таймаутами и сбоями подключения. Конечно, если они хотят делать надёжные продукты.
Я считаю, что ошибки нужно обрабатывать явно, это должно быть основной ценностью языка.Питер Бургон, GoTime #91
Присоединяюсь к словам Питера, они послужили толчком к написанию этой статьи. Я считаю, что своим успехом Go обязан явной обработке ошибок. Программисты в первую очередь думают о возможных сбоях. Сначала мы решаем задачи типа «а что если». В результате получаются программы, в которых сбои обрабатываются на стадии написания кода, а не по мере того, как они случаются в ходе эксплуатации.
Многословность этого кода
if err != nil {
return err
}
перевешивается важностью преднамеренной обработки каждого сбойного состояния в момент возникновения. Ключом к этому является ценность явной обработки каждой ошибки.
Лучше рано возвращать, чем глубоко вкладывать
Одноуровневость лучше вложенностиThe Zen of Python, запись 5
Этот мудрый совет происходит из языка, в котором отступы являются основной формой потока управления. Как нам интерпретировать этот совет в терминологии Go? gofmt управляет всем объёмом пустого пространства в Go-программах, так что нам тут нечего делать.
Выше я писал об именах пакетов. Пожалуй, можно посоветовать избегать сложной иерархии пакетов. По моему опыту, чем больше программист старается разделить и классифицировать кодовую базу на Go, тем выше риск циклического импортирования пакетов.
Я считаю, что лучшим применением пятой записи из The Zen of Python является создание потока управления внутри функции. Иными словами, избегайте потока управления, который требует многоуровневых отступов.
Прямая видимость — это прямая линия, на протяжении которой обзор ничем не заслонён.Мэй Райер, Code: Align the happy path to the left edge
Мэй Райер описывает эту идею как программирование в прямой видимости:
- Использование контрольных операторов для раннего возвращения результата, если не соблюдается предварительное условие.
- Размещение оператора успешного возвращения в конце функции, а не внутри условного блока.
- Уменьшение общего уровня вложенности с помощью извлечения функций и методов.
Старайтесь, чтобы важные функции никогда не смещались из прямой видимости к правому краю экрана. У этого принципа есть побочный эффект: вы будете избегать бессмысленных споров с командой о длине строк.
Каждый раз делая отступ вы добавляете ещё одно предварительное условие в головы программистов, занимая один из их 7 ±2 слотов кратковременной памяти. Вместо того, чтобы углублять вложенность, старайтесь удерживать успешный путь прохождения функции как можно ближе к левой стороне экрана.
Если вы считаете, будто что-то работает медленно, то докажите это бенчмарком
Откажитесь от соблазна угадывания перед лицом двусмысленности.The Zen of Python, запись 12
Программирование базируется на математике и логике. Эти две концепции редко используют элемент удачи. Но мы, как программисты, каждый день делаем многочисленные предположения. Что делает эта переменная? Что делает этот параметр? Что происходит, если я передаю сюда nil? Что происходит, если я дважды вызываю регистр? В современном программировании приходится много предполагать, особенно при использовании чужих библиотек.
API должно быть легко использовать и трудно использовать неправильно.Джош Блох
Один из лучших известных мне способов помочь программисту избегать предположений при создании API заключается в том, чтобы сосредоточиться на стандартных способах использования. Вызывающему должно быть как можно проще выполнять обычные операции. Впрочем, раньше я уже много писал и говорил о проектировании API, так что вот моя интерпретация записи 12: не гадайте на тему производительности.
Несмотря на ваше отношение к совету Кнута, одной из причин успеха Go является эффективность его исполнения. На этом языке можно писать эффективные программы, и благодаря этому люди будут выбирать Go. Есть много заблуждений, связанных с производительностью. Поэтому когда вы ищете способы повышения производительности кода, или следуете догматическим советам вроде «откладывания замедляют», «CGO дорогое» или «всегда используйте атомарные операции вместо мьютексов», не занимайтесь гаданиями.
Не усложняйте свой код из-за устаревших догматов. А если считаете, что что-то работает медленно, то сначала удостоверьтесь в этом с помощью бенчмарка. В Go есть прекрасные бесплатные инструменты для бенчмаркинга и профилирования. Находите с их помощью узкие места в производительности своего кода.
Перед запуском горутины выясните, когда она остановится
Думаю, я перечислил ценные пункты из PEP-20 и, возможно, расширил их интерпретацию за рамки хорошего вкуса. Это хорошо, поскольку хоть это и полезный риторический приём, но всё же мы говорим о двух разных языках.
Пишете g, o, пробел, а затем функциональный вызов. Три нажатия кнопки, короче быть не может. Три нажатия кнопки, и вы запустили подпроцесс.Роб Пайк, Simplicity is Complicated, dotGo 2015
Следующие два совета я посвящаю горутинам. Горутины — это характерная особенность языка, наш ответ высокоуровневой конкурентности. Их очень легко использовать: поставьте слово go
перед оператором, и вы асинхронно запустили функцию. Никаких потоков исполнения, никаких исполнителей пула, никаких ID, никакого отслеживания статуса завершения.
Горутины дёшевы. Благодаря способности runtime-среды мультиплексировать горутины в небольшом количестве потоков исполнения (которыми вам не нужно управлять), легко можно создавать сотни тысяч или миллионы горутин. Это позволяет создавать архитектуры, которые были бы непрактичны при использовании других моделей конкурентности, в виде потоков исполнения или событийных обратных вызовов.
Но как бы дёшевы не были горутины, они не бесплатны. На их стек уходит как минимум несколько килобайтов. И когда у вас миллионы горутин, это становится заметным. Я не хочу сказать, что вам не нужно использовать миллионы горутин, если вас к этому подталкивает архитектура. Но уж если используете, то крайне важно следить за ними, поскольку в таких количествах горутины могут потреблять немало ресурсов.
Горутины — основной источник владения в Go. Чтобы приносить пользу, горутина должна что-то делать. То есть почти всегда она содержит ссылку на ресурс, то есть информацию о владении: блокировкой, сетевым подключением, буфером с данными, отправляющим концом канала. Поку горутина живёт, блокировка удерживается, подключение остаётся открытым, буфер сохраняется, а получатели канала будут ждать новых данных.
Простейший способ освобождения ресурсов заключается в их привязке к жизненному циклу горутины. Когда она завершается, ресурсы освобождаются. И поскольку запустить горутину очень просто, прежде чем написать «go и пробел» убедитесь, что у вас есть ответы на эти вопросы:
- При каком условии останавливается горутина? Go не может сказать горутине, чтобы она завершилась. По определённой причине не существует функции для остановки или прерывания. Мы не можем приказать горутине остановиться, но можем вежливо попросить. Это почти всегда связано с работой канала. Когда он закрыт, диапазон зацикливается на выход из канала. При закрытии канала его можно выбирать. Сигнал от одной горутины к другой лучше всего выражать в виде закрытого канала.
- Что нужно для возникновения условия? Если каналы являются одновременно средством коммуникации между горутинами и механизмом, с помощью которого горутины объявляют о своём завершении, то возникают вопросы: кто будет закрывать канал и когда это происходит?
- С помощью какого сигнала можно узнать, что горутина остановилась? Когда вы говорите горутине остановиться, это произойдёт в какой-то момент в будущем относительно системы координат горутины. С точки зрения человека остановка может произойти быстро, но компьютер за секунду выполняет миллиарды инструкций. И с точки зрения каждой горутины её исполнение инструкций не синхронизировано. Чаще всего для обратной связи используют канал или группу ожидания, в которой нужно использовать коэффициент разветвления по входу.
Оставьте конкурентность вызывающему
Вероятно, в любой вашей серьёзной программе на Go используется конкурентность (concurrency). Это часто приводит к проблеме возникновения паттерна воркеров (worker) — одна горутина на подключение.
Ярким примером является net/http. Довольно просто остановить сервер, который владеет прослушивающим сокетом, а что насчёт горутин, которые порождены этим сокетом? net/http предоставляет внутри объекта запроса контекстный объект, который можно использовать для сообщения прослушивающему коду, что запрос нужно отменить, а следовательно прервать горутину. Но не ясно, как узнать, когда всё это нужно сделать. Одно дело — вызывать context.Cancel
, другое — знать, что отмена выполнена.
Я часто придираюсь к net/http, но не потому, что он плох. Наоборот, это самый успешный, старейший и наиболее популярный API в кодовой базе Go. Поэтому его архитектура, эволюция и недостатки тщательно анализируются. Считайте это лестью, а не критикой.
Так вот, я хочу привести net/http как контрпример хорошей практики. Поскольку каждое подключение обрабатывается горутиной, созданной внутри типа net/http.Server
, программа вне пакета net/http не может управлять горутинами, которые созданы принимающим сокетом.
Эта сфера архитектуры ещё развивается. Можно вспомнить run.Group
в go-kit, или ErrGroup команды разработчиков Go, который предоставляет фреймворк для исполнения, отмены и ожидания асинхронно выполняемых функций.
Для всех, кто пишет код, который может выполняться асинхронно, главный принцип создания архитектур заключается в том, что ответственность за запуск горутин нужно перекладывать на вызывающего. Пусть он сам выбирает, как он хочет запускать, отслеживать и ожидать вашего выполнения функций.
Пишите тесты, чтобы блокировать поведение API вашего пакета
Возможно, вы надеялись, что в этой статье я не упомяну о тестировании. Жаль, как нибудь в другой раз.
Ваши тесты — это соглашение о том, что делает и чего не делает ваша программа. Модульные тесты должны на уровне пакетов блокировать поведение их API. Тесты описывают в виде кода, что обещает делать пакет. Если для каждого входного преобразования есть модульный тест, значит вы в виде кода, а не документации, определили соглашение о том, что будет делать код.
Утвердить это соглашение так же просто, как написать тест. На любом этапе вы с высокой степенью уверенности можете утверждать, что поведение, на которое люди полагались до внесённых вами изменений, будет функционировать и после изменений.
Тесты блокируют поведение API. Любые изменения, которые добавляют, меняют или убирают публичный API, должны включать в себя и изменения в тестах.
Умеренность — это добродетель
Go — простой язык, в нём всего 25 ключевых слов. В каком-то смысле это выделяет возможности, встроенные в язык. Это те возможности, которые позволяют языку себя продвигать: простая конкурентность, структурное типизирование и т.д.
Думаю, все из нас сталкивались с путаницей, возникающей из-за попыток использования сразу всех возможностей Go. Кто из вас был так вдохновлён использованием каналов, что использовал их где только можно? Я выяснил, что получающиеся в результате программы трудно тестировать, они хрупки и слишком сложны. А вы?
Тот же опыт был у меня и с горутинами. Пытаясь разделить работу на крошечные фрагменты, я создал тьму горутин, которая с трудом поддавалась управлению, и полностью упустил из виду, что большинство из них всегда блокировались из-за ожидания выполнения работы своими предшественниками. Код был абсолютно последовательным, и мне пришлось сильно увеличить сложность ради получения небольшого преимущества. Кто из вас сталкивался с подобным?
То же самое было у меня со встраиванием. Сначала я перепутал его с наследованием. Затем столкнулся с проблемой хрупкого базового класса, объединив несколько сложных типов, которые уже имели несколько задач, в ещё более сложные огромные типы.
Возможно, это наименее действенный совет, но я считаю важным его упомянуть. Совет всё тот же: соблюдайте умеренность, и возможности Go не исключение. По мере возможности не используйте горутины, каналы, встраивание структур, анонимные функции, обилие пакетов и интерфейсов. Лучше применяйте более простые решения, чем умные.
Удобство сопровождения имеет значение
Напоследок приведу ещё одну запись из PEP-20:
Читабельность имеет значение.The Zen of Python, запись 7
О важности читабельности кода сказано очень много и во всех языках программирования. Те, кто продвигает Go, используют такие слова, как простота, читабельность, ясность, продуктивность. Но всё это синонимы одного понятия — удобства сопровождения.
Настоящая цель заключается в создании кода, который удобен в сопровождении. Кода, который переживёт автора. Кода, который может существовать не только как вложение времени, а как основа для получения будущей ценности. Это не означает, что читабельность не важна, просто удобство сопровождения важнее.
Go не из тех языков, что оптимизируются под однострочные программы. И не из тех языков, что оптимизируются под программы с минимальным количеством строк. Мы не оптимизируем под размер исходного кода на диске, или под скорость написания программ в редакторе. Мы хотим оптимизировать наш код, чтобы он стал понятнее для читающих. Потому что именно им придётся его сопровождать.
Если вы пишете программу для себя, то, возможно, она будет запущена только один раз, или вы единственный, кто увидит её код. В таком случае делайте что угодно. Но если над кодом работает больше одного человека, или если он будет использоваться в течение длительного времени и требования, возможности или среда исполнения могут меняться, то программа должна быть удобной в сопровождении. Если ПО нельзя сопровождать, то его нельзя переписать. И это может стать последним разом, когда ваша компания вкладывается в Go.
То, над чем вы упорно работаете, будет удобным в сопровождении после вашего ухода? Как вы можете сегодня облегчить сопровождение вашего кода тем, кто придёт после вас?