diff options
| author | 2026-01-31 20:38:50 +0300 | |
|---|---|---|
| committer | 2026-01-31 23:38:53 +0300 | |
| commit | 49458f5ffd5a48c465117ec27f6437683f75acc1 (patch) | |
| tree | a99ee68116d10c2b2e5a70c442cdadec95ba793c | |
| download | blog-49458f5ffd5a48c465117ec27f6437683f75acc1.tar.gz blog-49458f5ffd5a48c465117ec27f6437683f75acc1.tar.bz2 blog-49458f5ffd5a48c465117ec27f6437683f75acc1.tar.xz blog-49458f5ffd5a48c465117ec27f6437683f75acc1.zip | |
initial
264 files changed, 18493 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..98cc4fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +public +data
\ No newline at end of file diff --git a/.hugo_build.lock b/.hugo_build.lock new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/.hugo_build.lock diff --git a/archetypes/default.md b/archetypes/default.md new file mode 100644 index 0000000..25b6752 --- /dev/null +++ b/archetypes/default.md @@ -0,0 +1,5 @@ ++++ +date = '{{ .Date }}' +draft = true +title = '{{ replace .File.ContentBaseName "-" " " | title }}' ++++ diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..85bb929 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,25 @@ +--- +title: Добро пожаловать! +--- + +<img src="/files/logo512.webp" align="right" width="256" /> + +Добро пожаловать на мой личный сервер. + +Да, это старая добрая домашняя страница, персональный сайт, «хомяк», называйте как привычнее. + +Меня зовут Саня, я Go разработчик. Остальное обо мне на [отдельной странице](/me/). + +А ещё, у меня есть свой [Блог](/posts/)! + +И у него даже есть [Atom лента](/feed/posts/) для удобной подписки! + +Для связи со мной — пишите на e-mail или jabber [i@neonxp.ru](mailto:i@neonxp.ru) + +Другие форматы блога: + +- Telegram канал: https://t.me/neonxplog +- VK паблик: https://vk.com/neonxplog +- Dzen канал: https://dzen.ru/neonxp + +Подписаться можно где угодно, но первоисточник по [POSSE](/posts/2024-12-15-posse/) именно здесь. diff --git a/content/books/index.md b/content/books/index.md new file mode 100644 index 0000000..98bed36 --- /dev/null +++ b/content/books/index.md @@ -0,0 +1,6 @@ +--- +order: '10' +title: Я читаю +--- +TODO + diff --git a/content/books/under-construction90s.gif b/content/books/under-construction90s.gif Binary files differnew file mode 100644 index 0000000..ab87cfc --- /dev/null +++ b/content/books/under-construction90s.gif diff --git a/content/me/index.md b/content/me/index.md new file mode 100644 index 0000000..094845b --- /dev/null +++ b/content/me/index.md @@ -0,0 +1,37 @@ +--- +order: "30" +title: Обо мне... +--- + +<!--more--> +<div class="h-card"> + <h1 class="p-name"> + <span class="p-given-cname">Александр</span> <span class="p-nickname">NeonXP</span> <span class="p-family-name">Кирюхин</span> + </h1> + <ul> + <li>E-mail: <a href="mailto:i@neonxp.ru" rel="me" class="u-email">i@neonxp.ru</a></li> + <li>Jabber: <a href="xmpp:i@neonxp.ru" rel="me" class="u-jabber">i@neonxp.ru</a></li> + <li>PGP: <a href="https://neonxp.ru/files/0x96BF11A67E3C75F6.asc" rel="pgpkey" class="u-key">9E49 0BBE 2F1F 82C9 15F8 F440 96BF 11A6 7E3C 75F6</a></li> + <li> + <span class="p-locality">Казань</span>, + <abbr class="p-region" title="Республика Татарстан">РТ</abbr>, + <span class="p-country-name">Российская Федерация</span> + </li> + <li> + <a rel="me" href="https://neonxp.ru/" class="u-url">Веб-сайт</a> + </li> + <li> + <a rel="me" href="https://neonxp.ru/files/vcard.vcf" class="u-url">Моя визитка с RSS</a> + </li> + <li class="p-note"> + Golang разработчик + </li> + </ul> + <img src="https://neonxp.ru/files/photo.webp" class="u-photo rounded border shadow" width="600" /> +</div> + +# Другие ссылки + +- [Мой git](https://gitrepo.ru/neonxp/) +- [Мои Go пакеты](https://go.neonxp.ru/) +- [Telegram Канал](https://t.me/neonxplog) diff --git a/content/pages/_index.md b/content/pages/_index.md new file mode 100644 index 0000000..3d813e3 --- /dev/null +++ b/content/pages/_index.md @@ -0,0 +1,4 @@ +--- +order: '20' +title: Разное +--- diff --git a/content/pages/archive/2007-11-05.md b/content/pages/archive/2007-11-05.md new file mode 100644 index 0000000..ebd0aa3 --- /dev/null +++ b/content/pages/archive/2007-11-05.md @@ -0,0 +1,3 @@ +--- +{} +--- diff --git a/content/pages/archive/2007-11-06.md b/content/pages/archive/2007-11-06.md new file mode 100644 index 0000000..ebd0aa3 --- /dev/null +++ b/content/pages/archive/2007-11-06.md @@ -0,0 +1,3 @@ +--- +{} +--- diff --git a/content/pages/archive/2007-11-08.md b/content/pages/archive/2007-11-08.md new file mode 100644 index 0000000..ebd0aa3 --- /dev/null +++ b/content/pages/archive/2007-11-08.md @@ -0,0 +1,3 @@ +--- +{} +--- diff --git a/content/pages/archive/2007-12-11.md b/content/pages/archive/2007-12-11.md new file mode 100644 index 0000000..ebd0aa3 --- /dev/null +++ b/content/pages/archive/2007-12-11.md @@ -0,0 +1,3 @@ +--- +{} +--- diff --git a/content/pages/archive/2007-12-26.md b/content/pages/archive/2007-12-26.md new file mode 100644 index 0000000..ebd0aa3 --- /dev/null +++ b/content/pages/archive/2007-12-26.md @@ -0,0 +1,3 @@ +--- +{} +--- diff --git a/content/pages/archive/2011-05-10.md b/content/pages/archive/2011-05-10.md new file mode 100644 index 0000000..ebd0aa3 --- /dev/null +++ b/content/pages/archive/2011-05-10.md @@ -0,0 +1,3 @@ +--- +{} +--- diff --git a/content/pages/archive/2011-09-11-1.md b/content/pages/archive/2011-09-11-1.md new file mode 100644 index 0000000..ebd0aa3 --- /dev/null +++ b/content/pages/archive/2011-09-11-1.md @@ -0,0 +1,3 @@ +--- +{} +--- diff --git a/content/pages/archive/2011-09-11-2.md b/content/pages/archive/2011-09-11-2.md new file mode 100644 index 0000000..ebd0aa3 --- /dev/null +++ b/content/pages/archive/2011-09-11-2.md @@ -0,0 +1,3 @@ +--- +{} +--- diff --git a/content/pages/archive/2011-09-11-3.md b/content/pages/archive/2011-09-11-3.md new file mode 100644 index 0000000..ebd0aa3 --- /dev/null +++ b/content/pages/archive/2011-09-11-3.md @@ -0,0 +1,3 @@ +--- +{} +--- diff --git a/content/pages/archive/2011-09-12.md b/content/pages/archive/2011-09-12.md new file mode 100644 index 0000000..ebd0aa3 --- /dev/null +++ b/content/pages/archive/2011-09-12.md @@ -0,0 +1,3 @@ +--- +{} +--- diff --git a/content/pages/archive/2011-09-24.md b/content/pages/archive/2011-09-24.md new file mode 100644 index 0000000..ebd0aa3 --- /dev/null +++ b/content/pages/archive/2011-09-24.md @@ -0,0 +1,3 @@ +--- +{} +--- diff --git a/content/pages/archive/2011-11-20.md b/content/pages/archive/2011-11-20.md new file mode 100644 index 0000000..ebd0aa3 --- /dev/null +++ b/content/pages/archive/2011-11-20.md @@ -0,0 +1,3 @@ +--- +{} +--- diff --git a/content/pages/archive/2011-11-21.md b/content/pages/archive/2011-11-21.md new file mode 100644 index 0000000..ebd0aa3 --- /dev/null +++ b/content/pages/archive/2011-11-21.md @@ -0,0 +1,3 @@ +--- +{} +--- diff --git a/content/pages/archive/_index.md b/content/pages/archive/_index.md new file mode 100644 index 0000000..2a7cea3 --- /dev/null +++ b/content/pages/archive/_index.md @@ -0,0 +1,4 @@ +--- +order: '20' +title: Архив старых постов из WebArchive +--- diff --git a/content/pages/gostyleguide/_index.md b/content/pages/gostyleguide/_index.md new file mode 100644 index 0000000..d38468e --- /dev/null +++ b/content/pages/gostyleguide/_index.md @@ -0,0 +1,6 @@ +--- +order: 10 +title: Go styleguide +--- + +Переводы на русский язык двух самых популярных styleguide по Go. diff --git a/content/pages/gostyleguide/google/_index.md b/content/pages/gostyleguide/google/_index.md new file mode 100644 index 0000000..f28cb04 --- /dev/null +++ b/content/pages/gostyleguide/google/_index.md @@ -0,0 +1,10 @@ +--- +order: 4 +title: Google Go Style Guide +--- + +# Стиль Go + +Оригинал: https://google.github.io/styleguide/go + +<!--more--> diff --git a/content/pages/gostyleguide/google/best-practices.md b/content/pages/gostyleguide/google/best-practices.md new file mode 100644 index 0000000..4fc59b1 --- /dev/null +++ b/content/pages/gostyleguide/google/best-practices.md @@ -0,0 +1,3727 @@ +--- +order: 1 +title: Google Go Style Guide — Лучшие практики +--- + +# Лучшие практики стиля Go (Go Style Best Practices) + +Оригинал: https://google.github.io/styleguide/go/best-practices + +[Обзор](https://neonxp.ru/pages/gostyleguide/google/) | [Руководство](https://neonxp.ru/pages/gostyleguide/google/guide) | [Решения](https://neonxp.ru/pages/gostyleguide/google/decisions) | +[Лучшие практики](https://neonxp.ru/pages/gostyleguide/google/best-practices) + +**Примечание:** Это часть серии документов, описывающих [Стиль Go (Go +Style)](https://neonxp.ru/pages/gostyleguide/google/) в Google. Данный документ **не является ни [нормативным +(normative)](https://neonxp.ru/pages/gostyleguide/google/#normative), ни [каноническим (canonical)](https://neonxp.ru/pages/gostyleguide/google/#canonical)**, +а является вспомогательным документом к [основному руководству по стилю](https://neonxp.ru/pages/gostyleguide/google/guide/). +Подробнее см. [в обзоре](https://neonxp.ru/pages/gostyleguide/google/#about). + +<a id="about"></a> + +## О документе (About) + +В этом документе представлены **рекомендации о том, как наилучшим образом +применять Руководство по стилю Go**. Эти рекомендации предназначены для типичных +ситуаций, возникающих часто, но могут не применяться в каждом случае. По +возможности обсуждаются несколько альтернативных подходов, а также соображения, +которые учитываются при решении о том, когда их применять, а когда нет. + +См. [обзор](https://neonxp.ru/pages/gostyleguide/google/#about) для полного набора документов руководства по стилю. + +<a id="naming"></a> + +## Именование (Naming) + +<a id="function-names"></a> + +### Имена функций и методов + +<a id="function-name-repetition"></a> + +#### Избегайте повторения (Avoid repetition) + +При выборе имени для функции или метода учитывайте контекст, в котором это имя +будет прочитано. Рассмотрите следующие рекомендации, чтобы избежать избыточного +[повторения (repetition)](https://neonxp.ru/pages/gostyleguide/google/decisions/#repetition) в месте вызова (call site): + +* Следующее, как правило, можно опустить в именах функций и методов: + + * Типы входных и выходных данных (если нет конфликта) + * Тип получателя (receiver) метода + * Является ли входной или выходной параметр указателем (pointer) + +* Для функций не следует [повторять имя + пакета](https://neonxp.ru/pages/gostyleguide/google/decisions/#repetitive-with-package). + + ```go + // Плохо: + package yamlconfig + + func ParseYAMLConfig(input string) (*Config, error) + ``` + + ```go + // Хорошо: + package yamlconfig + + func Parse(input string) (*Config, error) + ``` + +* Для методов не следует повторять имя получателя метода. + + ```go + // Плохо: + func (c *Config) WriteConfigTo(w io.Writer) (int64, error) + ``` + + ```go + // Хорошо: + func (c *Config) WriteTo(w io.Writer) (int64, error) + ``` + +* Не повторяйте имена переменных, передаваемых в качестве параметров. + + ```go + // Плохо: + func OverrideFirstWithSecond(dest, source *Config) error + ``` + + ```go + // Хорошо: + func Override(dest, source *Config) error + ``` + +* Не повторяйте имена и типы возвращаемых значений. + + ```go + // Плохо: + func TransformToJSON(input *Config) *jsonconfig.Config + ``` + + ```go + // Хорошо: + func Transform(input *Config) *jsonconfig.Config + ``` + +Когда необходимо устранить неоднозначность между функциями с похожими именами, +допустимо включить дополнительную информацию. + +```go +// Хорошо: +func (c *Config) WriteTextTo(w io.Writer) (int64, error) +func (c *Config) WriteBinaryTo(w io.Writer) (int64, error) +``` + +<a id="function-name-conventions"></a> + +#### Соглашения об именовании (Naming conventions) + +Существуют и другие общие соглашения при выборе имен для функций и методов: + +* Функции, которые что-то возвращают, получают имена, похожие на + существительные. + + ```go + // Хорошо: + func (c *Config) JobName(key string) (value string, ok bool) + ``` + + Следствием этого является то, что имена функций и методов должны [избегать + префикса `Get`](https://neonxp.ru/pages/gostyleguide/google/decisions/#getters). + + ```go + // Плохо: + func (c *Config) GetJobName(key string) (value string, ok bool) + ``` + +* Функции, которые что-то делают, получают имена, похожие на глаголы. + + ```go + // Хорошо: + func (c *Config) WriteDetail(w io.Writer) (int64, error) + ``` + +* Идентичные функции, которые отличаются только типами, включают имя типа в + конце имени. + + ```go + // Хорошо: + func ParseInt(input string) (int, error) + func ParseInt64(input string) (int64, error) + func AppendInt(buf []byte, value int) []byte + func AppendInt64(buf []byte, value int64) []byte + ``` + + Если существует ясная "основная" версия, тип может быть опущен в имени для + этой версии: + + ```go + // Хорошо: + func (c *Config) Marshal() ([]byte, error) + func (c *Config) MarshalText() (string, error) + ``` + +<a id="naming-doubles"></a> + +### Тестовые дубли (Test doubles) и вспомогательные пакеты (helper packages) + +Существует несколько подходов, которые можно применить для [именования] пакетов +и типов, которые предоставляют тестовые вспомогательные средства и особенно +[тестовые дубли (test doubles)]. Тестовым дублем может быть заглушка (stub), +фейк (fake), мок (mock) или шпион (spy). + +Эти примеры в основном используют заглушки. Обновите свои имена соответствующим +образом, если ваш код использует фейки или другой вид тестового дубля. + +[именование]: guide#naming +[тестовые дубли (test doubles)]: + https://abseil.io/resources/swe-book/html/ch13.html#basic_concepts + +Предположим, у вас есть хорошо сфокусированный пакет, предоставляющий +production-код, подобный этому: + +```go +package creditcard + +import ( + "errors" + + "path/to/money" +) + +// ErrDeclined указывает, что эмитент отклонил операцию. +var ErrDeclined = errors.New("creditcard: declined") + +// Card содержит информацию о кредитной карте, такую как эмитент, +// срок действия и лимит. +type Card struct { + // опущено +} + +// Service позволяет выполнять операции с кредитными картами через внешние +// процессинговые системы, такие как списание, авторизация, возврат средств и подписка. +type Service struct { + // опущено +} + +func (s *Service) Charge(c *Card, amount money.Money) error { /* опущено */ } +``` + +<a id="naming-doubles-helper-package"></a> + +#### Создание вспомогательных тестовых пакетов (Creating test helper packages) + +Предположим, вы хотите создать пакет, содержащий тестовые дубли для другого +пакета. Воспользуемся `package creditcard` (из примера выше): + +Один из подходов — создать новый Go-пакет на основе production-пакета для +тестирования. Безопасный выбор — добавить слово `test` к оригинальному имени +пакета ("creditcard" + "test"): + +```go +// Хорошо: +package creditcardtest +``` + +Если явно не указано иное, все примеры в следующих разделах находятся в `package +creditcardtest`. + +<a id="naming-doubles-simple"></a> + +#### Простой случай (Simple case) + +Вы хотите добавить набор тестовых дублей для `Service`. Поскольку `Card` по сути +является простым типом данных, похожим на сообщение Protocol Buffer, он не +требует специальной обработки в тестах, поэтому дубль не нужен. Если вы ожидаете +только тестовые дубли для одного типа (например, `Service`), вы можете +использовать лаконичный подход к именованию дублей: + +```go +// Хорошо: +import ( + "path/to/creditcard" + "path/to/money" +) + +// Stub заглушает creditcard.Service и не предоставляет собственного поведения. +type Stub struct{} + +func (Stub) Charge(*creditcard.Card, money.Money) error { return nil } +``` + +Это строго предпочтительнее, чем выбор имен типа `StubService` или очень плохого +`StubCreditCardService`, потому что базовое имя пакета и его доменные типы +подразумевают, что такое `creditcardtest.Stub`. + +Наконец, если пакет собирается с помощью Bazel, убедитесь, что новое правило +`go_library` для пакета помечено как `testonly`: + +```build +# Хорошо: +go_library( + name = "creditcardtest", + srcs = ["creditcardtest.go"], + deps = [ + ":creditcard", + ":money", + ], + testonly = True, +) +``` + +Приведенный выше подход является общепринятым и будет достаточно хорошо понят +другими инженерами. + +См. также: + +* [Go Tip #42: Authoring a Stub for + Testing](https://google.github.io/styleguide/go/index.html#gotip) + +<a id="naming-doubles-multiple-behaviors"></a> + +#### Несколько вариантов поведения тестового дубля (Multiple test double behaviors) + +Когда одного вида заглушки недостаточно (например, нужна еще одна, которая +всегда завершается ошибкой), мы рекомендуем называть заглушки в соответствии с +поведением, которое они эмулируют. Здесь мы переименовываем `Stub` в +`AlwaysCharges` и вводим новую заглушку `AlwaysDeclines`: + +```go +// Хорошо: +// AlwaysCharges заглушает creditcard.Service и имитирует успех. +type AlwaysCharges struct{} + +func (AlwaysCharges) Charge(*creditcard.Card, money.Money) error { return nil } + +// AlwaysDeclines заглушает creditcard.Service и имитирует отклоненные операции. +type AlwaysDeclines struct{} + +func (AlwaysDeclines) Charge(*creditcard.Card, money.Money) error { + return creditcard.ErrDeclined +} +``` + +<a id="naming-doubles-multiple-types"></a> + +#### Несколько дублей для нескольких типов (Multiple doubles for multiple types) + +Но теперь предположим, что `package creditcard` содержит несколько типов, для +которых стоит создавать дубли, как показано ниже с `Service` и `StoredValue`: + +```go +package creditcard + +type Service struct { + // опущено +} + +type Card struct { + // опущено +} + +// StoredValue управляет кредитными балансами клиентов. Это применяется, когда +// возвращенный товар зачисляется на локальный счет клиента, а не обрабатывается +// эмитентом кредитной карты. По этой причине он реализован как отдельный сервис. +type StoredValue struct { + // опущено +} + +func (s *StoredValue) Credit(c *Card, amount money.Money) error { /* опущено */ } +``` + +В этом случае более явное именование тестовых дублей имеет смысл: + +```go +// Хорошо: +type StubService struct{} + +func (StubService) Charge(*creditcard.Card, money.Money) error { return nil } + +type StubStoredValue struct{} + +func (StubStoredValue) Credit(*creditcard.Card, money.Money) error { return nil } +``` + +<a id="naming-doubles-local-variables"></a> + +#### Локальные переменные в тестах (Local variables in tests) + +Когда переменные в ваших тестах ссылаются на дубли, выберите имя, которое +наиболее четко отличает дубль от других production-типов, исходя из контекста. +Рассмотрим некоторый production-код, который вы хотите протестировать: + +```go +package payment + +import ( + "path/to/creditcard" + "path/to/money" +) + +type CreditCard interface { + Charge(*creditcard.Card, money.Money) error +} + +type Processor struct { + CC CreditCard +} + +var ErrBadInstrument = errors.New("payment: instrument is invalid or expired") + +func (p *Processor) Process(c *creditcard.Card, amount money.Money) error { + if c.Expired() { + return ErrBadInstrument + } + return p.CC.Charge(c, amount) +} +``` + +В тестах тестовой дубль типа "шпион" (spy) для `CreditCard` противопоставляется +production-типам, поэтому добавление префикса к имени может улучшить ясность. + +```go +// Хорошо: +package payment + +import "path/to/creditcardtest" + +func TestProcessor(t *testing.T) { + var spyCC creditcardtest.Spy + proc := &Processor{CC: spyCC} + + // объявления опущены: card и amount + if err := proc.Process(card, amount); err != nil { + t.Errorf("proc.Process(card, amount) = %v, want nil", err) + } + + charges := []creditcardtest.Charge{ + {Card: card, Amount: amount}, + } + + if got, want := spyCC.Charges, charges; !cmp.Equal(got, want) { + t.Errorf("spyCC.Charges = %v, want %v", got, want) + } +} +``` + +Это понятнее, чем когда имя не имеет префикса. + +```go +// Плохо: +package payment + +import "path/to/creditcardtest" + +func TestProcessor(t *testing.T) { + var cc creditcardtest.Spy + + proc := &Processor{CC: cc} + + // объявления опущены: card и amount + if err := proc.Process(card, amount); err != nil { + t.Errorf("proc.Process(card, amount) = %v, want nil", err) + } + + charges := []creditcardtest.Charge{ + {Card: card, Amount: amount}, + } + + if got, want := cc.Charges, charges; !cmp.Equal(got, want) { + t.Errorf("cc.Charges = %v, want %v", got, want) + } +} +``` + +<a id="shadowing"></a> + +### Затенение (Shadowing) + +**Примечание:** Это объяснение использует два неформальных термина, *stomping* и +*shadowing*. Они не являются официальными концепциями в спецификации языка Go. + +Как и во многих языках программирования, в Go есть изменяемые переменные: +присваивание переменной меняет ее значение. + +```go +// Хорошо: +func abs(i int) int { + if i < 0 { + i *= -1 + } + return i +} +``` + +При использовании [короткого объявления переменных (short variable +declarations)] с оператором `:=` в некоторых случаях новая переменная не +создается. Мы можем назвать это *stomping* (затирание). Это допустимо, когда +исходное значение больше не нужно. + +```go +// Хорошо: +// innerHandler — это вспомогательная функция для некоторого обработчика запросов, который сам +// выполняет запросы к другим бэкендам. +func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse { + // Безусловно ограничиваем срок действия (deadline) для этой части обработки запроса. + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + ctxlog.Info(ctx, "Capped deadline in inner request") + + // Код здесь больше не имеет доступа к оригинальному контексту. + // Это хороший стиль, если при первом написании вы предполагаете, + // что даже по мере роста кода ни одна операция законно не должна + // использовать (возможно, неограниченный) оригинальный контекст, предоставленный вызывающей стороной. + + // ... +} +``` + +Однако будьте осторожны с использованием короткого объявления переменных в новой +области видимости: это вводит новую переменную. Мы можем назвать это *shadowing* +(затенение) исходной переменной. Код после конца блока ссылается на оригинал. +Вот ошибочная попытка условно сократить срок действия (deadline): + +```go +// Плохо: +func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse { + // Попытка условно ограничить срок действия. + if *shortenDeadlines { + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + ctxlog.Info(ctx, "Capped deadline in inner request") + } + + // ОШИБКА: "ctx" здесь снова означает контекст, предоставленный вызывающей стороной. + // Вышеуказанный ошибочный код скомпилировался, потому что и ctx, и cancel + // использовались внутри оператора if. + + // ... +} +``` + +Правильная версия кода может быть такой: + +```go +// Хорошо: +func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse { + if *shortenDeadlines { + var cancel func() + // Обратите внимание на использование простого присваивания, =, а не :=. + ctx, cancel = context.WithTimeout(ctx, 3*time.Second) + defer cancel() + ctxlog.Info(ctx, "Capped deadline in inner request") + } + // ... +} +``` + +В случае, который мы назвали stomping, поскольку нет новой переменной, тип +присваиваемого значения должен совпадать с типом исходной переменной. При +затенении вводится совершенно новая сущность, поэтому она может иметь другой +тип. Намеренное затенение может быть полезной практикой, но вы всегда можете +использовать новое имя, если это улучшает [ясность (clarity)](https://neonxp.ru/pages/gostyleguide/google/guide/#clarity). + +Не рекомендуется использовать переменные с теми же именами, что и у стандартных +пакетов, за исключением очень маленьких областей видимости, потому что это +делает функции и значения из этого пакета недоступными. И наоборот, при выборе +имени для вашего пакета избегайте имен, которые, вероятно, потребуют +[переименования импорта (import renaming)](https://neonxp.ru/pages/gostyleguide/google/decisions/#import-renaming) или +вызовут затенение иначе хороших имен переменных на стороне клиента. + +```go +// Плохо: +func LongFunction() { + url := "https://example.com/" + // Упс, теперь мы не можем использовать net/url в коде ниже. +} +``` + +[короткого объявления переменных (short variable declarations)]: + https://go.dev/ref/spec#Short_variable_declarations + +<a id="util-packages"></a> + +### Пакеты `util` (Util packages) + +Пакеты Go имеют имя, указанное в объявлении `package`, отдельное от пути +импорта. Имя пакета имеет большее значение для читаемости, чем путь. + +Имена пакетов Go должны быть [связаны с тем, что предоставляет +пакет](https://neonxp.ru/pages/gostyleguide/google/decisions/#package-names). Называть пакет просто `util`, `helper`, +`common` или подобным обычно плохой выбор (хотя это может быть использовано как +*часть* имени). Неинформативные имена затрудняют чтение кода, и если они +используются слишком широко, они могут вызывать ненужные [конфликты +импорта](https://neonxp.ru/pages/gostyleguide/google/decisions/#import-renaming). + +Вместо этого подумайте, как будет выглядеть место вызова (callsite). + +```go +// Хорошо: +db := spannertest.NewDatabaseFromFile(...) + +_, err := f.Seek(0, io.SeekStart) + +b := elliptic.Marshal(curve, x, y) +``` + +Вы можете примерно понять, что делает каждая из этих строк, даже не зная списка +импортов (`cloud.google.com/go/spanner/spannertest`, `io` и `crypto/elliptic`). +С менее сфокусированными именами они могли бы читаться так: + +```go +// Плохо: +db := test.NewDatabaseFromFile(...) + +_, err := f.Seek(0, common.SeekStart) + +b := helper.Marshal(curve, x, y) +``` + +<a id="package-size"></a> + +## Размер пакета (Package size) + +Если вы задаетесь вопросом, насколько большими должны быть ваши пакеты Go и +следует ли помещать связанные типы в один пакет или разделять их на разные, +хорошим началом будет [пост в блоге Go об именах пакетов][blog-pkg-names]. +Несмотря на название поста, он не только об именовании. Он содержит полезные +подсказки и ссылается на несколько полезных статей и докладов. + +Вот некоторые другие соображения и примечания. + +Пользователи видят [godoc] для пакета на одной странице, и любые +экспортированные методы типов, предоставляемых пакетом, группируются по их типу. +Godoc также группирует конструкторы вместе с типами, которые они возвращают. +Если *клиентскому коду* (client code) вероятно потребуется, чтобы два значения +разных типов взаимодействовали друг с другом, может быть удобно для пользователя +иметь их в одном пакете. + +Код внутри пакета имеет доступ к неэкспортированным идентификаторам пакета. Если +у вас есть несколько связанных типов, *реализация* которых тесно связана, +размещение их в одном пакете позволяет достичь этой связи без загрязнения +публичного API этими деталями. Хороший тест для этой связи — представить +гипотетического пользователя двух пакетов, где пакеты охватывают тесно связанные +темы: если пользователь должен импортировать оба пакета, чтобы использовать +любой из них хоть сколько-нибудь значимо, обычно правильным решением будет +объединить их вместе. Стандартная библиотека в целом хорошо демонстрирует такую +область видимости (scoping) и слоистость (layering). + +При всем сказанном, помещение всего вашего проекта в один пакет, вероятно, +сделает этот пакет слишком большим. Когда что-то концептуально отличается, +предоставление ему собственного небольшого пакета может облегчить его +использование. Короткое имя пакета, известное клиентам, вместе с именем +экспортированного типа работают вместе, чтобы создать значимый идентификатор: +например, `bytes.Buffer`, `ring.New`. [Пост об именах пакетов][blog-pkg-names] +содержит больше примеров. + +Стиль Go гибок относительно размера файлов, потому что сопровождающие могут +перемещать код внутри пакета из одного файла в другой, не влияя на вызывающую +сторону. Но в качестве общего руководства: обычно не стоит иметь один файл с +тысячами строк или множество крошечных файлов. Нет такого соглашения, как "один +тип — один файл", как в некоторых других языках. Эмпирическое правило: файлы +должны быть достаточно сфокусированными, чтобы сопровождающий мог определить, в +каком файле что-то находится, и достаточно маленькими, чтобы было легко найти +это, когда вы там окажетесь. Стандартная библиотека часто разделяет большие +пакеты на несколько исходных файлов, группируя связанный код по файлам. Исходный +код [пакета `bytes`] является хорошим примером. Пакеты с длинной документацией +могут выбрать выделение одного файла с именем `doc.go`, который содержит +[документацию пакета](https://neonxp.ru/pages/gostyleguide/google/decisions/#package-comments), объявление пакета и больше +ничего, но это не обязательно. + +Внутри кодовой базы Google и в проектах, использующих Bazel, структура каталогов +для кода Go отличается от таковой в open source проектах на Go: вы можете иметь +несколько целей `go_library` в одном каталоге. Хорошей причиной для выделения +каждому пакету собственного каталога является ожидание открытия исходного кода +вашего проекта в будущем. + +Несколько неканонических справочных примеров, чтобы помочь продемонстрировать +эти идеи на практике: + +* маленькие пакеты, содержащие одну связную идею, которая не требует + добавления или удаления чего-либо еще: + + * [пакет `csv`][package `csv`]: кодирование и декодирование данных CSV с + разделением ответственности соответственно между [reader.go] и + [writer.go]. + * [пакет `expvar`][package `expvar`]: "белый ящик" (whitebox) телеметрии + программы, полностью содержащийся в [expvar.go]. + +* пакеты умеренного размера, содержащие одну большую предметную область и + несколько связанных с ней ответственностей: + + * [пакет `flag`][package `flag`]: управление флагами командной строки, + полностью содержащееся в [flag.go]. + +* большие пакеты, которые разделяют несколько тесно связанных предметных + областей по нескольким файлам: + + * [пакет `http`][package `http`]: ядро HTTP: [client.go][http-client], + поддержка HTTP-клиентов; [server.go][http-server], поддержка + HTTP-серверов; [cookie.go], управление куками. + * [пакет `os`][package `os`]: кроссплатформенные абстракции операционной + системы: [exec.go], управление подпроцессами; [file.go], управление + файлами; [tempfile.go], временные файлы. + +См. также: + +* [Пакеты тестовых дублей (Test double packages)](#naming-doubles) +* [Organizing Go Code (Blog Post)] +* [Organizing Go Code (Presentation)] + +[blog-pkg-names]: https://go.dev/blog/package-names +[пакет `bytes`]: https://go.dev/src/bytes/ +[Organizing Go Code (Blog Post)]: https://go.dev/blog/organizing-go-code +[Organizing Go Code (Presentation)]: https://go.dev/talks/2014/organizeio.slide +[пакет `csv`]: https://pkg.go.dev/encoding/csv +[reader.go]: + https://go.googlesource.com/go/+/refs/heads/master/src/encoding/csv/reader.go +[writer.go]: + https://go.googlesource.com/go/+/refs/heads/master/src/encoding/csv/writer.go +[пакет `expvar`]: https://pkg.go.dev/expvar +[expvar.go]: + https://go.googlesource.com/go/+/refs/heads/master/src/expvar/expvar.go +[пакет `flag`]: https://pkg.go.dev/flag +[flag.go]: https://go.googlesource.com/go/+/refs/heads/master/src/flag/flag.go +[godoc]: https://pkg.go.dev/ +[пакет `http`]: https://pkg.go.dev/net/http +[http-client]: + https://go.googlesource.com/go/+/refs/heads/master/src/net/http/client.go +[http-server]: + https://go.googlesource.com/go/+/refs/heads/master/src/net/http/server.go +[cookie.go]: + https://go.googlesource.com/go/+/refs/heads/master/src/net/http/cookie.go +[пакет `os`]: https://pkg.go.dev/os +[exec.go]: https://go.googlesource.com/go/+/refs/heads/master/src/os/exec.go +[file.go]: https://go.googlesource.com/go/+/refs/heads/master/src/os/file.go +[tempfile.go]: + https://go.googlesource.com/go/+/refs/heads/master/src/os/tempfile.go + +<a id="imports"></a> + +## Импорт (Imports) + +<a id="import-protos"></a> + +### Сообщения Protocol Buffer и заглушки (Stubs) + +Импорты библиотек proto обрабатываются иначе, чем стандартные импорты Go, из-за +их межъязыковой природы. Соглашение для переименованных импортов proto основано +на правиле, которое сгенерировало пакет: + +* Суффикс `pb` обычно используется для правил `go_proto_library`. +* Суффикс `grpc` обычно используется для правил `go_grpc_library`. + +Часто используется одно слово, описывающее пакет: + +```go +// Хорошо: +import ( + foopb "path/to/package/foo_service_go_proto" + foogrpc "path/to/package/foo_service_go_grpc" +) +``` + +Следуйте рекомендациям по стилю для [имен +пакетов](https://google.github.io/styleguide/go/decisions#package-names). +Предпочитайте целые слова. Короткие имена хороши, но избегайте неоднозначности. +В случае сомнений используйте имя пакета proto до _go с суффиксом pb: + +```go +// Хорошо: +import ( + pushqueueservicepb "path/to/package/push_queue_service_go_proto" +) +``` + +**Примечание:** Предыдущие рекомендации поощряли очень короткие имена, такие как +"xpb" или даже просто "pb". Новый код должен предпочитать более описательные +имена. Существующий код, использующий короткие имена, не должен использоваться в +качестве примера, но его не нужно менять. + +<a id="import-order"></a> + +### Порядок импорта (Import ordering) + +См. [Go Style Decisions: Группировка импортов](https://neonxp.ru/pages/gostyleguide/google/decisions/.md#import-grouping). + +<a id="error-handling"></a> + +## Обработка ошибок (Error handling) + +В Go [ошибки — это значения (errors are values)]; они создаются кодом и +потребляются кодом. Ошибки могут быть: + +* Преобразованы в диагностическую информацию для отображения человеку +* Использованы сопровождающим +* Интерпретированы конечным пользователем + +Сообщения об ошибках также появляются на самых разных поверхностях, включая +сообщения журнала (log messages), дампы ошибок и отрисованные пользовательские +интерфейсы. + +Код, который обрабатывает (производит или потребляет) ошибки, должен делать это +осознанно. Может возникнуть соблазн проигнорировать или слепо распространить +возвращаемое значение ошибки. Однако всегда стоит подумать, находится ли текущая +функция в стеке вызовов в наилучшей позиции для обработки ошибки. Это обширная +тема, и трудно дать категоричные рекомендации. Используйте свое суждение, но +учитывайте следующие соображения: + +* Создавая значение ошибки, решите, придавать ли ему какую-либо + [структуру](#error-structure). +* Обрабатывая ошибку, рассмотрите возможность [добавления + информации](#error-extra-info), которая есть у вас, но которой может не быть + у вызывающей и/или вызываемой стороны. +* См. также рекомендации по [логированию ошибок](#error-logging). + +Хотя обычно нецелесообразно игнорировать ошибку, разумным исключением из этого +является оркестрация связанных операций, где часто только первая ошибка полезна. +Пакет [`errgroup`] предоставляет удобную абстракцию для группы операций, которые +могут завершиться ошибкой или быть отменены как группа. + +[ошибки — это значения (errors are values)]: + https://go.dev/blog/errors-are-values +[`errgroup`]: https://pkg.go.dev/golang.org/x/sync/errgroup + +См. также: + +* [Effective Go об ошибках](https://go.dev/doc/effective_go#errors) +* [Пост в блоге Go об ошибках](https://go.dev/blog/go1.13-errors) +* [Пакет `errors`](https://pkg.go.dev/errors) +* [Пакет + `upspin.io/errors`](https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html) +* [GoTip #89: When to Use Canonical Status Codes as + Errors](https://google.github.io/styleguide/go/index.html#gotip) +* [GoTip #48: Error Sentinel + Values](https://google.github.io/styleguide/go/index.html#gotip) +* [GoTip #13: Designing Errors for + Checking](https://google.github.io/styleguide/go/index.html#gotip) + +<a id="error-structure"></a> + +### Структура ошибок (Error structure) + +Если вызывающим сторонам необходимо анализировать ошибку (например, различать +различные условия ошибки), придайте значению ошибки структуру, чтобы это можно +было сделать программно, а не заставлять вызывающую сторону выполнять +сопоставление строк. Этот совет применим как к production-коду, так и к тестам, +которые заботятся о разных условиях ошибок. + +Простейшие структурированные ошибки — это непараметризованные глобальные +значения. + +```go +type Animal string + +var ( + // ErrDuplicate возникает, если это животное уже было замечено. + ErrDuplicate = errors.New("duplicate") + + // ErrMarsupial возникает, потому что у нас аллергия на сумчатых за пределами Австралии. + // Извините. + ErrMarsupial = errors.New("marsupials are not supported") +) + +func process(animal Animal) error { + switch { + case seen[animal]: + return ErrDuplicate + case marsupial(animal): + return ErrMarsupial + } + seen[animal] = true + // ... + return nil +} +``` + +Вызывающая сторона может просто сравнить возвращенное значение ошибки функции с +одним из известных значений ошибок: + +```go +// Хорошо: +func handlePet(...) { + switch err := process(an); err { + case ErrDuplicate: + return fmt.Errorf("feed %q: %v", an, err) + case ErrMarsupial: + // Попробуем восстановиться с помощью друга. + alternate = an.BackupAnimal() + return handlePet(..., alternate, ...) + } +} +``` + +Выше используются сторожевые (sentinel) значения, где ошибка должна быть равна +(в смысле `==`) ожидаемому значению. Во многих случаях это вполне адекватно. +Если `process` возвращает обернутые ошибки (wrapped errors) (обсуждается ниже), +вы можете использовать [`errors.Is`]. + +```go +// Хорошо: +func handlePet(...) { + switch err := process(an); { + case errors.Is(err, ErrDuplicate): + return fmt.Errorf("feed %q: %v", an, err) + case errors.Is(err, ErrMarsupial): + // ... + } +} +``` + +Не пытайтесь различать ошибки на основе их строковой формы. (См. [GoTip #13: +Designing Errors for +Checking](https://google.github.io/styleguide/go/index.html#gotip) для получения +дополнительной информации.) + +```go +// Плохо: +func handlePet(...) { + err := process(an) + if regexp.MatchString(`duplicate`, err.Error()) {...} + if regexp.MatchString(`marsupial`, err.Error()) {...} +} +``` + +Если в ошибке есть дополнительная информация, которая нужна вызывающей стороне +программно, ее, в идеале, следует представить структурно. Например, тип +[`os.PathError`] документирован так, что помещает путь к операции, завершившейся +неудачей, в поле структуры, к которому вызывающая сторона может легко получить +доступ. + +Могут использоваться и другие структуры ошибок, например, структура проекта, +содержащая код ошибки и строку с деталями. [Пакет `status`][status] — +распространенная инкапсуляция; если вы выбираете этот подход (вы не обязаны это +делать), используйте [канонические коды (canonical codes)]. См. [GoTip #89: +When to Use Canonical Status Codes as +Errors](https://google.github.io/styleguide/go/index.html#gotip) чтобы понять, +является ли использование кодов статуса правильным выбором. + +[`os.PathError`]: https://pkg.go.dev/os#PathError +[`errors.Is`]: https://pkg.go.dev/errors#Is +[`errors.As`]: https://pkg.go.dev/errors#As +[`package cmp`]: https://pkg.go.dev/github.com/google/go-cmp/cmp +[status]: https://pkg.go.dev/google.golang.org/grpc/status +[канонические коды (canonical codes)]: + https://pkg.go.dev/google.golang.org/grpc/codes + +<a id="error-extra-info"></a> + +### Добавление информации к ошибкам (Adding information to errors) + +Добавляя информацию к ошибкам, избегайте избыточной информации, которую уже +предоставляет лежащая в основе ошибка. Пакет `os`, например, уже включает +информацию о пути в своих ошибках. + +```go +// Хорошо: +if err := os.Open("settings.txt"); err != nil { + return fmt.Errorf("launch codes unavailable: %v", err) +} + +// Вывод: +// +// launch codes unavailable: open settings.txt: no such file or directory +``` + +Здесь "launch codes unavailable" добавляет конкретный смысл ошибке `os.Open`, +релевантный для контекста текущей функции, без дублирования информации о пути к +файлу. + +```go +// Плохо: +if err := os.Open("settings.txt"); err != nil { + return fmt.Errorf("could not open settings.txt: %v", err) +} + +// Вывод: +// +// could not open settings.txt: open settings.txt: no such file or directory +``` + +Не добавляйте аннотацию, если ее единственная цель — указать на сбой без +добавления новой информации. Наличие ошибки достаточно передает сбой вызывающей +стороне. + +```go +// Плохо: +return fmt.Errorf("failed: %v", err) // просто верните err вместо этого +``` + +[Выбор между `%v` и `%w` при оборачивании ошибок (wrapping +errors)](https://go.dev/blog/go1.13-errors#whether-to-wrap) с помощью +`fmt.Errorf` — это тонкое решение, которое значительно влияет на то, как ошибки +распространяются, обрабатываются, проверяются и документируются в вашем +приложении. Основной принцип — сделать значения ошибок полезными для их +наблюдателей, будь то люди или код. + +1. **`%v` для простой аннотации или новой ошибки** + + Глагол `%v` — это ваш универсальный инструмент для строкового форматирования + любого значения Go, включая ошибки. При использовании с `fmt.Errorf` он + встраивает строковое представление ошибки (то, что возвращает ее метод + `Error()`) в новое значение ошибки, отбрасывая любую структурированную + информацию из исходной ошибки. Примеры использования `%v`: + + * Добавление интересного, не избыточного контекста: как в примере выше. + + * Логирование или отображение ошибок: Когда основная цель — представить + удобочитаемое сообщение об ошибке в журналах или пользователю, и вы не + планируете, чтобы вызывающая сторона программно проверяла ошибку с + помощью `errors.Is` или `errors.As` (Примечание: `errors.Unwrap` здесь, + как правило, не рекомендуется, так как он не обрабатывает множественные + ошибки (multi-errors)). + + * Создание новых, независимых ошибок: Иногда необходимо преобразовать + ошибку в новое сообщение об ошибке, тем самым скрывая специфику исходной + ошибки. Эта практика особенно полезна на границах систем, включая, + помимо прочего, RPC, IPC и хранилища, где мы переводим + доменно-специфичные ошибки в каноническое пространство ошибок. + + ```go + // Хорошо: + func (*FortuneTeller) SuggestFortune(context.Context, *pb.SuggestionRequest) (*pb.SuggestionResponse, error) { + // ... + if err != nil { + return nil, fmt.Errorf("couldn't find fortune database: %v", err) + } + } + ``` + + Мы также могли бы явно аннотировать RPC код `Internal` в примере выше. + + ```go + // Хорошо: + import ( + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + ) + + func (*FortuneTeller) SuggestFortune(context.Context, *pb.SuggestionRequest) (*pb.SuggestionResponse, error) { + // ... + if err != nil { + // Или используйте fmt.Errorf с глаголом %w, если намеренно оборачиваете ошибку, + // которую вызывающая сторона должна развернуть (unwrap). + return nil, status.Errorf(codes.Internal, "couldn't find fortune database", status.ErrInternal) + } + } + ``` + +1. **`%w` (wrap) для программной проверки и цепочки ошибок (error chaining)** + + Глагол `%w` специально предназначен для оборачивания ошибок (error + wrapping). Он создает новую ошибку, которая предоставляет метод `Unwrap()`, + позволяя вызывающим сторонам программно проверять цепочку ошибок с помощью + `errors.Is` и `errors.As`. Примеры использования `%w`: + + * Добавление контекста с сохранением исходной ошибки для программной + проверки: Это основной случай использования во вспомогательных функциях + (helpers) вашего приложения. Вы хотите обогатить ошибку дополнительным + контекстом (например, какая операция выполнялась, когда она завершилась + неудачей), но при этом позволить вызывающей стороне проверить, является + ли лежащая в основе ошибка конкретной сторожевой ошибкой или типом. + + ```go + // Хорошо: + func (s *Server) internalFunction(ctx context.Context) error { + // ... + if err != nil { + return fmt.Errorf("couldn't find remote file: %w", err) + } + } + ``` + + Это позволяет функции более высокого уровня выполнить `errors.Is(err, + fs.ErrNotExist)`, даже если исходная ошибка была обернута. + + В точках, где ваша система взаимодействует с внешними системами, такими + как RPC, IPC или хранилище, часто лучше переводить доменно-специфичные + ошибки в стандартизированное пространство ошибок (например, коды статуса + gRPC), а не просто оборачивать исходную ошибку с помощью `%w`. Клиента + обычно не волнует точная внутренняя ошибка файловой системы; их волнует + канонический результат (например, `Internal`, `NotFound`, + `PermissionDenied`). + + * Когда вы явно документируете и тестируете лежащие в основе ошибки, + которые вы раскрываете: Если API вашего пакета гарантирует, что + определенные лежащие в основе ошибки могут быть развернуты и проверены + вызывающими сторонами (например, "эта функция может вернуть + `ErrInvalidConfig`, обернутый в более общую ошибку"), то `%w` уместен. + Это становится частью контракта вашего пакета. + +См. также: + +* [Соглашения по документации ошибок (Error Documentation + Conventions)](#documentation-conventions-errors) +* [Пост в блоге об оборачивании ошибок](https://blog.golang.org/go1.13-errors) + +<a id="error-percent-w"></a> + +### Размещение `%w` в ошибках (Placement of %w in errors) + +Предпочитайте размещать `%w` в конце строки ошибки *если* вы используете +[оборачивание ошибок (error wrapping)](https://go.dev/blog/go1.13-errors) с +глаголом форматирования `%w`. + +Ошибки могут быть обернуты с помощью глагола `%w` или путем помещения их в +[структурированную +ошибку](https://google.github.io/styleguide/go/index.html#gotip), которая +реализует `Unwrap() error` (например, +[`fs.PathError`](https://pkg.go.dev/io/fs#PathError)). + +Обернутые ошибки образуют цепочки ошибок (error chains): каждый новый слой +обертывания добавляет новую запись в начало цепочки ошибок. Цепочку ошибок можно +обойти с помощью метода `Unwrap() error`. Например: + +```go +err1 := fmt.Errorf("err1") +err2 := fmt.Errorf("err2: %w", err1) +err3 := fmt.Errorf("err3: %w", err2) +``` + +Это формирует цепочку ошибок следующего вида: + +```mermaid +flowchart LR + err3 == err3 wraps err2 ==> err2; + err2 == err2 wraps err1 ==> err1; +``` + +Независимо от того, где размещен глагол `%w`, возвращаемая ошибка всегда +представляет начало цепочки ошибок, а `%w` — это следующий дочерний элемент. +Аналогично, `Unwrap() error` всегда обходит цепочку ошибок от самой новой к +самой старой ошибке. + +Однако размещение глагола `%w` влияет на то, печатается ли цепочка ошибок от +самой новой к самой старой, от самой старой к самой новой или ни то, ни другое: + +```go +// Хорошо: +err1 := fmt.Errorf("err1") +err2 := fmt.Errorf("err2: %w", err1) +err3 := fmt.Errorf("err3: %w", err2) +fmt.Println(err3) // err3: err2: err1 +// err3 — это цепочка ошибок от самой новой к самой старой, которая печатается от самой новой к самой старой. +``` + +```go +// Плохо: +err1 := fmt.Errorf("err1") +err2 := fmt.Errorf("%w: err2", err1) +err3 := fmt.Errorf("%w: err3", err2) +fmt.Println(err3) // err1: err2: err3 +// err3 — это цепочка ошибок от самой новой к самой старой, которая печатается от самой старой к самой новой. +``` + +```go +// Плохо: +err1 := fmt.Errorf("err1") +err2 := fmt.Errorf("err2-1 %w err2-2", err1) +err3 := fmt.Errorf("err3-1 %w err3-2", err2) +fmt.Println(err3) // err3-1 err2-1 err1 err2-2 err3-2 +// err3 — это цепочка ошибок от самой новой к самой старой, которая печатается ни от самой новой к самой старой, +// ни от самой старой к самой новой. +``` + +Поэтому, чтобы текст ошибки отражал структуру цепочки ошибок, предпочитайте +размещать глагол `%w` в конце в форме `[...]: %w`. + +<a id="error-logging"></a> + +### Логирование ошибок (Logging errors) + +Иногда функциям необходимо сообщить внешней системе об ошибке, не передавая ее +своим вызывающим сторонам. Логирование — очевидный выбор здесь; но будьте +внимательны к тому, что и как вы логируете. + +* Как и [хорошие сообщения о неудачных тестах (good test failure messages)], + сообщения журнала должны четко выражать, что пошло не так, и помогать + сопровождающему, включая соответствующую информацию для диагностики + проблемы. + +* Избегайте дублирования. Если вы возвращаете ошибку, обычно лучше не + логировать ее самостоятельно, а позволить вызывающей стороне обработать ее. + Вызывающая сторона может выбрать логирование ошибки или, возможно, + ограничить частоту логирования с помощью [`rate.Sometimes`]. Другие варианты + включают попытку восстановления или даже [остановку программы]. В любом + случае, предоставление контроля вызывающей стороне помогает избежать спама в + журналах. + + Однако обратной стороной этого подхода является то, что любое логирование + записывается с использованием координат строк вызывающей стороны. + +* Будьте осторожны с [PII]. Многие приемники журналов (log sinks) не являются + подходящими местами назначения для конфиденциальной информации конечных + пользователей. + +* Используйте `log.Error` скупо. Логирование уровня ERROR вызывает сброс + (flush) и является более дорогостоящим, чем более низкие уровни логирования. + Это может серьезно повлиять на производительность вашего кода. Принимая + решение между уровнями error и warning, учитывайте лучшую практику: + сообщения на уровне error должны быть actionable (то есть требовать + действий), а не просто "более серьезными", чем warning. + +* Внутри Google у нас есть системы мониторинга, которые можно настроить для + более эффективного оповещения, чем просто запись в файл журнала в надежде, + что кто-то его заметит. Это похоже, но не идентично стандартной библиотеке + [пакету `expvar`]. + +[хорошие сообщения о неудачных тестах (good test failure messages)]: + https://google.github.io/styleguide/go/decisions#useful-test-failures +[остановку программы]: #checks-and-panics +[`rate.Sometimes`]: https://pkg.go.dev/golang.org/x/time/rate#Sometimes +[PII]: https://en.wikipedia.org/wiki/Personal_data +[пакет `expvar`]: https://pkg.go.dev/expvar + +<a id="vlog"></a> + +#### Пользовательские уровни детализации (Custom verbosity levels) + +Используйте детальное логирование ([`log.V`]) с пользой. Детальное логирование +может быть полезно для разработки и трассировки. Установление соглашения об +уровнях детализации может быть полезным. Например: + +* Записывайте небольшое количество дополнительной информации на `V(1)` +* Трассируйте больше информации на `V(2)` +* Выводите большие внутренние состояния на `V(3)` + +Чтобы минимизировать стоимость детального логирования, вы должны убедиться, что +случайно не вызываете дорогие функции, даже когда `log.V` выключен. `log.V` +предлагает два API. Более удобный из них несет риск этих случайных затрат. В +случае сомнений используйте немного более многословный стиль. + +```go +// Хорошо: +for _, sql := range queries { + log.V(1).Infof("Handling %v", sql) + if log.V(2) { + log.Infof("Handling %v", sql.Explain()) + } + sql.Run(...) +} +``` + +```go +// Плохо: +// sql.Explain вызывается даже когда это сообщение не печатается. +log.V(2).Infof("Handling %v", sql.Explain()) +``` + +[`log.V`]: https://pkg.go.dev/github.com/golang/glog#V + +<a id="program-init"></a> + +### Инициализация программы (Program initialization) + +Ошибки инициализации программы (например, неправильные флаги и конфигурация) +должны передаваться вверх в `main`, который должен вызвать `log.Exit` с ошибкой, +объясняющей, как исправить ошибку. В этих случаях `log.Fatal` обычно не следует +использовать, потому что трассировка стека, указывающая на проверку, вряд ли +будет так полезна, как сгенерированное человеком, actionable сообщение. + +<a id="checks-and-panics"></a> + +### Проверки программы и паники (Program checks and panics) + +Как указано в [решении против паник (decision against panics)], стандартная +обработка ошибок должна быть структурирована вокруг возвращаемых значений +ошибок. Библиотеки должны предпочитать возвращать ошибку вызывающей стороне, а +не завершать программу, особенно для временных ошибок. + +Иногда необходимо выполнить проверку согласованности (consistency check) +инварианта и завершить программу, если он нарушен. Как правило, это делается +только в том случае, если сбой проверки инварианта означает, что внутреннее +состояние стало невосстановимым. Наиболее надежный способ сделать это в кодовой +базе Google — вызвать `log.Fatal`. Использование `panic` в этих случаях +ненадежно, потому что возможно, что отложенные (deferred) функции заблокируют +или еще больше повредят внутреннее или внешнее состояние. + +Аналогично, сопротивляйтесь искушению восстановить паники (recover panics), +чтобы избежать сбоев, так как это может привести к распространению поврежденного +состояния. Чем дальше вы от паники, тем меньше вы знаете о состоянии программы, +которая может удерживать блокировки или другие ресурсы. Затем программа может +развить другие неожиданные режимы сбоев, которые могут еще больше затруднить +диагностику проблемы. Вместо того чтобы пытаться обрабатывать неожиданные паники +в коде, используйте инструменты мониторинга для выявления неожиданных сбоев и +исправляйте связанные ошибки с высоким приоритетом. + +**Примечание:** Стандартный [`net/http` server] нарушает этот совет и +восстанавливает паники из обработчиков запросов. Консенсус среди опытных +инженеров Go заключается в том, что это была историческая ошибка. Если вы +исследуете журналы серверов приложений на других языках, часто можно найти +большие трассировки стека, которые остаются необработанными. Избегайте этой +ловушки в своих серверах. + +[решении против паник (decision against panics)]: + https://google.github.io/styleguide/go/decisions#dont-panic +[`net/http` server]: https://pkg.go.dev/net/http#Server + +<a id="when-to-panic"></a> + +### Когда использовать panic (When to panic) + +Стандартная библиотека вызывает panic при неправильном использовании API. +Например, [`reflect`] вызывает panic во многих случаях, когда значение доступно +таким образом, что предполагает его неправильную интерпретацию. Это аналогично +паникам на ошибки ядра языка, такие как доступ к элементу среза вне его границ. +Проверка кода и тесты должны обнаруживать такие ошибки, которые не ожидаются в +production-коде. Эти паники действуют как проверки инвариантов, которые не +зависят от библиотеки, поскольку стандартная библиотека не имеет доступа к +[уровневому пакету `log`], который используется в кодовой базе Google. + +[`reflect`]: https://pkg.go.dev/reflect +[уровневому пакету `log`]: decisions#logging + +Другой случай, когда паники могут быть полезны, хотя и нечасто, — это внутренняя +деталь реализации пакета, которая всегда имеет соответствующее восстановление +(recover) в цепочке вызовов. Парсеры и подобные глубоко вложенные, тесно +связанные внутренние группы функций могут выиграть от такого дизайна, где +проталкивание возвратов ошибок добавляет сложность без ценности. + +Ключевой атрибут этого дизайна заключается в том, что эти **паники никогда не +должны выходить за границы пакета** и не должны быть частью API пакета. Обычно +это достигается с помощью функции верхнего уровня с отложенным вызовом +(deferred), которая использует `recover` для преобразования распространенной +паники в возвращаемую ошибку на публичной границе API. Это требует, чтобы код, +который вызывает panic и восстанавливается, отличал паники, которые код вызывает +сам, от тех, которые он не вызывает: + +```go +// Хорошо: +type syntaxError struct { + msg string +} + +func parseInt(in string) int { + n, err := strconv.Atoi(in) + if err != nil { + panic(&syntaxError{"not a valid integer"}) + } +} + +func Parse(in string) (_ *Node, err error) { + defer func() { + if p := recover(); p != nil { + sErr, ok := p.(*syntaxError) + if !ok { + panic(p) // Распространяем panic, поскольку он находится вне области нашего кода. + } + err = fmt.Errorf("syntax error: %v", sErr.msg) + } + }() + ... // Парсим входные данные, вызывая parseInt внутри для парсинга целых чисел +} +``` + +> **Предупреждение:** Код, использующий этот шаблон, должен позаботиться об +> управлении любыми ресурсами, связанными с кодом, запущенным в таких разделах, +> управляемых defer (например, закрыть, освободить или разблокировать). +> +> См.: [Go Tip #81: Avoiding Resource Leaks in API Design] + +Паника также используется, когда компилятор не может идентифицировать +недостижимый код, например, при использовании функции типа `log.Fatal`, которая +не вернется: + +```go +// Хорошо: +func answer(i int) string { + switch i { + case 42: + return "yup" + case 54: + return "base 13, huh" + default: + log.Fatalf("Sorry, %d is not the answer.", i) + panic("unreachable") + } +} +``` + +[Не вызывайте функции `log` до того, как флаги будут +распарсены.](https://pkg.go.dev/github.com/golang/glog#pkg-overview) Если вы +должны завершиться в функции инициализации пакета (в `init` или +["must"-функции](https://neonxp.ru/pages/gostyleguide/google/decisions/#must-functions)), panic допустима вместо вызова +фатального логирования. + +См. также: + +* [Handling panics](https://go.dev/ref/spec#Handling_panics) и [Run-time + Panics](https://go.dev/ref/spec#Run_time_panics) в спецификации языка +* [Defer, Panic, and Recover](https://go.dev/blog/defer-panic-and-recover) +* [On the uses and misuses of panics in + Go](https://eli.thegreenplace.net/2018/on-the-uses-and-misuses-of-panics-in-go/) + +[Go Tip #81: Avoiding Resource Leaks in API Design]: + https://google.github.io/styleguide/go/index.html#gotip + +<a id="documentation"></a> + +## Документация (Documentation) + +<a id="documentation-conventions"></a> + +### Соглашения (Conventions) + +Этот раздел дополняет раздел [комментариев (commentary)] в документе решений. + +Код на Go, который документирован в знакомом стиле, легче читать и менее +вероятно, что его будут использовать неправильно, по сравнению с тем, что плохо +задокументировано или не задокументировано вовсе. Запускаемые [примеры +(examples)] появляются в Godoc и Code Search и являются отличным способом +объяснить, как использовать ваш код. + +[комментариев (commentary)]: decisions#commentary +[примеры (examples)]: decisions#examples + +<a id="documentation-conventions-params"></a> + +#### Параметры и конфигурация (Parameters and configuration) + +Не каждый параметр должен быть перечислен в документации. Это относится к: + +* параметрам функций и методов +* полям структур (struct fields) +* API для опций (options) + +Документируйте подверженные ошибкам или неочевидные поля и параметры, объясняя, +почему они интересны. + +В следующем фрагменте выделенный комментарий добавляет мало полезной информации +для читателя: + +```go +// Плохо: +// Sprintf форматирует в соответствии со спецификатором формата и возвращает результирующую строку. +// +// format — это формат, а data — данные для интерполяции. +func Sprintf(format string, data ...any) string +``` + +Однако этот фрагмент демонстрирует сценарий кода, похожий на предыдущий, где +комментарий вместо этого говорит что-то неочевидное или существенно полезное для +читателя: + +```go +// Хорошо: +// Sprintf форматирует в соответствии со спецификатором формата и возвращает результирующую строку. +// +// Предоставленные данные используются для интерполяции строки формата. Если данные не соответствуют +// ожидаемым глаголам формата или количество данных не удовлетворяет спецификации формата, +// функция будет встраивать предупреждения об ошибках форматирования в выходную строку, как описано +// в разделе "Format errors" выше. +func Sprintf(format string, data ...any) string +``` + +Учитывайте вашу вероятную аудиторию при выборе того, что документировать и на +какую глубину. Сопровождающие, новички в команде, внешние пользователи и даже вы +сами через шесть месяцев могут оценить немного другую информацию, отличную от +той, что у вас на уме, когда вы впервые начинаете писать документацию. + +См. также: + +* [GoTip #41: Identify Function Call Parameters] +* [GoTip #51: Patterns for Configuration] + +[GoTip #41: Identify Function Call Parameters]: + https://google.github.io/styleguide/go/index.html#gotip +[GoTip #51: Patterns for Configuration]: + https://google.github.io/styleguide/go/index.html#gotip + +<a id="documentation-conventions-contexts"></a> + +#### Контексты (Contexts) + +Подразумевается, что отмена (cancellation) аргумента контекста прерывает +функцию, которой он предоставлен. Если функция может возвращать ошибку, по +соглашению это `ctx.Err()`. + +Этот факт не нужно повторять: + +```go +// Плохо: +// Run выполняет рабочий цикл (run loop) воркера. +// +// Метод будет обрабатывать работу до отмены контекста и соответственно возвращает ошибку. +func (Worker) Run(ctx context.Context) error +``` + +Поскольку это подразумевается, следующее лучше: + +```go +// Хорошо: +// Run выполняет рабочий цикл воркера. +func (Worker) Run(ctx context.Context) error +``` + +Когда поведение контекста отличается или неочевидно, его следует прямо +задокументировать, если верно любое из следующего. + +* Функция возвращает ошибку, отличную от `ctx.Err()`, когда контекст отменен: + + ```go + // Хорошо: + // Run выполняет рабочий цикл воркера. + // + // Если контекст отменен, Run возвращает nil ошибку. + func (Worker) Run(ctx context.Context) error + ``` + +* Функция имеет другие механизмы, которые могут ее прервать или повлиять на + время жизни: + + ```go + // Хорошо: + // Run выполняет рабочий цикл воркера. + // + // Run обрабатывает работу до отмены контекста или вызова Stop. + // Отмена контекста обрабатывается асинхронно внутри: run может вернуться до того, + // как вся работа остановится. Метод Stop является синхронным и ожидает завершения + // всех операций из рабочего цикла. Используйте Stop для плавного завершения работы. + func (Worker) Run(ctx context.Context) error + + func (Worker) Stop() + ``` + +* Функция имеет особые ожидания относительно времени жизни контекста, его + происхождения (lineage) или прикрепленных значений (attached values): + + ```go + // Хорошо: + // NewReceiver начинает получать сообщения, отправленные в указанную очередь. + // Контекст не должен иметь дедлайна (deadline). + func NewReceiver(ctx context.Context) *Receiver + + // Principal возвращает человеко-читаемое имя стороны, совершившей вызов. + // Контекст должен иметь прикрепленное к нему значение из security.NewContext. + func Principal(ctx context.Context) (name string, ok bool) + ``` + + **Предупреждение:** Избегайте разработки API, которые предъявляют такие + требования (например, отсутствие дедлайнов у контекстов) от своих вызывающих + сторон. Вышеприведенное — лишь пример того, как это задокументировать, если + этого нельзя избежать, а не одобрение такого шаблона. + +<a id="documentation-conventions-concurrency"></a> + +#### Параллелизм (Concurrency) + +Пользователи Go предполагают, что концептуально доступные только для чтения +операции безопасны для параллельного использования и не требуют дополнительной +синхронизации. + +Дополнительное замечание о параллелизме можно безопасно удалить в этой Godoc: + +```go +// Хорошо: +// Len возвращает количество байт непрочитанной части буфера; +// b.Len() == len(b.Bytes()). +// +// Безопасно для вызова несколькими горутинами одновременно. +func (*Buffer) Len() int +``` + +Однако мутирующие операции не считаются безопасными для параллельного +использования и требуют, чтобы пользователь учитывал синхронизацию. + +Аналогично, дополнительное замечание о параллелизме можно безопасно удалить +здесь: + +```go +// Хорошо: +// Grow увеличивает емкость буфера. +// +// Не безопасно для вызова несколькими горутинами одновременно. +func (*Buffer) Grow(n int) +``` + +Настоятельно рекомендуется документировать, если верно любое из следующего. + +* Непонятно, является ли операция доступной только для чтения или мутирующей: + + ```go + // Хорошо: + package lrucache + + // Lookup возвращает данные, связанные с ключом, из кэша. + // + // Эта операция не безопасна для параллельного использования. + func (*Cache) Lookup(key string) (data []byte, ok bool) + ``` + + Почему? При попадании в кэш (cache hit) при поиске ключа внутреннее + состояние LRU-кэша мутирует. Как это реализовано, может быть неочевидно для + всех читателей. + +* Синхронизация предоставляется API: + + ```go + // Хорошо: + package fortune_go_proto + + // NewFortuneTellerClient возвращает *rpc.Client для сервиса FortuneTeller. + // Безопасно для одновременного использования несколькими горутинами. + func NewFortuneTellerClient(cc *rpc.ClientConn) *FortuneTellerClient + ``` + + Почему? Stubby предоставляет синхронизацию. + + **Примечание:** Если API является типом и API предоставляет синхронизацию в + целом, по соглашению только определение типа документирует семантику. + +* API потребляет пользовательские реализации типов или интерфейсов, и + потребитель интерфейса имеет особые требования к параллелизму: + + ```go + // Хорошо: + package health + + // Watcher сообщает о состоянии здоровья некоторой сущности (обычно серверной службы). + // + // Методы Watcher безопасны для одновременного использования несколькими горутинами. + type Watcher interface { + // Watch отправляет true на переданный канал, когда статус Watcher изменился. + Watch(changed chan<- bool) (unwatch func()) + + // Health возвращает nil, если за которой следят сущность здорова, или + // ненулевую ошибку, объясняющую, почему сущность нездорова. + Health() error + } + ``` + + Почему? Является ли API безопасным для использования несколькими горутинами + — это часть его контракта. + +<a id="documentation-conventions-cleanup"></a> + +#### Очистка (Cleanup) + +Документируйте любые явные требования к очистке, которые есть у API. В противном +случае вызывающие стороны будут использовать API неправильно, что приведет к +утечкам ресурсов и другим возможным ошибкам. + +Указывайте очистки, которые зависят от вызывающей стороны: + +```go +// Хорошо: +// NewTicker возвращает новый Ticker, содержащий канал, который будет отправлять +// текущее время на канал после каждого тика. +// +// Вызовите Stop, чтобы освободить ресурсы, связанные с Ticker, когда закончите. +func NewTicker(d Duration) *Ticker + +func (*Ticker) Stop() +``` + +Если может быть неясно, как очистить ресурсы, объясните, как: + +```go +// Хорошо: +// Get выполняет GET к указанному URL. +// +// Когда err равен nil, resp всегда содержит ненулевой resp.Body. +// Вызывающая сторона должна закрыть resp.Body, когда закончит читать из него. +// +// resp, err := http.Get("http://example.com/") +// if err != nil { +// // обработать ошибку +// } +// defer resp.Body.Close() +// body, err := io.ReadAll(resp.Body) +func (c *Client) Get(url string) (resp *Response, err error) +``` + +См. также: + +* [GoTip #110: Don’t Mix Exit With Defer] + +[GoTip #110: Don’t Mix Exit With Defer]: + https://google.github.io/styleguide/go/index.html#gotip + +<a id="documentation-conventions-errors"></a> + +#### Ошибки (Errors) + +Документируйте значимые сторожевые значения ошибок (error sentinel values) или +типы ошибок, которые ваши функции возвращают вызывающим сторонам, чтобы +вызывающие стороны могли предвидеть, какие типы условий они могут обработать в +своем коде. + +```go +// Хорошо: +package os + +// Read читает до len(b) байт из File и сохраняет их в b. Он возвращает +// количество прочитанных байт и любую встреченную ошибку. +// +// При достижении конца файла Read возвращает 0, io.EOF. +func (*File) Read(b []byte) (n int, err error) { +``` + +Когда функция возвращает определенный тип ошибки, правильно укажите, является ли +ошибка указателем (pointer receiver) или нет: + +```go +// Хорошо: +package os + +type PathError struct { + Op string + Path string + Err error +} + +// Chdir меняет текущий рабочий каталог на указанный каталог. +// +// Если есть ошибка, она будет типа *PathError. +func Chdir(dir string) error { +``` + +Документирование того, являются ли возвращаемые значения указателями, позволяет +вызывающим сторонам правильно сравнивать ошибки с помощью [`errors.Is`], +[`errors.As`] и [`package cmp`]. Это связано с тем, что не указатель +(non-pointer value) не эквивалентен указателю (pointer value). + +**Примечание:** В примере `Chdir` тип возвращаемого значения записан как +`error`, а не `*PathError`, из-за [как работают нулевые значения интерфейса (nil +interface values)](https://go.dev/doc/faq#nil_error). + +Документируйте общие соглашения об ошибках в [документации +пакета](https://neonxp.ru/pages/gostyleguide/google/decisions/#package-comments), когда поведение применимо к большинству +ошибок, встречающихся в пакете: + +```go +// Хорошо: +// Пакет os предоставляет независимый от платформы интерфейс к функциям операционной системы. +// +// Часто доступно больше информации внутри ошибки. Например, если вызов, принимающий имя файла, +// завершается неудачей, такой как Open или Stat, ошибка будет включать имя файла, которое +// не удалось, когда она печатается, и будет иметь тип *PathError, который может быть распакован +// для получения дополнительной информации. +package os +``` + +Вдумчивое применение этих подходов может добавить [дополнительную информацию к +ошибкам](#error-extra-info) без особых усилий и помочь вызывающим сторонам +избежать добавления избыточных аннотаций. + +См. также: + +* [Go Tip #106: Error Naming + Conventions](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #89: When to Use Canonical Status Codes as + Errors](https://google.github.io/styleguide/go/index.html#gotip) + +<a id="documentation-preview"></a> + +### Предварительный просмотр (Preview) + +Go имеет [сервер +документации](https://pkg.go.dev/golang.org/x/pkgsite/cmd/pkgsite). +Рекомендуется предварительно просматривать документацию, которую производит ваш +код, как до, так и во время процесса ревью кода. Это помогает проверить, что +[форматирование godoc] отображается правильно. + +[форматирование godoc]: #godoc-formatting + +<a id="godoc-formatting"></a> + +### Форматирование Godoc (Godoc formatting) + +[Godoc] предоставляет специальный синтаксис для [форматирования документации]. + +* Требуется пустая строка для разделения абзацев: + + ```go + // Хорошо: + // LoadConfig читает конфигурацию из указанного файла. + // + // См. some/shortlink для подробностей о формате файла конфигурации. + ``` + +* Файлы тестов могут содержать [запускаемые примеры (runnable examples)], + которые появляются прикрепленными к соответствующей документации в godoc: + + ```go + // Хорошо: + func ExampleConfig_WriteTo() { + cfg := &Config{ + Name: "example", + } + if err := cfg.WriteTo(os.Stdout); err != nil { + log.Exitf("Failed to write config: %s", err) + } + // Output: + // { + // "name": "example" + // } + } + ``` + +* Отступ строк на два дополнительных пробела форматирует их буквально + (verbatim): + + ```go + // Хорошо: + // Update выполняет функцию в атомарной транзакции. + // + // Обычно это используется с анонимной TransactionFunc: + // + // if err := db.Update(func(state *State) { state.Foo = bar }); err != nil { + // //... + // } + ``` + + Однако обратите внимание, что часто может быть более уместно поместить код в + запускаемый пример, а не включать его в комментарий. + + Это буквальное форматирование может быть использовано для форматирования, не + родного для godoc, такого как списки и таблицы: + + ```go + // Хорошо: + // LoadConfig читает конфигурацию из указанного файла. + // + // LoadConfig обрабатывает следующие ключи особым образом: + // "import" заставит эту конфигурацию наследовать из указанного файла. + // "env" если присутствует, будет заполнен системным окружением. + ``` + +* Одна строка, которая начинается с заглавной буквы, не содержит знаков + препинания, кроме скобок и запятых, и за которой следует другой абзац, + форматируется как заголовок: + + ```go + // Хорошо: + // Следующая строка форматируется как заголовок. + // + // Использование заголовков + // + // Заголовки поставляются с автоматически сгенерированными якорными тегами для удобного связывания. + ``` + +[Godoc]: https://pkg.go.dev/ +[форматирования документации]: https://go.dev/doc/comment +[запускаемые примеры (runnable examples)]: decisions#examples + +<a id="signal-boost"></a> + +### Усиление сигнала (Signal boosting) + +Иногда строка кода выглядит как нечто обычное, но на самом деле это не так. Один +из лучших примеров этого — проверка `err == nil` (поскольку `err != nil` +встречается гораздо чаще). Следующие две условные проверки трудно различить: + +```go +// Хорошо: +if err := doSomething(); err != nil { + // ... +} +``` + +```go +// Плохо: +if err := doSomething(); err == nil { + // ... +} +``` + +Вы можете вместо этого "усилить" сигнал условного оператора, добавив +комментарий: + +```go +// Хорошо: +if err := doSomething(); err == nil { // если ошибки НЕТ + // ... +} +``` + +Комментарий привлекает внимание к различию в условном операторе. + +<a id="vardecls"></a> + +## Объявление переменных (Variable declarations) + +<a id="vardeclinitialization"></a> + +### Инициализация (Initialization) + +Для единообразия предпочитайте `:=` вместо `var` при инициализации новой +переменной ненулевым значением. + +```go +// Хорошо: +i := 42 +``` + +```go +// Плохо: +var i = 42 +``` + +<a id="vardeclzero"></a> + +### Объявление переменных с нулевыми значениями (Declaring variables with zero values) + +Следующие объявления используют [нулевое значение (zero value)]: + +```go +// Хорошо: +var ( + coords Point + magic [4]byte + primes []int +) +``` + +[нулевое значение (zero value)]: https://golang.org/ref/spec#The_zero_value + +Вы должны объявлять значения, используя нулевое значение, когда хотите передать +пустое значение, которое **готово к использованию позже**. Использование +составных литералов (composite literals) с явной инициализацией может быть +громоздким: + +```go +// Плохо: +var ( + coords = Point{X: 0, Y: 0} + magic = [4]byte{0, 0, 0, 0} + primes = []int(nil) +) +``` + +Распространенное применение объявления с нулевым значением — когда переменная +используется как выход при демаршалинге (unmarshalling): + +```go +// Хорошо: +var coords Point +if err := json.Unmarshal(data, &coords); err != nil { +``` + +Также допустимо использовать нулевое значение в следующей форме, когда вам нужна +переменная типа указателя: + +```go +// Хорошо: +msg := new(pb.Bar) // или "&pb.Bar{}" +if err := proto.Unmarshal(data, msg); err != nil { +``` + +Если в вашей структуре нужна блокировка (lock) или другое поле, которое [не +должно копироваться](https://neonxp.ru/pages/gostyleguide/google/decisions/#copying), вы можете сделать его типом значения +(value type), чтобы воспользоваться преимуществами инициализации нулевым +значением. Это означает, что содержащий тип теперь должен передаваться по +указателю, а не по значению. Методы этого типа должны принимать +получатели-указатели (pointer receivers). + +```go +// Хорошо: +type Counter struct { + // Это поле не обязательно должно быть "*sync.Mutex". Однако + // пользователи теперь должны передавать *Counter объекты между собой, а не Counter. + mu sync.Mutex + data map[string]int64 +} + +// Обратите внимание, что это должен быть получатель-указатель, чтобы предотвратить копирование. +func (c *Counter) IncrementBy(name string, n int64) +``` + +Допустимо использовать типы значений для локальных переменных составных типов +(таких как структуры и массивы), даже если они содержат такие некопируемые поля. +Однако, если составной тип возвращается функцией, или если все обращения к нему +в конечном итоге требуют взятия адреса, предпочтительнее объявить переменную как +тип указателя с самого начала. Аналогично, сообщения protobuf должны объявляться +как типы указателей. + +```go +// Хорошо: +func NewCounter(name string) *Counter { + c := new(Counter) // "&Counter{}" тоже подходит. + registerCounter(name, c) + return c +} + +var msg = new(pb.Bar) // или "&pb.Bar{}". +``` + +Это потому, что `*pb.Something` удовлетворяет [`proto.Message`], а +`pb.Something` — нет. + +```go +// Плохо: +func NewCounter(name string) *Counter { + var c Counter + registerCounter(name, &c) + return &c +} + +var msg = pb.Bar{} +``` + +[`proto.Message`]: https://pkg.go.dev/google.golang.org/protobuf/proto#Message + +> **Важно:** Типы map должны быть явно инициализированы перед тем, как их можно +> будет изменять. Однако чтение из map с нулевым значением вполне допустимо. +> +> Для типов map и slice, если код особенно чувствителен к производительности и +> если вы заранее знаете размеры, см. раздел [подсказки по размеру (size +> hints)](#vardeclsize). + +<a id="vardeclcomposite"></a> + +### Составные литералы (Composite literals) + +Следующие объявления являются [составными литералами (composite literal)]: + +```go +// Хорошо: +var ( + coords = Point{X: x, Y: y} + magic = [4]byte{'I', 'W', 'A', 'D'} + primes = []int{2, 3, 5, 7, 11} + captains = map[string]string{"Kirk": "James Tiberius", "Picard": "Jean-Luc"} +) +``` + +Вы должны объявлять значение с помощью составного литерала, когда знаете +начальные элементы или члены. + +В отличие от этого, использование составных литералов для объявления пустых +значений или значений без членов может быть визуально шумным по сравнению с +[инициализацией нулевым значением](#vardeclzero). + +Когда вам нужен указатель на нулевое значение, у вас есть два варианта: пустые +составные литералы и `new`. Оба варианта допустимы, но ключевое слово `new` +может служить напоминанием читателю, что если бы потребовалось ненулевое +значение, составной литерал не сработал бы: + +```go +// Хорошо: +var ( + buf = new(bytes.Buffer) // непустые Buffers инициализируются конструкторами. + msg = new(pb.Message) // непустые proto сообщения инициализируются билдерами или установкой полей по одному. +) +``` + +[составные литералы (composite literal)]: + https://golang.org/ref/spec#Composite_literals + +<a id="vardeclsize"></a> + +### Подсказки по размеру (Size hints) + +Следующие объявления используют подсказки по размеру, чтобы предварительно +выделить емкость: + +```go +// Хорошо: +var ( + // Предпочтительный размер буфера для целевой файловой системы: st_blksize. + buf = make([]byte, 131072) + // Обычно обрабатывается до 8-10 элементов за запуск (16 — безопасное предположение). + q = make([]Node, 0, 16) + // Каждый шард обрабатывает shardSize (обычно 32000+) элементов. + seen = make(map[string]bool, shardSize) +) +``` + +Подсказки по размеру и предварительное выделение — важные шаги **в сочетании с +эмпирическим анализом кода и его интеграций**, для создания производительного и +ресурсоэффективного кода. + +Большинству кода не нужны подсказки по размеру или предварительное выделение, и +он может позволить среде выполнения увеличивать срез или карту по мере +необходимости. Допустимо предварительно выделять память, когда окончательный +размер известен (например, при преобразовании между map и срезом), но это не +является требованием читаемости и может не стоить загромождения в простых +случаях. + +**Предупреждение:** Предварительное выделение больше памяти, чем нужно, может +тратить память в парке (fleet) или даже вредить производительности. В случае +сомнений см. [GoTip #3: Benchmarking Go Code] и по умолчанию используйте +[инициализацию нулевым значением](#vardeclzero) или [объявление составным +литералом](#vardeclcomposite). + +[GoTip #3: Benchmarking Go Code]: + https://google.github.io/styleguide/go/index.html#gotip + +<a id="decl-chan"></a> + +### Направление каналов (Channel direction) + +Указывайте [направление канала (channel direction)] там, где это возможно. + +```go +// Хорошо: +// sum вычисляет сумму всех значений. Она читает из канала до тех пор, +// пока канал не закроется. +func sum(values <-chan int) int { + // ... +} +``` + +Это предотвращает случайные ошибки программирования, которые возможны без +спецификации: + +```go +// Плохо: +func sum(values chan int) (out int) { + for v := range values { + out += v + } + // values уже должен быть закрыт для достижения этого кода, что означает, + // что второе закрытие вызовет панику. + close(values) +} +``` + +Когда направление указано, компилятор перехватывает простые ошибки, подобные +этой. Это также помогает передать меру владения (ownership) типу. + +См. также доклад Брайана Миллса "Rethinking Classical Concurrency Patterns": +[слайды][rethinking-concurrency-slides] [видео][rethinking-concurrency-video]. + +[rethinking-concurrency-slides]: + https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view?usp=sharing +[rethinking-concurrency-video]: https://www.youtube.com/watch?v=5zXAHh5tJqQ +[направление канала (channel direction)]: https://go.dev/ref/spec#Channel_types + +<a id="funcargs"></a> + +## Списки аргументов функций (Function argument lists) + +Не позволяйте сигнатуре функции становиться слишком длинной. По мере добавления +большего количества параметров в функцию роль отдельных параметров становится +менее ясной, а соседние параметры одного типа становится легче спутать. Функции +с большим количеством аргументов менее запоминаемы и их труднее читать в месте +вызова. + +При проектировании API рассмотрите возможность разделения высоконастраиваемой +функции, сигнатура которой становится сложной, на несколько более простых. Они +могут использовать общую (неэкспортируемую) реализацию, если это необходимо. + +Если функции требуется много входных данных, рассмотрите возможность введения +[структуры опций (option struct)] для некоторых аргументов или использование +более продвинутой техники [вариативных опций (variadic options)]. Основным +критерием выбора стратегии должно быть то, как выглядит вызов функции во всех +ожидаемых случаях использования. + +Приведенные ниже рекомендации в первую очередь применяются к экспортированным +API, к которым предъявляются более высокие стандарты, чем к неэкспортированным. +Эти методы могут быть не нужны для вашего случая использования. Используйте свое +суждение и балансируйте между принципами [ясности (clarity)] и [наименьшей +механизации (least mechanism)]. + +См. также: [Go Tip #24: Use Case-Specific +Constructions](https://google.github.io/styleguide/go/index.html#gotip) + +[структуры опций (option struct)]: #option-structure +[вариативных опций (variadic options)]: #variadic-options +[ясности (clarity)]: guide#clarity +[наименьшей механизации (least mechanism)]: guide#least-mechanism + +<a id="option-structure"></a> + +### Структура опций (Option structure) + +Структура опций (option structure) — это тип struct, который собирает некоторые +или все аргументы функции или метода, а затем передается в качестве последнего +аргумента функции или методу. (Структура должна быть экспортирована только если +она используется в экспортированной функции.) + +Использование структуры опций имеет ряд преимуществ: + +* Литерал структуры включает как поля, так и значения для каждого аргумента, + что делает их самодокументированными и затрудняет их перестановку. +* Несущественные или "значения по умолчанию" поля могут быть опущены. +* Вызывающие стороны могут совместно использовать структуру опций и писать + вспомогательные функции для работы с ней. +* Структуры обеспечивают более чистую документацию для каждого поля, чем + аргументы функций. +* Структуры опций могут расти со временем без влияния на места вызова. + +Вот пример функции, которую можно улучшить: + +```go +// Плохо: +func EnableReplication(ctx context.Context, config *replicator.Config, primaryRegions, readonlyRegions []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) { + // ... +} +``` + +Функция выше может быть переписана со структурой опций следующим образом: + +```go +// Хорошо: +type ReplicationOptions struct { + Config *replicator.Config + PrimaryRegions []string + ReadonlyRegions []string + ReplicateExisting bool + OverwritePolicies bool + ReplicationInterval time.Duration + CopyWorkers int + HealthWatcher health.Watcher +} + +func EnableReplication(ctx context.Context, opts ReplicationOptions) { + // ... +} +``` + +Затем функцию можно вызвать в другом пакете: + +```go +// Хорошо: +func foo(ctx context.Context) { + // Сложный вызов: + storage.EnableReplication(ctx, storage.ReplicationOptions{ + Config: config, + PrimaryRegions: []string{"us-east1", "us-central2", "us-west3"}, + ReadonlyRegions: []string{"us-east5", "us-central6"}, + OverwritePolicies: true, + ReplicationInterval: 1 * time.Hour, + CopyWorkers: 100, + HealthWatcher: watcher, + }) + + // Простой вызов: + storage.EnableReplication(ctx, storage.ReplicationOptions{ + Config: config, + PrimaryRegions: []string{"us-east1", "us-central2", "us-west3"}, + }) +} +``` + +**Примечание:** [Контексты никогда не включаются в структуры +опций](https://neonxp.ru/pages/gostyleguide/google/decisions/#contexts). + +Этот вариант часто предпочтителен, когда применимо одно из следующих условий: + +* Все вызывающие стороны должны указать одну или несколько опций. +* Большому количеству вызывающих сторон необходимо предоставить множество + опций. +* Опции используются совместно несколькими функциями, которые будет вызывать + пользователь. + +<a id="variadic-options"></a> + +### Вариативные опции (Variadic options) + +Используя вариативные опции, создаются экспортированные функции, которые +возвращают замыкания (closures), которые могут быть переданы в [вариативный +(`...`) параметр] функции. Функция принимает в качестве параметров значения +опции (если есть), а возвращаемое замыкание принимает изменяемую ссылку (обычно +указатель на тип struct), которая будет обновлена на основе входных данных. + +[вариативный (`...`) параметр]: + https://golang.org/ref/spec#Passing_arguments_to_..._parameters + +Использование вариативных опций может предоставить ряд преимуществ: + +* Опции не занимают места в месте вызова, когда конфигурация не нужна. +* Опции все еще являются значениями, поэтому вызывающие стороны могут делиться + ими, писать вспомогательные функции и накапливать их. +* Опции могут принимать несколько параметров (например, + `cartesian.Translate(dx, dy int) TransformOption`). +* Функции опций могут возвращать именованный тип, чтобы группировать опции + вместе в godoc. +* Пакеты могут разрешать (или запрещать) сторонним пакетам определять (или + запрещать определение) свои собственные опции. + +**Примечание:** Использование вариативных опций требует значительного количества +дополнительного кода (см. следующий пример), поэтому их следует использовать +только тогда, когда преимущества перевешивают накладные расходы. + +Вот пример функции, которую можно улучшить: + +```go +// Плохо: +func EnableReplication(ctx context.Context, config *placer.Config, primaryCells, readonlyCells []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) { + ... +} +``` + +Пример выше может быть переписан с вариативными опциями следующим образом: + +```go +// Хорошо: +type replicationOptions struct { + readonlyCells []string + replicateExisting bool + overwritePolicies bool + replicationInterval time.Duration + copyWorkers int + healthWatcher health.Watcher +} + +// ReplicationOption настраивает EnableReplication. +type ReplicationOption func(*replicationOptions) + +// ReadonlyCells добавляет дополнительные ячейки, которые дополнительно +// должны содержать реплики только для чтения данных. +// +// Передача этой опции несколько раз добавит дополнительные +// ячейки только для чтения. +// +// По умолчанию: нет +func ReadonlyCells(cells ...string) ReplicationOption { + return func(opts *replicationOptions) { + opts.readonlyCells = append(opts.readonlyCells, cells...) + } +} + +// ReplicateExisting контролирует, будут ли файлы, уже существующие в +// первичных ячейках, реплицированы. В противном случае только недавно добавленные +// файлы будут кандидатами на репликацию. +// +// Повторная передача этой опции перезапишет предыдущие значения. +// +// По умолчанию: false +func ReplicateExisting(enabled bool) ReplicationOption { + return func(opts *replicationOptions) { + opts.replicateExisting = enabled + } +} + +// ... другие опции ... + +// DefaultReplicationOptions управляют значениями по умолчанию перед +// применением опций, переданных в EnableReplication. +var DefaultReplicationOptions = []ReplicationOption{ + OverwritePolicies(true), + ReplicationInterval(12 * time.Hour), + CopyWorkers(10), +} + +func EnableReplication(ctx context.Context, config *placer.Config, primaryCells []string, opts ...ReplicationOption) { + var options replicationOptions + for _, opt := range DefaultReplicationOptions { + opt(&options) + } + for _, opt := range opts { + opt(&options) + } +} +``` + +Затем функцию можно вызвать в другом пакете: + +```go +// Хорошо: +func foo(ctx context.Context) { + // Сложный вызов: + storage.EnableReplication(ctx, config, []string{"po", "is", "ea"}, + storage.ReadonlyCells("ix", "gg"), + storage.OverwritePolicies(true), + storage.ReplicationInterval(1*time.Hour), + storage.CopyWorkers(100), + storage.HealthWatcher(watcher), + ) + + // Простой вызов: + storage.EnableReplication(ctx, config, []string{"po", "is", "ea"}) +} +``` + +Предпочитайте этот вариант, когда применимо большинство из следующего: + +* Большинству вызывающих сторон не нужно указывать никакие опции. +* Большинство опций используется редко. +* Существует большое количество опций. +* Опции требуют аргументов. +* Опции могут завершиться неудачей или быть установлены неправильно (в этом + случае функция опции возвращает `error`). +* Опции требуют большого количества документации, которую трудно уместить в + структуре. +* Пользователи или другие пакеты могут предоставлять пользовательские опции. + +Опции в этом стиле должны принимать параметры, а не использовать наличие +(presence) для сигнализации своего значения; последнее может значительно +усложнить динамическое составление аргументов. Например, двоичные настройки +должны принимать логическое значение (например, `rpc.FailFast(enable bool)` +предпочтительнее, чем `rpc.EnableFailFast()`). Перечисляемая опция должна +принимать перечисляемую константу (например, `log.Format(log.Capacitor)` +предпочтительнее, чем `log.CapacitorFormat()`). Альтернатива значительно +усложняет жизнь пользователям, которые должны программно выбирать, какие опции +передавать; такие пользователи вынуждены изменять фактический состав параметров, +а не просто изменять аргументы опций. Не предполагайте, что все пользователи +будут статически знать полный набор опций. + +Как правило, опции должны обрабатываться по порядку. Если возникает конфликт или +если некумулятивная опция передается несколько раз, должен побеждать последний +аргумент. + +Параметр функции опции в этом шаблоне обычно не экспортируется, чтобы ограничить +определение опций только самим пакетом. Это хороший вариант по умолчанию, хотя +могут быть случаи, когда уместно позволить другим пакетам определять опции. + +См. [оригинальный пост в блоге Роба Пайка] и [доклад Дейва Ченея] для более +глубокого изучения того, как эти опции могут быть использованы. + +[оригинальный пост в блоге Роба Пайка]: + http://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html +[доклад Дейва Ченея]: + https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis + +<a id="complex-clis"></a> + +## Сложные интерфейсы командной строки (Complex command-line interfaces) + +Некоторые программы хотят предоставить пользователям богатый интерфейс командной +строки, включающий подкоманды. Например, `kubectl create`, `kubectl run` и +многие другие подкоманды предоставляются программой `kubectl`. Существует по +крайней мере следующие общеупотребительные библиотеки для достижения этого. + +Если у вас нет предпочтений или другие соображения равны, рекомендуется +[subcommands], поскольку она самая простая и с ней легко работать правильно. +Однако, если вам нужны другие функции, которые она не предоставляет, выберите +один из других вариантов. + +* **[cobra]** + + * Соглашение о флагах: getopt + * Распространена за пределами кодовой базы Google. + * Много дополнительных функций. + * Подводные камни в использовании (см. ниже). + +* **[subcommands]** + + * Соглашение о флагах: Go + * Проста и с ней легко работать правильно. + * Рекомендуется, если вам не нужны дополнительные функции. + +**Предупреждение**: функции команд cobra должны использовать `cmd.Context()` для +получения контекста, а не создавать свой собственный корневой контекст с помощью +`context.Background`. Код, использующий пакет subcommands, уже получает +правильный контекст как параметр функции. + +Вы не обязаны помещать каждую подкоманду в отдельный пакет, и часто в этом нет +необходимости. Применяйте те же соображения о границах пакетов, что и в любой +кодовой базе Go. Если ваш код может использоваться как библиотека и как бинарный +файл, обычно полезно отделить CLI-код от библиотеки, делая CLI просто еще одним +из ее клиентов. (Это не специфично для CLI с подкомандами, но упоминается здесь, +потому что это частое место, где это возникает.) + +[subcommands]: https://pkg.go.dev/github.com/google/subcommands +[cobra]: https://pkg.go.dev/github.com/spf13/cobra + +<a id="tests"></a> + +## Тесты (Tests) + +<a id="test-functions"></a> + +### Оставляйте тестирование функции `Test` + +<!-- Примечание для сопровождающих: Этот раздел пересекается с decisions#assert и +decisions#mark-test-helpers. Цель не в том, чтобы повторять информацию, а +в том, чтобы иметь одно место, которое суммирует различие, о котором часто +задумываются новички в языке. --> + +Go различает "тестовые помощники (test helpers)" и "помощники утверждений +(assertion helpers)": + +* **Тестовые помощники** — это функции, которые выполняют задачи настройки или + очистки. Все сбои, которые происходят в тестовых помощниках, ожидаемо + являются сбоями окружения (а не тестируемого кода) — например, когда + тестовая база данных не может быть запущена, потому что на этой машине + больше нет свободных портов. Для таких функций часто уместно вызывать + `t.Helper`, чтобы [пометить их как тестовый помощник]. См. [обработку ошибок + в тестовых помощниках] для более подробной информации. + +* **Помощники утверждений** — это функции, которые проверяют правильность + системы и завершают тест с ошибкой, если ожидание не выполняется. Помощники + утверждений [не считаются идиоматичными] в Go. + +Цель теста — сообщить о условиях прохождения/непрохождения тестируемого кода. +Идеальное место для завершения теста с ошибкой — внутри самой функции `Test`, +так как это обеспечивает ясность [сообщений об ошибках] и логики теста. + +[пометить их как тестовый помощник]: decisions#mark-test-helpers +[обработку ошибок в тестовых помощниках]: #test-helper-error-handling +[не считаются идиоматичными]: decisions#assert +[сообщений об ошибках]: decisions#useful-test-failures + +По мере роста вашего тестового кода может стать необходимым вынести некоторую +функциональность в отдельные функции. Стандартные соображения программной +инженерии все еще применяются, поскольку *тестовый код — это все еще код*. Если +функциональность не взаимодействует с тестовым фреймворком, то применяются все +обычные правила. Однако, когда общий код взаимодействует с фреймворком, +необходимо соблюдать осторожность, чтобы избежать распространенных подводных +камней, которые могут привести к неинформативным сообщениям об ошибках и +неудобным в поддержке тестам. + +Если многим отдельным тестовым случаям требуется одна и та же логика валидации, +организуйте тест одним из следующих способов вместо использования помощников +утверждений или сложных функций валидации: + +* Встройте логику (и валидацию, и завершение с ошибкой) в функцию `Test`, даже + если это повторяется. Это лучше всего работает в простых случаях. +* Если входные данные похожи, рассмотрите возможность объединения их в + [табличный тест (table-driven test)], сохраняя логику встроенной в цикл. Это + помогает избежать повторения, сохраняя валидацию и завершение с ошибкой в + `Test`. +* Если есть несколько вызывающих сторон, которым нужна одна и та же функция + валидации, но табличные тесты не подходят (обычно потому, что входные данные + недостаточно просты или валидация требуется как часть последовательности + операций), организуйте функцию валидации так, чтобы она возвращала значение + (обычно `error`), а не принимала параметр `testing.T` и использовала его для + завершения теста с ошибкой. Используйте логику внутри `Test`, чтобы решить, + завершать ли тест с ошибкой, и предоставить [полезные сообщения об ошибках + теста]. Вы также можете создать тестовые помощники для выноса общего + шаблонного кода настройки. + +Дизайн, описанный в последнем пункте, сохраняет ортогональность. Например, +[пакет `cmp`] не предназначен для завершения тестов с ошибкой, а для сравнения +(и вычисления различий) значений. Поэтому ему не нужно знать о контексте, в +котором было сделано сравнение, поскольку вызывающая сторона может предоставить +его. Если ваш общий тестовый код предоставляет `cmp.Transformer` для вашего типа +данных, это часто может быть самым простым дизайном. Для других проверок +рассмотрите возможность возврата значения `error`. + +```go +// Хорошо: +// polygonCmp возвращает cmp.Option, которое приравнивает объекты геометрии s2 +// с некоторой небольшой ошибкой с плавающей точкой. +func polygonCmp() cmp.Option { + return cmp.Options{ + cmp.Transformer("polygon", func(p *s2.Polygon) []*s2.Loop { return p.Loops() }), + cmp.Transformer("loop", func(l *s2.Loop) []s2.Point { return l.Vertices() }), + cmpopts.EquateApprox(0.00000001, 0), + cmpopts.EquateEmpty(), + } +} + +func TestFenceposts(t *testing.T) { + // Это тест для вымышленной функции Fenceposts, которая рисует забор + // вокруг некоторого объекта Place. Детали не важны, за исключением того, + // что результат — это некоторый объект, имеющий геометрию s2 (github.com/golang/geo/s2) + got := Fencepost(tomsDiner, 1*meter) + if diff := cmp.Diff(want, got, polygonCmp()); diff != "" { + t.Errorf("Fencepost(tomsDiner, 1m) returned unexpected diff (-want+got):\n%v", diff) + } +} + +func FuzzFencepost(f *testing.F) { + // Фаззинг-тест (https://go.dev/doc/fuzz) для того же. + + f.Add(tomsDiner, 1*meter) + f.Add(school, 3*meter) + + f.Fuzz(func(t *testing.T, geo Place, padding Length) { + got := Fencepost(geo, padding) + // Простая эталонная реализация: не используется в prod, но проста для + // понимания и поэтому полезна для проверки в случайных тестах. + reference := slowFencepost(geo, padding) + + // Во фаззинг-тесте входные и выходные данные могут быть большими, поэтому + // не беспокойтесь о печати diff. cmp.Equal достаточно. + if !cmp.Equal(got, reference, polygonCmp()) { + t.Errorf("Fencepost returned wrong placement") + } + }) +} +``` + +Функция `polygonCmp` агностична относительно того, как ее вызывают; она не +принимает конкретный тип входных данных и не контролирует, что делать, если два +объекта не совпадают. Поэтому больше вызывающих сторон могут использовать ее. + +**Примечание:** Существует аналогия между тестовыми помощниками и обычным +библиотечным кодом. Код в библиотеках обычно [не должен вызывать panic] за +редкими исключениями; код, вызываемый из теста, не должен останавливать тест, +если нет [смысла продолжать]. + +[табличный тест (table-driven test)]: decisions#table-driven-tests +[полезные сообщения об ошибках теста]: decisions#useful-test-failures +[пакет `cmp`]: https://pkg.go.dev/github.com/google/go-cmp/cmp +[не должен вызывать panic]: decisions#dont-panic +[смысла продолжать]: #t-fatal + +<a id="test-validation-apis"></a> + +### Проектирование расширяемых API валидации (Designing extensible validation APIs) + +Большая часть советов о тестировании в руководстве по стилю касается +тестирования вашего собственного кода. Этот раздел о том, как предоставить +средства для других людей тестировать код, который они пишут, чтобы убедиться, +что он соответствует требованиям вашей библиотеки. + +<a id="test-validation-apis-what"></a> + +#### Приемочное тестирование (Acceptance testing) + +Такое тестирование называется [приемочным тестированием (acceptance testing)]. +Предпосылка такого тестирования заключается в том, что человек, использующий +тест, не знает всех деталей того, что происходит в тесте; он просто передает +входные данные в тестовое средство, чтобы оно выполнило работу. Это можно +рассматривать как форму [инверсии управления (inversion of control)]. + +В типичном тесте Go тестовая функция контролирует поток программы, и +рекомендации [без утверждений (no assert)](https://neonxp.ru/pages/gostyleguide/google/decisions/#assert) и [тестовые +функции](#test-functions) побуждают вас сохранять это так. Этот раздел +объясняет, как создавать поддержку для таких тестов способом, согласующимся со +стилем Go. + +Прежде чем углубляться в "как", рассмотрим пример из [`io/fs`], приведенный +ниже: + +```go +type FS interface { + Open(name string) (File, error) +} +``` + +Хотя существуют хорошо известные реализации `fs.FS`, от разработчика Go может +потребоваться создать свою. Чтобы помочь проверить правильность пользовательской +реализации `fs.FS`, была предоставлена универсальная библиотека в +[`testing/fstest`] под названием [`fstest.TestFS`]. Этот API рассматривает +реализацию как черный ящик (blackbox), чтобы убедиться, что она соблюдает самые +основные части контракта `io/fs`. + +[приемочным тестированием (acceptance testing)]: + https://en.wikipedia.org/wiki/Acceptance_testing +[инверсии управления (inversion of control)]: + https://en.wikipedia.org/wiki/Inversion_of_control +[`io/fs`]: https://pkg.go.dev/io/fs +[`testing/fstest`]: https://pkg.go.dev/testing/fstest +[`fstest.TestFS`]: https://pkg.go.dev/testing/fstest#TestFS + +<a id="test-validation-apis-writing"></a> + +#### Написание приемочного теста (Writing an acceptance test) + +Теперь, когда мы знаем, что такое приемочный тест и почему вы можете его +использовать, давайте рассмотрим создание приемочного теста для `package chess`, +пакета, используемого для симуляции шахматных игр. Пользователи `chess` должны +реализовать интерфейс `chess.Player`. Эти реализации — основное, что мы будем +проверять. Наш приемочный тест касается того, делает ли реализация игрока +легальные ходы, а не того, являются ли ходы умными. + +1. Создайте новый пакет для поведения валидации, [обычно + именуемый](#naming-doubles-helper-package) добавлением слова `test` к имени + пакета (например, `chesstest`). + +1. Создайте функцию, которая выполняет валидацию, принимая тестируемую + реализацию в качестве аргумента и проверяя ее: + + ```go + // ExercisePlayer тестирует реализацию Player за один ход на доске. + // Сама доска выборочно проверяется на разумность и правильность. + // + // Возвращает nil ошибку, если игрок делает правильный ход в контексте + // предоставленной доски. В противном случае ExercisePlayer возвращает одну из + // ошибок этого пакета, чтобы указать, как и почему игрок не прошел валидацию. + func ExercisePlayer(b *chess.Board, p chess.Player) error + ``` + + Тест должен отмечать, какие инварианты нарушены и как. Ваш дизайн может + выбрать одну из двух дисциплин для сообщения о сбоях: + + * **Завершение при первой ошибке (Fail fast)**: возвращать ошибку, как + только реализация нарушает инвариант. + + Это самый простой подход, и он хорошо работает, если ожидается, что + приемочный тест будет выполняться быстро. Простые [сторожевые ошибки + (sentinels)] и [пользовательские типы] могут быть легко использованы + здесь, что, в свою очередь, облегчает тестирование самого приемочного + теста. + + ```go + for color, army := range b.Armies { + // Король никогда не должен покидать доску, потому что игра заканчивается + // матом. + if army.King == nil { + return &MissingPieceError{Color: color, Piece: chess.King} + } + } + ``` + + * **Агрегация всех сбоев (Aggregate all failures)**: собирать все сбои и + сообщать о них всех. + + Этот подход напоминает рекомендацию [продолжать выполнение (keep going)] + и может быть предпочтительнее, если ожидается, что приемочный тест будет + выполняться медленно. + + То, как вы агрегируете сбои, должно определяться тем, хотите ли вы дать + пользователям или себе возможность исследовать отдельные сбои (например, + для тестирования вашего приемочного теста). Ниже демонстрируется + использование [пользовательского типа ошибки][пользовательские типы], + который [агрегирует ошибки]: + + ```go + var badMoves []error + + move := p.Move() + if putsOwnKingIntoCheck(b, move) { + badMoves = append(badMoves, PutsSelfIntoCheckError{Move: move}) + } + + if len(badMoves) > 0 { + return SimulationError{BadMoves: badMoves} + } + return nil + ``` + +Приемочный тест должен соблюдать рекомендацию [продолжать выполнение (keep +going)], не вызывая `t.Fatal`, если тест не обнаруживает нарушение инварианта в +системе, которая тестируется. + +Например, `t.Fatal` должен быть зарезервирован для исключительных случаев, таких +как [сбой настройки](#test-helper-error-handling), как обычно: + +```go +func ExerciseGame(t *testing.T, cfg *Config, p chess.Player) error { + t.Helper() + + if cfg.Simulation == Modem { + conn, err := modempool.Allocate() + if err != nil { + t.Fatalf("No modem for the opponent could be provisioned: %v", err) + } + t.Cleanup(func() { modempool.Return(conn) }) + } + // Запустить приемочный тест (целую игру). +} +``` + +Эта техника может помочь вам создавать лаконичные, каноничные проверки. Но не +пытайтесь использовать ее, чтобы обойти [рекомендации об +утверждениях](https://neonxp.ru/pages/gostyleguide/google/decisions/#assert). + +Конечный продукт должен быть похож на этот для конечных пользователей: + +```go +// Хорошо: +package deepblue_test + +import ( + "chesstest" + "deepblue" +) + +func TestAcceptance(t *testing.T) { + player := deepblue.New() + err := chesstest.ExerciseGame(t, chesstest.SimpleGame, player) + if err != nil { + t.Errorf("Deep Blue player failed acceptance test: %v", err) + } +} +``` + +[сторожевые ошибки (sentinels)]: + https://google.github.io/styleguide/go/index.html#gotip +[пользовательские типы]: https://google.github.io/styleguide/go/index.html#gotip +[агрегирует ошибки]: https://google.github.io/styleguide/go/index.html#gotip + +<a id="use-real-transports"></a> + +### Используйте реальные транспорты (Use real transports) + +При тестировании интеграции компонентов, особенно когда HTTP или RPC +используются в качестве базового транспорта между компонентами, предпочитайте +использовать реальный базовый транспорт для подключения к тестовой версии +бэкенда. + +Например, предположим, что код, который вы хотите протестировать (иногда +называемый "системой под тестом" или SUT), взаимодействует с бэкендом, +реализующим API [долго выполняющихся операций (long running operations)]. Чтобы +протестировать ваш SUT, используйте реальный [OperationsClient], подключенный к +[тестовому двойнику (test +double)](https://abseil.io/resources/swe-book/html/ch13.html#basic_concepts) +(например, моку, заглушке или фейку) [OperationsServer]. + +[тестовому двойнику (test double)]: + https://abseil.io/resources/swe-book/html/ch13.html#basic_concepts +[долго выполняющихся операций (long running operations)]: + https://pkg.go.dev/google.golang.org/genproto/googleapis/longrunning +[OperationsClient]: + https://pkg.go.dev/google.golang.org/genproto/googleapis/longrunning#OperationsClient +[OperationsServer]: + https://pkg.go.dev/google.golang.org/genproto/googleapis/longrunning#OperationsServer + +Это рекомендуется вместо ручной реализации клиента из-за сложности правильной +имитации поведения клиента. Используя production-клиент с тестовым сервером, вы +гарантируете, что ваш тест использует как можно больше реального кода. + +**Совет:** По возможности используйте тестовую библиотеку, предоставленную +авторами тестируемого сервиса. + +<a id="t-fatal"></a> + +### `t.Error` против `t.Fatal` + +Как обсуждалось в [решениях](https://neonxp.ru/pages/gostyleguide/google/decisions/#keep-going), тесты, как правило, не +должны прерываться при первой встреченной проблеме. + +Однако некоторые ситуации требуют, чтобы тест не продолжался. Вызов `t.Fatal` +уместен, когда какая-то часть настройки теста завершается неудачей, особенно во +[вспомогательных функциях настройки теста], без которых вы не можете запустить +остальную часть теста. В табличном тесте `t.Fatal` уместен для сбоев, которые +настраивают всю тестовую функцию до начала цикла теста. Сбои, которые +затрагивают одну запись в таблице теста и делают невозможным продолжение работы +с этой записью, должны сообщаться следующим образом: + +* Если вы не используете подтесты `t.Run`, используйте `t.Error`, за которым + следует оператор `continue` для перехода к следующей записи таблицы. +* Если вы используете подтесты (и вы внутри вызова `t.Run`), используйте + `t.Fatal`, который завершает текущий подтест и позволяет вашему тестовому + случаю перейти к следующему подтесту. + +**Предупреждение:** Не всегда безопасно вызывать `t.Fatal` и подобные функции. +[Подробнее здесь](#t-fatal-goroutine). + +[вспомогательных функциях настройки теста]: #test-helper-error-handling + +<a id="test-helper-error-handling"></a> + +### Обработка ошибок во вспомогательных тестовых функциях (Error handling in test helpers) + +**Примечание:** В этом разделе обсуждаются [тестовые помощники (test helpers)] в +том смысле, в котором Go использует этот термин: функции, которые выполняют +настройку и очистку теста, а не общие средства утверждений. См. раздел [тестовые +функции](#test-functions) для более подробного обсуждения. + +[тестовые помощники (test helpers)]: decisions#mark-test-helpers + +Операции, выполняемые тестовым помощником, иногда завершаются неудачей. +Например, настройка каталога с файлами включает ввод-вывод, который может +завершиться неудачей. Когда тестовые помощники завершаются неудачей, их сбой +часто означает, что тест не может продолжиться, поскольку не выполнилось +предварительное условие настройки. Когда это происходит, предпочтительнее +вызвать одну из функций `Fatal` в помощнике: + +```go +// Хорошо: +func mustAddGameAssets(t *testing.T, dir string) { + t.Helper() + if err := os.WriteFile(path.Join(dir, "pak0.pak"), pak0, 0644); err != nil { + t.Fatalf("Setup failed: could not write pak0 asset: %v", err) + } + if err := os.WriteFile(path.Join(dir, "pak1.pak"), pak1, 0644); err != nil { + t.Fatalf("Setup failed: could not write pak1 asset: %v", err) + } +} +``` + +Это делает вызывающую сторону чище, чем если бы помощник возвращал ошибку самому +тесту: + +```go +// Плохо: +func addGameAssets(t *testing.T, dir string) error { + t.Helper() + if err := os.WriteFile(path.Join(d, "pak0.pak"), pak0, 0644); err != nil { + return err + } + if err := os.WriteFile(path.Join(d, "pak1.pak"), pak1, 0644); err != nil { + return err + } + return nil +} +``` + +**Предупреждение:** Не всегда безопасно вызывать `t.Fatal` и подобные функции. +[Подробнее](#t-fatal-goroutine) здесь. + +Сообщение об ошибке должно включать описание того, что произошло. Это важно, так +как вы можете предоставлять тестовый API многим пользователям, особенно с +увеличением количества шагов, производящих ошибки, в помощнике. Когда тест +завершается неудачей, пользователь должен знать, где и почему. + +**Совет:** Go 1.14 представила функцию [`t.Cleanup`], которую можно использовать +для регистрации функций очистки, которые запускаются при завершении вашего +теста. Функция также работает с тестовыми помощниками. См. [GoTip #4: Cleaning +Up Your Tests](https://google.github.io/styleguide/go/index.html#gotip) для +рекомендаций по упрощению тестовых помощников. + +Сниппет ниже в вымышленном файле `paint_test.go` демонстрирует, как +`(*testing.T).Helper` влияет на сообщение об ошибке в тесте Go: + +```go +package paint_test + +import ( + "fmt" + "testing" +) + +func paint(color string) error { + return fmt.Errorf("no %q paint today", color) +} + +func badSetup(t *testing.T) { + // Здесь должен быть вызов t.Helper, но его нет. + if err := paint("taupe"); err != nil { + t.Fatalf("Could not paint the house under test: %v", err) // строка 15 + } +} + +func goodSetup(t *testing.T) { + t.Helper() + if err := paint("lilac"); err != nil { + t.Fatalf("Could not paint the house under test: %v", err) + } +} + +func TestBad(t *testing.T) { + badSetup(t) + // ... +} + +func TestGood(t *testing.T) { + goodSetup(t) // строка 32 + // ... +} +``` + +Вот пример вывода при запуске. Обратите внимание на выделенный текст и на то, +как он отличается: + +```text +=== RUN TestBad + paint_test.go:15: Could not paint the house under test: no "taupe" paint today +--- FAIL: TestBad (0.00s) +=== RUN TestGood + paint_test.go:32: Could not paint the house under test: no "lilac" paint today +--- FAIL: TestGood (0.00s) +FAIL +``` + +Ошибка с `paint_test.go:15` относится к строке функции настройки, которая +завершилась неудачей в `badSetup`: + +`t.Fatalf("Could not paint the house under test: %v", err)` + +Тогда как `paint_test.go:32` относится к строке теста, которая завершилась +неудачей в `TestGood`: + +`goodSetup(t)` + +Правильное использование `(*testing.T).Helper` гораздо лучше определяет +местоположение сбоя, когда: + +* вспомогательные функции растут +* вспомогательные функции вызывают другие вспомогательные функции +* количество использований вспомогательных функций в тестовых функциях растет + +**Совет:** Если вспомогательная функция вызывает `(*testing.T).Error` или +`(*testing.T).Fatal`, предоставьте некоторый контекст в строке формата, чтобы +помочь определить, что пошло не так и почему. + +**Совет:** Если ничто из того, что делает помощник, не может привести к неудаче +теста, ему не нужно вызывать `t.Helper`. Упростите его сигнатуру, удалив `t` из +списка параметров функции. + +[`t.Cleanup`]: https://pkg.go.dev/testing#T.Cleanup + +<a id="t-fatal-goroutine"></a> + +### Не вызывайте `t.Fatal` из отдельных горутин (Don't call `t.Fatal` from separate goroutines) + +Как [документировано в пакете testing](https://pkg.go.dev/testing#T), +неправильно вызывать `t.FailNow`, `t.Fatal` и т.д. из любой горутины, кроме той, +которая запускает функцию Test (или подтест). Если ваш тест запускает новые +горутины, они не должны вызывать эти функции внутри этих горутин. + +[Тестовые помощники](#test-functions) обычно не сигнализируют о сбое из новых +горутин, поэтому для них допустимо использовать `t.Fatal`. В случае сомнений +вызовите `t.Error` и вернитесь. + +```go +// Хорошо: +func TestRevEngine(t *testing.T) { + engine, err := Start() + if err != nil { + t.Fatalf("Engine failed to start: %v", err) + } + + num := 11 + var wg sync.WaitGroup + wg.Add(num) + for i := 0; i < num; i++ { + go func() { + defer wg.Done() + if err := engine.Vroom(); err != nil { + // Здесь нельзя использовать t.Fatalf. + t.Errorf("No vroom left on engine: %v", err) + return + } + if rpm := engine.Tachometer(); rpm > 1e6 { + t.Errorf("Inconceivable engine rate: %d", rpm) + } + }() + } + wg.Wait() + + if seen := engine.NumVrooms(); seen != num { + t.Errorf("engine.NumVrooms() = %d, want %d", seen, num) + } +} +``` + +Добавление `t.Parallel` к тесту или подтесту не делает небезопасным вызов +`t.Fatal`. + +Когда все вызовы API `testing` находятся в [тестовой функции](#test-functions), +обычно легко заметить неправильное использование, потому что ключевое слово `go` +легко увидеть. Передача аргументов `testing.T` усложняет отслеживание такого +использования. Обычно причина передачи этих аргументов — введение тестового +помощника, и они не должны зависеть от тестируемой системы. Поэтому, если +тестовый помощник [регистрирует фатальную ошибку +теста](#test-helper-error-handling), он может и должен делать это из горутины +теста. + +<a id="t-field-names"></a> + +### Используйте имена полей в литералах структур (Use field names in struct literals) + +<a id="t-field-labels"></a> + +В табличных тестах предпочитайте указывать имена полей при инициализации +литералов структур тестовых случаев. Это полезно, когда тестовые случаи +охватывают большое вертикальное пространство (например, более 20-30 строк), +когда есть соседние поля с одинаковым типом, а также когда вы хотите опустить +поля, имеющие нулевое значение. Например: + +```go +// Хорошо: +func TestStrJoin(t *testing.T) { + tests := []struct { + slice []string + separator string + skipEmpty bool + want string + }{ + { + slice: []string{"a", "b", ""}, + separator: ",", + want: "a,b,", + }, + { + slice: []string{"a", "b", ""}, + separator: ",", + skipEmpty: true, + want: "a,b", + }, + // ... + } + // ... +} +``` + +<a id="t-common-setup-scope"></a> + +### Ограничивайте код настройки конкретными тестами (Keep setup code scoped to specific tests) + +По возможности настройка ресурсов и зависимостей должна быть максимально +ограничена конкретными тестовыми случаями. Например, учитывая функцию настройки: + +```go +// mustLoadDataSet загружает набор данных для тестов. +// +// Этот пример очень прост и легко читается. Часто реалистичная настройка более +// сложная, подверженная ошибкам и потенциально медленная. +func mustLoadDataset(t *testing.T) []byte { + t.Helper() + data, err := os.ReadFile("path/to/your/project/testdata/dataset") + + if err != nil { + t.Fatalf("Could not load dataset: %v", err) + } + return data +} +``` + +Вызовите `mustLoadDataset` явно в тестовых функциях, которые в этом нуждаются: + +```go +// Хорошо: +func TestParseData(t *testing.T) { + data := mustLoadDataset(t) + parsed, err := ParseData(data) + if err != nil { + t.Fatalf("Unexpected error parsing data: %v", err) + } + want := &DataTable{ /* ... */ } + if got := parsed; !cmp.Equal(got, want) { + t.Errorf("ParseData(data) = %v, want %v", got, want) + } +} + +func TestListContents(t *testing.T) { + data := mustLoadDataset(t) + contents, err := ListContents(data) + if err != nil { + t.Fatalf("Unexpected error listing contents: %v", err) + } + want := []string{ /* ... */ } + if got := contents; !cmp.Equal(got, want) { + t.Errorf("ListContents(data) = %v, want %v", got, want) + } +} + +func TestRegression682831(t *testing.T) { + if got, want := guessOS("zpc79.example.com"), "grhat"; got != want { + t.Errorf(`guessOS("zpc79.example.com") = %q, want %q`, got, want) + } +} +``` + +Тестовая функция `TestRegression682831` не использует набор данных и поэтому не +вызывает `mustLoadDataset`, которая может быть медленной и подверженной сбоям: + +```go +// Плохо: +var dataset []byte + +func TestParseData(t *testing.T) { + // Как описано выше без вызова mustLoadDataset напрямую. +} + +func TestListContents(t *testing.T) { + // Как описано выше без вызова mustLoadDataset напрямую. +} + +func TestRegression682831(t *testing.T) { + if got, want := guessOS("zpc79.example.com"), "grhat"; got != want { + t.Errorf(`guessOS("zpc79.example.com") = %q, want %q`, got, want) + } +} + +func init() { + dataset = mustLoadDataset() +} +``` + +Пользователь может захотеть запустить функцию изолированно от других и не должен +быть наказан этими факторами: + +```shell +# Нет причин для выполнения дорогой инициализации. +$ go test -run TestRegression682831 +``` + +<a id="t-custom-main"></a> + +#### Когда использовать пользовательскую точку входа `TestMain` (When to use a custom `TestMain` entrypoint) + +Если **все тесты в пакете** требуют общей настройки и **настройка требует +очистки (teardown)**, вы можете использовать [пользовательскую точку входа +testmain]. Это может произойти, если ресурс, требующийся тестовым случаям, +особенно дорог в настройке, и стоимость должна быть амортизирована. Обычно к +этому моменту вы уже убрали несвязанные тесты из набора тестов. Обычно это +используется только для [функциональных тестов (functional tests)]. + +Использование пользовательского `TestMain` **не должно быть вашим первым +выбором** из-за количества осторожности, которое требуется для правильного +использования. Сначала рассмотрите, достаточно ли решения в разделе +[*амортизация общей настройки теста*] или обычного [тестового помощника] для +ваших нужд. + +[пользовательскую точку входа testmain]: + https://golang.org/pkg/testing/#hdr-Main +[функциональных тестов (functional tests)]: + https://en.wikipedia.org/wiki/Functional_testing +[*амортизация общей настройки теста*]: #t-setup-amortization +[тестового помощника]: #t-common-setup-scope + +```go +// Хорошо: +var db *sql.DB + +func TestInsert(t *testing.T) { /* omitted */ } + +func TestSelect(t *testing.T) { /* omitted */ } + +func TestUpdate(t *testing.T) { /* omitted */ } + +func TestDelete(t *testing.T) { /* omitted */ } + +// runMain устанавливает зависимости теста и в конечном итоге выполняет тесты. +// Она определена как отдельная функция, чтобы этапы настройки могли четко +// откладывать (defer) свои шаги очистки. +func runMain(ctx context.Context, m *testing.M) (code int, err error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + d, err := setupDatabase(ctx) + if err != nil { + return 0, err + } + defer d.Close() // Явно очищаем базу данных. + db = d // db определена как переменная на уровне пакета. + + // m.Run() выполняет обычные, определенные пользователем тестовые функции. + // Любые операторы defer, которые были сделаны, будут выполнены после завершения m.Run(). + return m.Run(), nil +} + +func TestMain(m *testing.M) { + code, err := runMain(context.Background(), m) + if err != nil { + // Сообщения о сбоях должны записываться в STDERR, что и использует log.Fatal. + log.Fatal(err) + } + // ПРИМЕЧАНИЕ: операторы defer не выполняются после здесь из-за os.Exit + // завершающего процесс. + os.Exit(code) +} +``` + +В идеале тестовый случай является герметичным (hermetic) между вызовами самого +себя и между другими тестовыми случаями. + +По крайней мере, убедитесь, что отдельные тестовые случаи сбрасывают любое +глобальное состояние, которое они изменили, если они это сделали (например, если +тесты работают с внешней базой данных). + +<a id="t-setup-amortization"></a> + +#### Амортизация общей настройки теста (Amortizing common test setup) + +Использование `sync.Once` может быть уместным, хотя и не обязательно, если все +из следующего верно для общей настройки: + +* Она дорогая. +* Она применяется только к некоторым тестам. +* Она не требует очистки. + +```go +// Хорошо: +var dataset struct { + once sync.Once + data []byte + err error +} + +func mustLoadDataset(t *testing.T) []byte { + t.Helper() + dataset.once.Do(func() { + data, err := os.ReadFile("path/to/your/project/testdata/dataset") + // dataset определена как переменная на уровне пакета. + dataset.data = data + dataset.err = err + }) + if err := dataset.err; err != nil { + t.Fatalf("Could not load dataset: %v", err) + } + return dataset.data +} +``` + +Когда `mustLoadDataset` используется в нескольких тестовых функциях, ее +стоимость амортизируется: + +```go +// Хорошо: +func TestParseData(t *testing.T) { + data := mustLoadDataset(t) + + // Как описано выше. +} + +func TestListContents(t *testing.T) { + data := mustLoadDataset(t) + + // Как описано выше. +} + +func TestRegression682831(t *testing.T) { + if got, want := guessOS("zpc79.example.com"), "grhat"; got != want { + t.Errorf(`guessOS("zpc79.example.com") = %q, want %q`, got, want) + } +} +``` + +Причина, по которой общая очистка сложна, заключается в том, что нет единого +места для регистрации процедур очистки. Если функция настройки (в данном случае +`mustLoadDataset`) полагается на контекст, `sync.Once` может быть +проблематичным. Это потому, что второй из двух конкурентных вызовов функции +настройки должен будет ждать завершения первого вызова, прежде чем вернуться. +Этот период ожидания нельзя легко заставить уважать отмену контекста. + +<a id="string-concat"></a> + +## Конкатенация строк (String concatenation) + +Есть несколько способов конкатенации строк в Go. Некоторые примеры включают: + +* Оператор "+" +* `fmt.Sprintf` +* `strings.Builder` +* `text/template` +* `safehtml/template` + +Хотя не существует универсального правила, какой выбрать, следующие рекомендации +описывают, когда каждый метод предпочтителен. + +<a id="string-concat-simple"></a> + +### Предпочитайте "+" для простых случаев (Prefer "+" for simple cases) + +Предпочитайте использовать "+" при конкатенации нескольких строк. Этот метод +синтаксически самый простой и не требует импорта. + +```go +// Хорошо: +key := "projectid: " + p +``` + +<a id="string-concat-fmt"></a> + +### Предпочитайте `fmt.Sprintf` при форматировании (Prefer `fmt.Sprintf` when formatting) + +Предпочитайте использовать `fmt.Sprintf` при построении сложной строки с +форматированием. Использование многих операторов "+" может затмить конечный +результат. + +```go +// Хорошо: +str := fmt.Sprintf("%s [%s:%d]-> %s", src, qos, mtu, dst) +``` + +```go +// Плохо: +bad := src.String() + " [" + qos.String() + ":" + strconv.Itoa(mtu) + "]-> " + dst.String() +``` + +**Лучшая практика:** Когда результатом операции построения строки является +`io.Writer`, не конструируйте временную строку с помощью `fmt.Sprintf`, чтобы +просто отправить ее в Writer. Вместо этого используйте `fmt.Fprintf`, чтобы +отправлять прямо в Writer. + +Когда форматирование еще сложнее, предпочитайте [`text/template`] или +[`safehtml/template`] по мере необходимости. + +[`text/template`]: https://pkg.go.dev/text/template +[`safehtml/template`]: https://pkg.go.dev/github.com/google/safehtml/template + +<a id="string-concat-piecemeal"></a> + +### Предпочитайте `strings.Builder` для построения строки по частям (Prefer `strings.Builder` for constructing a string piecemeal) + +Предпочитайте использовать `strings.Builder` при построении строки по частям. +`strings.Builder` занимает амортизированное линейное время, тогда как "+" и +`fmt.Sprintf` занимают квадратичное время при последовательном вызове для +формирования большей строки. + +```go +// Хорошо: +b := new(strings.Builder) +for i, d := range digitsOfPi { + fmt.Fprintf(b, "the %d digit of pi is: %d\n", i, d) +} +str := b.String() +``` + +**Примечание:** Для более подробного обсуждения см. [GoTip #29: Building +Strings Efficiently](https://google.github.io/styleguide/go/index.html#gotip). + +<a id="string-constants"></a> + +### Константные строки (Constant strings) + +Предпочитайте использовать обратные кавычки (\`) при создании константных, +многострочных строковых литералов. + +```go +// Хорошо: +usage := `Usage: + +custom_tool [args]` +``` + +```go +// Плохо: +usage := "" + + "Usage:\n" + + "\n" + + "custom_tool [args]" +``` + +<a id="globals"></a> + +## Глобальное состояние (Global state) + +Библиотеки не должны заставлять своих клиентов использовать API, которые +полагаются на [глобальное состояние (global +state)](https://en.wikipedia.org/wiki/Global_variable). Им рекомендуется не +раскрывать API или экспортировать переменные на [уровне пакета (package level)], +которые контролируют поведение для всех клиентов как часть их API. В остальной +части раздела "глобальное" и "состояние на уровне пакета" используются как +синонимы. + +Вместо этого, если ваша функциональность поддерживает состояние, позвольте вашим +клиентам создавать и использовать экземпляры значений. + +**Важно:** Хотя это руководство применимо ко всем разработчикам, оно наиболее +критично для поставщиков инфраструктуры, которые предлагают библиотеки, +интеграции и сервисы другим командам. + +[глобальное состояние (global state)]: + https://en.wikipedia.org/wiki/Global_variable +[уровне пакета (package level)]: https://go.dev/ref/spec#TopLevelDecl + +```go +// Хорошо: +// Пакет sidecar управляет подпроцессами, которые предоставляют функции для приложений. +package sidecar + +type Registry struct { plugins map[string]*Plugin } + +func New() *Registry { return &Registry{plugins: make(map[string]*Plugin)} } + +func (r *Registry) Register(name string, p *Plugin) error { ... } +``` + +Ваши пользователи будут создавать необходимые им данные (`*sidecar.Registry`), а +затем передавать их как явную зависимость: + +```go +// Хорошо: +package main + +func main() { + sidecars := sidecar.New() + if err := sidecars.Register("Cloud Logger", cloudlogger.New()); err != nil { + log.Exitf("Could not setup cloud logger: %v", err) + } + cfg := &myapp.Config{Sidecars: sidecars} + myapp.Run(context.Background(), cfg) +} +``` + +Существуют разные подходы к миграции существующего кода для поддержки передачи +зависимостей. Основной, который вы будете использовать, — передача зависимостей +в качестве параметров конструкторам, функциям, методам или полям структур в +цепочке вызовов. + +См. также: + +* [Go Tip #5: Slimming Your Client + Libraries](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #24: Use Case-Specific + Constructions](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #40: Improving Time Testability with Function + Parameters](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #41: Identify Function Call + Parameters](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #44: Improving Time Testability with Struct + Fields](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #80: Dependency Injection + Principles](https://google.github.io/styleguide/go/index.html#gotip) + +API, которые не поддерживают явную передачу зависимостей, становятся хрупкими с +увеличением числа клиентов: + +```go +// Плохо: +package sidecar + +var registry = make(map[string]*Plugin) + +func Register(name string, p *Plugin) error { /* регистрирует плагин в registry */ } +``` + +Рассмотрим, что происходит в случае тестов, проверяющих код, который транзитивно +зависит от sidecar для облачного логирования. + +```go +// Плохо: +package app + +import ( + "cloudlogger" + "sidecar" + "testing" +) + +func TestEndToEnd(t *testing.T) { + // Система под тестом (SUT) полагается на sidecar для production облачного + // логгера, который уже зарегистрирован. + ... // Проверяем SUT и проверяем инварианты. +} + +func TestRegression_NetworkUnavailability(t *testing.T) { + // У нас был сбой из-за сетевого раздела, который сделал облачный логгер + // неработоспособным, поэтому мы добавили регрессионный тест для проверки SUT с + // тестовым двойником, имитирующим недоступность сети для логгера. + sidecar.Register("cloudlogger", cloudloggertest.UnavailableLogger) + ... // Проверяем SUT и проверяем инварианты. +} + +func TestRegression_InvalidUser(t *testing.T) { + // Система под тестом (SUT) полагается на sidecar для production облачного + // логгера, который уже зарегистрирован. + // + // Упс. cloudloggertest.UnavailableLogger все еще зарегистрирован с + // предыдущего теста. + ... // Проверяем SUT и проверяем инварианты. +} +``` + +Тесты Go выполняются последовательно по умолчанию, поэтому вышеуказанные тесты +выполняются как: + +1. `TestEndToEnd` +2. `TestRegression_NetworkUnavailability`, который переопределяет значение по + умолчанию cloudlogger +3. `TestRegression_InvalidUser`, который требует значения по умолчанию + cloudlogger, зарегистрированного в `package sidecar` + +Это создает тестовый случай, зависящий от порядка, что нарушает запуск с +фильтрами тестов и не позволяет тестам запускаться параллельно или +шардироваться. + +Использование глобального состояния создает проблемы, на которые нет простых +ответов для вас и клиентов API: + +* Что произойдет, если клиенту нужно использовать разные и отдельно работающие + наборы `Plugin` (например, для поддержки нескольких серверов) в одном + процессе? + +* Что произойдет, если клиент захочет заменить зарегистрированный `Plugin` + альтернативной реализацией в тесте, например, [тестовым двойником]? + + Что произойдет, если тестам клиента требуется герметичность между + экземплярами `Plugin` или между всеми зарегистрированными плагинами? + +* Что произойдет, если несколько клиентов `Register` плагин `Plugin` под одним + и тем же именем? Кто победит, если вообще победит? + + Как следует [обрабатывать](https://neonxp.ru/pages/gostyleguide/google/decisions/#handle-errors) ошибки? Если код + вызывает panic или `log.Fatal`, будет ли это всегда [уместно для всех мест, + в которых может быть вызван API](https://neonxp.ru/pages/gostyleguide/google/decisions/#dont-panic)? Может ли клиент + проверить, что он не делает ничего плохого, прежде чем сделать это? + +* Существуют ли определенные этапы начальной загрузки программы или ее + жизненного цикла, во время которых можно вызывать `Register`, а когда нет? + + Что произойдет, если `Register` будет вызван в неподходящее время? Клиент + может вызвать `Register` в [`func + init`](https://go.dev/ref/spec#Package_initialization), до разбора флагов + или после `main`. Этап, на котором вызывается функция, влияет на обработку + ошибок. Если автор API предполагает, что API вызывается *только* во время + инициализации программы без требования, чтобы это было так, это + предположение может подтолкнуть автора к проектированию обработки ошибок для + [завершения программы](https://neonxp.ru/pages/gostyleguide/google/best-practices/#program-init), моделируя API как + функцию типа `Must`. Завершение не подходит для библиотечных функций общего + назначения, которые могут использоваться на любом этапе. + +* Что, если потребности в параллелизме клиента и дизайнера не совпадают? + +См. также: + +* [Go Tip #36: Enclosing Package-Level + State](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #71: Reducing Parallel Test + Flakiness](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #80: Dependency Injection + Principles](https://google.github.io/styleguide/go/index.html#gotip) +* Обработка ошибок: [Look Before You + Leap](https://docs.python.org/3/glossary.html#term-LBYL) против [Easier to + Ask for Forgiveness than + Permission](https://docs.python.org/3/glossary.html#term-EAFP) +* [Unit Testing Practices on Public APIs] + +Глобальное состояние имеет каскадные эффекты на [здоровье кодовой базы +Google](https://neonxp.ru/pages/gostyleguide/google/guide/.md#maintainability). К глобальному состоянию следует подходить с +**крайней тщательностью**. + +[Глобальное состояние бывает нескольких форм](#globals-forms), и вы можете +использовать несколько [лакмусовых тестов, чтобы определить, когда оно +безопасно](#globals-litmus-tests). + +[Unit Testing Practices on Public APIs]: index.md#unit-testing-practices + +<a id="globals-forms"></a> + +### Основные формы API состояния пакета (Major forms of package state APIs) + +Ниже перечислены несколько наиболее распространенных проблемных форм API: + +* Переменные верхнего уровня, независимо от того, экспортируются они или нет. + + ```go + // Плохо: + package logger + + // Sinks управляет выходными источниками по умолчанию для API логирования этого пакета. + // Эта переменная должна быть установлена во время инициализации пакета и никогда после этого. + var Sinks []Sink + ``` + + См. [лакмусовые тесты](#globals-litmus-tests), чтобы узнать, когда они + безопасны. + +* Шаблон [локатора служб (service locator + pattern)](https://en.wikipedia.org/wiki/Service_locator_pattern). См. + [первый пример](#globals). Сам шаблон локатора служб не является + проблематичным, а проблема в том, что локатор определен как глобальный. + +* Реестры для [обратных вызовов + (callbacks)](https://en.wikipedia.org/wiki/Callback_\(computer_programming\)) + и подобного поведения. + + ```go + // Плохо: + package health + + var unhealthyFuncs []func + + func OnUnhealthy(f func()) { + unhealthyFuncs = append(unhealthyFuncs, f) + } + ``` + +* "Толстые" (thick) клиентские синглтоны для таких вещей, как бэкенды, + хранилища, уровни доступа к данным и другие системные ресурсы. Они часто + создают дополнительные проблемы с надежностью служб. + + ```go + // Плохо: + package useradmin + + var client pb.UserAdminServiceClientInterface + + func Client() *pb.UserAdminServiceClient { + if client == nil { + client = ... // Настройка клиента. + } + return client + } + ``` + +> **Примечание:** Многие устаревшие API в кодовой базе Google не следуют этому +> руководству; фактически, некоторые стандартные библиотеки Go позволяют +> настраивать поведение через глобальные значения. Тем не менее, нарушение +> этого руководства устаревшим API **[не должно использоваться как +> прецедент](https://neonxp.ru/pages/gostyleguide/google/guide/#local-consistency)** для продолжения шаблона. +> +> Лучше инвестировать в правильный дизайн API сегодня, чем платить за его +> перепроектирование позже. + +<a id="globals-litmus-tests"></a> + +### Лакмусовые тесты (Litmus tests) + +[API, использующие шаблоны выше](#globals-forms), небезопасны, когда: + +* Несколько функций взаимодействуют через глобальное состояние при выполнении + в одной программе, несмотря на то, что в остальном они независимы (например, + написаны разными авторами в совершенно разных каталогах). +* Независимые тестовые случаи взаимодействуют друг с другом через глобальное + состояние. +* Пользователи API склонны заменять или подменять глобальное состояние для + целей тестирования, особенно чтобы заменить любую часть состояния [тестовым + двойником], например, заглушкой, фейком, шпионом или моком. +* Пользователи должны учитывать особые требования к порядку при взаимодействии + с глобальным состоянием: `func init`, разобраны ли уже флаги и т.д. + +При условии, что вышеуказанные условия избегаются, существует **несколько +ограниченных обстоятельств, при которых эти API безопасны**, а именно, когда +верно любое из следующего: + +* Глобальное состояние логически постоянно + ([пример](https://github.com/klauspost/compress/blob/290f4cfacb3eff892555a491e3eeb569a48665e7/zstd/snappy.go#L413)). +* Наблюдаемое поведение пакета является бессостоятельным (stateless). + Например, общедоступная функция может использовать частную глобальную + переменную в качестве кэша, но пока вызывающая сторона не может отличить + попадания в кэш от промахов, функция является бессостоятельной. +* Глобальное состояние не просачивается в вещи, внешние по отношению к + программе, такие как sidecar-процессы или файлы в общей файловой системе. +* Нет ожидания предсказуемого поведения + ([пример](https://pkg.go.dev/math/rand)). + +> **Примечание:** +> [Sidecar-процессы](https://www.oreilly.com/library/view/designing-distributed-systems/9781491983638/ch02.html) +> могут **не** быть строго локальными для процесса. Они могут и часто +> используются совместно более чем одним процессом приложения. Более того, эти +> sidecar часто взаимодействуют с внешними распределенными системами. +> +> Кроме того, те же правила бессостоятельности, идемпотентности и локальности в +> дополнение к базовым соображениям выше применялись бы к коду самого +> sidecar-процесса! + +Пример одной из таких безопасных ситуаций — [`package +image`](https://pkg.go.dev/image) с его функцией +[`image.RegisterFormat`](https://pkg.go.dev/image#RegisterFormat). Рассмотрим +лакмусовые тесты, примененные к типичному декодеру, например, для обработки +формата [PNG](https://pkg.go.dev/image/png): + +* Множественные вызовы API `package image`, использующие зарегистрированные + декодеры (например, `image.Decode`), не могут мешать друг другу, аналогично + и для тестов. Единственное исключение — `image.RegisterFormat`, но это + смягчается пунктами ниже. +* Крайне маловероятно, что пользователь захочет заменить декодер [тестовым + двойником], так как декодер PNG является примером случая, когда предпочтение + нашей кодовой базы реальным объектам применяется. Однако пользователь с + большей вероятностью заменит декодер тестовым двойником, если декодер + состоятельно взаимодействует с ресурсами операционной системы (например, + сетью). +* Коллизии при регистрации возможны, хотя на практике они, вероятно, редки. +* Декодеры являются бессостоятельными, идемпотентными и чистыми (pure). + +<a id="globals-default-instance"></a> + +### Предоставление экземпляра по умолчанию (Providing a default instance) + +Хотя и не рекомендуется, допустимо предоставить упрощенный API, использующий +состояние на уровне пакета, если вам нужно максимизировать удобство для +пользователя. + +Следуйте [лакмусовым тестам](#globals-litmus-tests) с этими рекомендациями в +таких случаях: + +1. Пакет должен предлагать клиентам возможность создавать изолированные + экземпляры типов пакета, как [описано выше](#globals-forms). +2. Общедоступные API, использующие глобальное состояние, должны быть тонкой + прослойкой (thin proxy) к предыдущему API. Хороший пример этого — + [`http.Handle`](https://pkg.go.dev/net/http#Handle), внутренне вызывающий + [`(*http.ServeMux).Handle`](https://pkg.go.dev/net/http#ServeMux.Handle) на + переменной пакета + [`http.DefaultServeMux`](https://pkg.go.dev/net/http#DefaultServeMux). +3. Этот API уровня пакета должен использоваться только [целями сборки + бинарников (binary build targets)], а не [библиотеками (libraries)], если + только библиотеки не предпринимают рефакторинг для поддержки передачи + зависимостей. Инфраструктурные библиотеки, которые могут быть импортированы + другими пакетами, не должны полагаться на состояние на уровне пакета + импортируемых ими пакетов. + + Например, поставщик инфраструктуры, реализующий sidecar, который должен + использоваться совместно с другими командами, использующими API сверху, + должен предложить API для этого: + + ```go + // Хорошо: + package cloudlogger + + func New() *Logger { ... } + + func Register(r *sidecar.Registry, l *Logger) { + r.Register("Cloud Logging", l) + } + ``` + +4. Этот API уровня пакета должен [документировать](#documentation-conventions) + и обеспечивать соблюдение своих инвариантов (например, на каком этапе + жизненного цикла программы его можно вызывать, можно ли использовать его + параллельно). Кроме того, он должен предоставлять API для сброса глобального + состояния к известному хорошему значению по умолчанию (например, для + облегчения тестирования). + +[целями сборки бинарников (binary build targets)]: + https://github.com/bazelbuild/rules_go/blob/master/docs/go/core/rules.md#go_binary +[библиотеками (libraries)]: + https://github.com/bazelbuild/rules_go/blob/master/docs/go/core/rules.md#go_library + +См. также: + +* [Go Tip #36: Enclosing Package-Level + State](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #80: Dependency Injection + Principles](https://google.github.io/styleguide/go/index.html#gotip) diff --git a/content/pages/gostyleguide/google/decisions.md b/content/pages/gostyleguide/google/decisions.md new file mode 100644 index 0000000..acb17b7 --- /dev/null +++ b/content/pages/gostyleguide/google/decisions.md @@ -0,0 +1,4057 @@ +--- +order: 2 +title: Google Go Style Guide — Решения +--- + +# Решения по стилю Go + +Оригинал: https://google.github.io/styleguide/go/decisions + +[Обзор](https://neonxp.ru/pages/gostyleguide/google/) | [Руководство](https://neonxp.ru/pages/gostyleguide/google/guide) | [Решения](https://neonxp.ru/pages/gostyleguide/google/decisions) | +[Лучшие практики](https://neonxp.ru/pages/gostyleguide/google/best-practices) + + +**Примечание:** Это часть серии документов, описывающих [Стиль Go](https://neonxp.ru/pages/gostyleguide/google/) в +Google. Этот документ является **[нормативным](https://neonxp.ru/pages/gostyleguide/google/#normative), но не +[каноническим](https://neonxp.ru/pages/gostyleguide/google/#canonical)** и подчиняется [основному руководству по +стилю](https://neonxp.ru/pages/gostyleguide/google/guide/). Подробнее см. [в обзоре](https://neonxp.ru/pages/gostyleguide/google/#about). + +<a id="about"></a> + +## Об этом документе + +В этом документе содержатся решения по стилю, призванные унифицировать и дать +стандартные рекомендации, пояснения и примеры для советов, которые дают +наставники по читаемости Go. + +Этот документ **не является исчерпывающим** и будет пополняться со временем. В +случаях, когда [основное руководство по стилю](https://neonxp.ru/pages/gostyleguide/google/guide/) противоречит приведенным +здесь рекомендациям, **руководство по стилю имеет приоритет**, и этот документ +должен быть обновлен соответственно. + +Полный набор документов по стилю Go см. в +[Обзоре](https://google.github.io/styleguide/go#about). + +Следующие разделы были перемещены из "Решений по стилю" в другие части +руководства: + +* **MixedCaps**: см. [guide#mixed-caps](https://neonxp.ru/pages/gostyleguide/google/guide/#mixed-caps) <a + id="mixed-caps"></a> + +* **Форматирование**: см. [guide#formatting](https://neonxp.ru/pages/gostyleguide/google/guide/#formatting) <a + id="formatting"></a> + +* **Длина строки**: см. [guide#line-length](https://neonxp.ru/pages/gostyleguide/google/guide/#line-length) <a + id="line-length"></a> + +<a id="naming"></a> + +## Именование + +Общие рекомендации по именованию см. в разделе об именовании в [основном +руководстве по стилю](https://neonxp.ru/pages/gostyleguide/google/guide/#naming). Следующие разделы дают дальнейшие +разъяснения по конкретным областям именования. + +<a id="underscores"></a> + +### Подчеркивания + +Имена в Go, как правило, не должны содержать подчеркиваний. Существует три +исключения из этого принципа: + +1. Имена пакетов, которые импортируются только сгенерированным кодом, могут + содержать подчеркивания. Подробнее о том, как выбирать имена многословных + пакетов, см. в разделе [имена пакетов](#package-names). +1. Имена тестовых (`Test`), бенчмарк (`Benchmark`) и примеров (`Example`) + функций в файлах `*_test.go` могут содержать подчеркивания. +1. Низкоуровневые библиотеки, взаимодействующие с операционной системой или + cgo, могут повторно использовать идентификаторы, как это сделано в + [`syscall`]. Ожидается, что это будет очень редко встречаться в большинстве + кодовых баз. + +**Примечание:** Имена файлов исходного кода не являются идентификаторами Go и не +должны следовать этим соглашениям. Они могут содержать подчеркивания. + +[`syscall`]: https://pkg.go.dev/syscall#pkg-constants + +<a id="package-names"></a> + +### Имена пакетов + +<a id="TOC-PackageNames"></a> + +В Go имена пакетов должны быть краткими и использовать только строчные буквы и +цифры (например, [`k8s`], [`oauth2`]). Многословные имена пакетов должны +оставаться целыми и в нижнем регистре (например, [`tabwriter`] вместо +`tabWriter`, `TabWriter` или `tab_writer`). + +Избегайте выбора имен пакетов, которые могут быть [затенены] часто используемыми +локальными именами переменных. Например, `usercount` — лучшее имя пакета, чем +`count`, так как `count` — часто используемое имя переменной. + +Имена пакетов Go не должны содержать подчеркиваний. Если вам нужно импортировать +пакет, который содержит их в своем имени (обычно из сгенерированного или +стороннего кода), его необходимо переименовать при импорте в имя, подходящее для +использования в коде Go. + +Исключением является то, что имена пакетов, которые импортируются только +сгенерированным кодом, могут содержать подчеркивания. Конкретные примеры +включают: + +* Использование суффикса `_test` для модульных тестов, проверяющих только + экспортированный API пакета (пакет `testing` называет это ["черным + ящиком"](https://pkg.go.dev/testing)). Например, пакет `linkedlist` должен + определять свои модульные тесты "черного ящика" в пакете с именем + `linkedlist_test` (не `linked_list_test`) + +* Использование подчеркиваний и суффикса `_test` для пакетов, содержащих + функциональные или интеграционные тесты. Например, интеграционный тест + сервиса связного списка может называться `linked_list_service_test` + +* Использование суффикса `_test` для [примеров документации на уровне + пакета](https://go.dev/blog/examples) + +[`tabwriter`]: https://pkg.go.dev/text/tabwriter +[`k8s`]: https://pkg.go.dev/k8s.io/client-go/kubernetes +[`oauth2`]: https://pkg.go.dev/golang.org/x/oauth2 +[shadowed]: best-practices#shadowing + +Избегайте неинформативных имен пакетов, таких как `util`, `utility`, `common`, +`helper`, `model`, `testhelper` и т.д., которые могут побуждать пользователей +пакета [переименовывать его при импорте](#import-renaming). См.: + +* [Рекомендации по так называемым "служебным + пакетам"](https://neonxp.ru/pages/gostyleguide/google/best-practices/#util-packages) +* [Go Tip #97: Что в + имени](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #108: Сила хорошего имени + пакета](https://google.github.io/styleguide/go/index.html#gotip) + +Когда импортированный пакет переименовывается (например, `import foopb +"path/to/foo_go_proto"`), локальное имя пакета должно соответствовать правилам +выше, так как локальное имя определяет, как на символы в пакете ссылаются в +файле. Если данный импорт переименован в нескольких файлах, особенно в одном и +том же или соседних пакетах, по возможности следует использовать одно и то же +локальное имя для согласованности. + +<!--#include file="/go/g3doc/style/includes/special-name-exception.md"--> + +См. также: [Пост в блоге Go об именах +пакетов](https://go.dev/blog/package-names). + +<a id="receiver-names"></a> + +### Имена получателей (Receiver) + +<a id="TOC-ReceiverNames"></a> + +Имена [получателей] (receiver) должны быть: + +* Краткими (обычно одна или две буквы) +* Сокращениями для самого типа +* Применяться последовательно для каждого получателя этого типа + +Длинное имя | Лучшее имя +----------------------------- | ------------------------- +`func (tray Tray)` | `func (t Tray)` +`func (info *ResearchInfo)` | `func (ri *ResearchInfo)` +`func (this *ReportWriter)` | `func (w *ReportWriter)` +`func (self *Scanner)` | `func (s *Scanner)` + +[получателей]: https://golang.org/ref/spec#Method_declarations + +<a id="constant-names"></a> + +### Имена констант + +Имена констант должны использовать [MixedCaps], как и все остальные имена в Go. +([Экспортируемые] константы начинаются с заглавной буквы, а неэкспортируемые — +со строчной.) Это применимо, даже если это нарушает соглашения в других языках. +Имена констант не должны быть производными от их значений и должны объяснять, +что означает это значение. + +```go +// Хорошо: +const MaxPacketSize = 512 + +const ( + ExecuteBit = 1 << iota + WriteBit + ReadBit +) +``` + +[MixedCaps]: guide#mixed-caps +[Экспортируемые]: https://tour.golang.org/basics/3 + +Не используйте имена констант не в стиле MixedCaps или константы с префиксом +`K`. + +```go +// Плохо: +const MAX_PACKET_SIZE = 512 +const kMaxBufferSize = 1024 +const KMaxUsersPergroup = 500 +``` + +Называйте константы в соответствии с их ролью, а не значениями. Если у константы +нет роли, кроме ее значения, то нет необходимости определять ее как константу. + +```go +// Плохо: +const Twelve = 12 + +const ( + UserNameColumn = "username" + GroupColumn = "group" +) +``` + +<!--#include file="/go/g3doc/style/includes/special-name-exception.md"--> + +<a id="initialisms"></a> + +### Аббревиатуры и акронимы (Initialisms) + +<a id="TOC-Initialisms"></a> + +Слова в именах, которые являются аббревиатурами или акронимами (например, `URL` +и `NATO`), должны иметь одинаковый регистр. `URL` должен появляться как `URL` +или `url` (как в `urlPony` или `URLPony`), но никогда как `Url`. Как общее +правило, идентификаторы (например, `ID` и `DB`) также должны быть написаны с +заглавной буквы, аналогично их использованию в английской прозе. + +* В именах с несколькими аббревиатурами (например, `XMLAPI`, потому что оно + содержит `XML` и `API`) каждая буква в данной аббревиатуре должна иметь один + регистр, но каждая аббревиатура в имени не обязана иметь одинаковый регистр. +* В именах с аббревиатурой, содержащей строчную букву (например, `DDoS`, + `iOS`, `gRPC`), аббревиатура должна отображаться, как в стандартной прозе, + если только вам не нужно изменить первую букву ради [экспортируемости + (exportedness)]. В этих случаях вся аббревиатура должна быть в одном + регистре (например, `ddos`, `IOS`, `GRPC`). + +[экспортируемости (exportedness)]: + https://golang.org/ref/spec#Exported_identifiers + +<!-- Keep this table narrow. If it must grow wider, replace with a list. --> + +Использование в английском | Область видимости | Правильно | Неправильно +-------------------------- | ----------------- | --------- | -------------------------------------- +XML API | Экспортировано | `XMLAPI` | `XmlApi`, `XMLApi`, `XmlAPI`, `XMLapi` +XML API | Не экспортировано | `xmlAPI` | `xmlapi`, `xmlApi` +iOS | Экспортировано | `IOS` | `Ios`, `IoS` +iOS | Не экспортировано | `iOS` | `ios` +gRPC | Экспортировано | `GRPC` | `Grpc` +gRPC | Не экспортировано | `gRPC` | `grpc` +DDoS | Экспортировано | `DDoS` | `DDOS`, `Ddos` +DDoS | Не экспортировано | `ddos` | `dDoS`, `dDOS` +ID | Экспортировано | `ID` | `Id` +ID | Не экспортировано | `id` | `iD` +DB | Экспортировано | `DB` | `Db` +DB | Не экспортировано | `db` | `dB` +Txn | Экспортировано | `Txn` | `TXN` + +<!--#include file="/go/g3doc/style/includes/special-name-exception.md"--> + +<a id="getters"></a> + +### Геттеры (Getters) + +<a id="TOC-Getters"></a> + +Имена функций и методов не должны использовать префикс `Get` или `get`, если +только базовое понятие не использует слово "get" (например, HTTP GET). +Предпочитайте начинать имя с существительного напрямую, например, используйте +`Counts` вместо `GetCounts`. + +Если функция включает выполнение сложных вычислений или удаленного вызова, +вместо `Get` можно использовать другое слово, например `Compute` или `Fetch`, +чтобы читателю было ясно, что вызов функции может занять время и может +блокироваться или завершиться неудачей. + +<!--#include file="/go/g3doc/style/includes/special-name-exception.md"--> + +<a id="variable-names"></a> + +### Имена переменных + +<a id="TOC-VariableNames"></a> + +Общее эмпирическое правило заключается в том, что длина имени должна быть +пропорциональна размеру его области видимости и обратно пропорциональна +количеству раз, которое оно используется в этой области. Переменная, созданная +на уровне файла, может потребовать несколько слов, тогда как переменная в +области видимости одного внутреннего блока может быть одним словом или даже +всего одним-двумя символами, чтобы сохранить код понятным и избежать лишней +информации. + +Вот приблизительный базовый уровень. Эти численные рекомендации не являются +строгими правилами. Применяйте суждение, основанное на контексте, [ясности] и +[лаконичности]. + +* Малая область видимости — это область, в которой выполняется одна или две + небольшие операции, скажем, 1-7 строк. +* Средняя область видимости — это несколько небольших или одна большая + операция, скажем, 8-15 строк. +* Большая область видимости — это одна или несколько больших операций, скажем, + 15-25 строк. +* Очень большая область видимости — это все, что занимает больше страницы + (скажем, более 25 строк). + +[ясности]: guide#clarity +[лаконичности]: guide#concision + +Имя, которое может быть совершенно понятным (например, `c` для счетчика) в +маленькой области видимости, может оказаться недостаточным в большей области и +потребует уточнения, чтобы напомнить читателю о его назначении дальше по коду. +Область видимости, в которой много переменных или переменных, представляющих +похожие значения или понятия, может потребовать более длинных имен переменных, +чем предполагает область видимости. + +Специфичность понятия также может помочь сохранить имя переменной кратким. +Например, если используется только одна база данных, короткое имя переменной +вроде `db`, которое обычно зарезервировано для очень малых областей видимости, +может оставаться совершенно понятным даже при очень большой области видимости. В +этом случае одно слово `database`, вероятно, приемлемо в зависимости от размера +области видимости, но не обязательно, поскольку `db` — очень распространенное +сокращение для этого слова с малым количеством альтернативных интерпретаций. + +Имя локальной переменной должно отражать то, что она содержит, и как она +используется в текущем контексте, а не откуда взялось значение. Например, часто +бывает, что лучшее локальное имя переменной не совпадает с именем поля структуры +или поля protobuf. + +В общем: + +* Однобуквенные имена, такие как `count` или `options`, — хорошая отправная + точка. +* Дополнительные слова могут быть добавлены для различения похожих имен, + например `userCount` и `projectCount`. +* Не просто выбрасывайте буквы, чтобы сэкономить на печати. Например, + `Sandbox` предпочтительнее, чем `Sbx`, особенно для экспортируемых имен. +* Опускайте [типы и слова, похожие на типы] из большинства имен переменных. + * Для числа `userCount` — лучшее имя, чем `numUsers` или `usersInt`. + * Для среза `users` — лучшее имя, чем `userSlice`. + * Допустимо включать квалификатор, похожий на тип, если в области + видимости есть две версии значения, например, у вас может быть ввод, + сохраненный в `ageString`, и `age` для распарсенного значения. +* Опускайте слова, которые ясны из [окружающего контекста]. Например, в + реализации метода `UserCount` локальная переменная с именем `userCount`, + вероятно, избыточна; `count`, `users` или даже `c` так же читаемы. + +[типы и слова, похожие на типы]: #repetitive-with-type +[окружающего контекста]: #repetitive-in-context + +<a id="v"></a> + +#### Однобуквенные имена переменных + +Однобуквенные имена переменных могут быть полезным инструментом для минимизации +[повторов](#repetition), но также могут сделать код излишне непрозрачным. +Ограничьте их использование случаями, когда полное слово очевидно и где было бы +излишне повторять его вместо однобуквенной переменной. + +В общем: + +* Для [переменной-получателя метода] предпочтительно одно- или двухбуквенное + имя. +* Использование знакомых имен переменных для распространенных типов часто + полезно: + * `r` для `io.Reader` или `*http.Request` + * `w` для `io.Writer` или `http.ResponseWriter` +* Однобуквенные идентификаторы допустимы в качестве целочисленных переменных + цикла, особенно для индексов (например, `i`) и координат (например, `x` и + `y`). +* Сокращения могут быть допустимыми идентификаторами цикла, если область + видимости короткая, например `for _, n := range nodes { ... }`. + +[переменной-получателя метода]: #receiver-names + +<a id="repetition"></a> + +### Повторы + +<!-- +Примечание для будущих редакторов: + +Не используйте термин "stutter" (заикание) для обозначения случаев, когда имя повторяется. +--> + +Исходный код Go должен избегать ненужных повторов. Один из распространенных +источников этого — повторяющиеся имена, которые часто включают ненужные слова +или повторяют свой контекст или тип. Сам код также может быть излишне +повторяющимся, если один и тот же или похожий сегмент кода появляется несколько +раз в непосредственной близости. + +Повторяющееся именование может принимать многие формы, включая: + +<a id="repetitive-with-package"></a> + +#### Имя пакета vs. экспортируемый символ + +При именовании экспортируемых символов имя пакета всегда видно за пределами +вашего пакета, поэтому избыточную информацию между ними следует сократить или +устранить. Если пакет экспортирует только один тип, и он назван в честь самого +пакета, каноническое имя для конструктора — `New`, если он требуется. + +> **Примеры:** Повторяющееся имя -> Лучшее имя +> +> * `widget.NewWidget` -> `widget.New` +> * `widget.NewWidgetWithName` -> `widget.NewWithName` +> * `db.LoadFromDatabase` -> `db.Load` +> * `goatteleportutil.CountGoatsTeleported` -> `gtutil.CountGoatsTeleported` +> или `goatteleport.Count` +> * `myteampb.MyTeamMethodRequest` -> `mtpb.MyTeamMethodRequest` или +> `myteampb.MethodRequest` + +<a id="repetitive-with-type"></a> + +#### Имя переменной vs. тип + +Компилятор всегда знает тип переменной, и в большинстве случаев читателю также +понятен тип переменной по тому, как она используется. Уточнять тип переменной +необходимо только если ее значение появляется дважды в одной и той же области +видимости. + +Повторяющееся имя | Лучшее имя +------------------------------- | ---------------------- +`var numUsers int` | `var users int` +`var nameString string` | `var name string` +`var primaryProject *Project` | `var primary *Project` + +Если значение появляется в нескольких формах, это можно уточнить либо с помощью +дополнительного слова, например `raw` и `parsed`, либо с помощью базового +представления: + +```go +// Хорошо: +limitRaw := r.FormValue("limit") +limit, err := strconv.Atoi(limitRaw) +``` + +```go +// Хорошо: +limitStr := r.FormValue("limit") +limit, err := strconv.Atoi(limitStr) +``` + +<a id="repetitive-in-context"></a> + +#### Внешний контекст vs. локальные имена + +Имена, включающие информацию из окружающего их контекста, часто создают лишний +шум без пользы. Имя пакета, имя метода, имя типа, имя функции, путь импорта и +даже имя файла могут предоставить контекст, который автоматически квалифицирует +все имена внутри. + +```go +// Плохо: +// В пакете "ads/targeting/revenue/reporting" +type AdsTargetingRevenueReport struct{} + +func (p *Project) ProjectName() string +``` + +```go +// Хорошо: +// В пакете "ads/targeting/revenue/reporting" +type Report struct{} + +func (p *Project) Name() string +``` + +```go +// Плохо: +// В пакете "sqldb" +type DBConnection struct{} +``` + +```go +// Хорошо: +// В пакете "sqldb" +type Connection struct{} +``` + +```go +// Плохо: +// В пакете "ads/targeting" +func Process(in *pb.FooProto) *Report { + adsTargetingID := in.GetAdsTargetingID() +} +``` + +```go +// Хорошо: +// В пакете "ads/targeting" +func Process(in *pb.FooProto) *Report { + id := in.GetAdsTargetingID() +} +``` + +Повторение, как правило, следует оценивать в контексте использования символа, а +не изолированно. Например, следующий код содержит множество имен, которые могут +быть хороши в некоторых обстоятельствах, но избыточны в контексте: + +```go +// Плохо: +func (db *DB) UserCount() (userCount int, err error) { + var userCountInt64 int64 + if dbLoadError := db.LoadFromDatabase("count(distinct users)", &userCountInt64); dbLoadError != nil { + return 0, fmt.Errorf("failed to load user count: %s", dbLoadError) + } + userCount = int(userCountInt64) + return userCount, nil +} +``` + +Вместо этого информацию об именах, которые ясны из контекста или использования, +часто можно опустить: + +```go +// Хорошо: +func (db *DB) UserCount() (int, error) { + var count int64 + if err := db.Load("count(distinct users)", &count); err != nil { + return 0, fmt.Errorf("failed to load user count: %s", err) + } + return int(count), nil +} +``` + +<a id="commentary"></a> + +## Комментарии + +Соглашения, касающиеся комментариев (что комментировать, какой стиль +использовать, как предоставлять исполняемые примеры и т.д.), предназначены для +поддержки удобства чтения документации публичного API. Подробнее см. [Effective +Go](http://golang.org/doc/effective_go.html#commentary). + +Раздел о [соглашениях по документации] в документе о лучших практиках +рассматривает это подробнее. + +**Лучшая практика:** Используйте [предпросмотр документации (doc preview)] во +время разработки и проверки кода, чтобы увидеть, является ли документация и +исполняемые примеры полезными и представлены ли они так, как вы ожидаете. + +**Совет:** Godoc использует очень мало специального форматирования; списки и +фрагменты кода обычно должны иметь отступ, чтобы избежать переноса строк. Кроме +отступа, декорации, как правило, следует избегать. + +[предпросмотр документации (doc preview)]: best-practices#documentation-preview +[соглашениям по документации]: best-practices#documentation-conventions + +<a id="comment-line-length"></a> + +### Длина строк комментария + +Убедитесь, что комментарии читаемы из исходного кода даже на узких экранах. + +Когда комментарий становится слишком длинным, рекомендуется разбить его на +несколько однострочных комментариев. По возможности стремитесь к комментариям, +которые будут хорошо читаться на терминале шириной 80 колонок, однако это не +жесткий предел; в Go нет фиксированного ограничения длины строки для +комментариев. Стандартная библиотека, например, часто предпочитает разрывать +комментарий по пунктуации, что иногда оставляет отдельные строки ближе к отметке +60-70 символов. + +Существует множество существующего кода, в котором комментарии превышают 80 +символов в длину. Эти рекомендации не следует использовать как оправдание для +изменения такого кода в ходе проверки на читаемость (см. +[согласованность](https://neonxp.ru/pages/gostyleguide/google/guide/#consistency)), хотя командам рекомендуется использовать +возможность обновлять комментарии в соответствии с этим руководством в рамках +других рефакторингов. Основная цель этого руководства — гарантировать, что все +наставники по читаемости Go дают одинаковые рекомендации, когда и если +рекомендации даются. + +Подробнее о комментариях см. в [этом посте из блога Go о документации]. + +[этом посте из блога Go о документации]: + https://blog.golang.org/godoc-documenting-go-code + +```text +# Хорошо: +// Это абзац комментария. +// Длина отдельных строк не имеет значения в Godoc; +// но выбор переноса делает его легко читаемым на узких экранах. +// +// Не беспокойтесь слишком о длинном URL: +// https://supercalifragilisticexpialidocious.example.com:8080/Animalia/Chordata/Mammalia/Rodentia/Geomyoidea/Geomyidae/ +// +// Аналогично, если у вас есть другая информация, которая становится неудобной +// из-за слишком большого количества разрывов строк, используйте свое суждение и включите длинную строку, +// если она помогает, а не мешает. +``` + +Избегайте комментариев, которые будут многократно переноситься на маленьких +экранах, что ухудшает удобство чтения. + +```text +# Плохо: +// Это абзац комментария. Длина отдельных строк не имеет значения в Godoc; +// но выбор переноса создает неровные строки на узких экранах или при просмотре кода, +// что может раздражать, особенно в блоке комментариев, который будет переноситься +// многократно. +// +// Не беспокойтесь слишком о длинном URL: +// https://supercalifragilisticexpialidocious.example.com:8080/Animalia/Chordata/Mammalia/Rodentia/Geomyoidea/Geomyidae/ +``` + +<a id="doc-comments"></a> + +### Документирующие комментарии (Doc comments) + +<a id="TOC-DocComments"></a> + +Все экспортируемые имена верхнего уровня должны иметь документирующие +комментарии, как и неэкспортируемые объявления типов или функций с неочевидным +поведением или смыслом. Эти комментарии должны быть [полными предложениями], +которые начинаются с имени описываемого объекта. Артикль ("a", "an", "the") +может предшествовать имени, чтобы оно читалось более естественно. + +```go +// Хорошо: +// Request представляет запрос на выполнение команды. +type Request struct { ... + +// Encode записывает JSON-кодировку req в w. +func Encode(w io.Writer, req *Request) { ... +``` + +Документирующие комментарии появляются в [Godoc](https://pkg.go.dev/) и +выводятся в IDE, поэтому их следует писать для всех, кто использует пакет. + +[полными предложениями]: #comment-sentences + +Документирующий комментарий относится к следующему символу или группе полей, +если он появляется в структуре. + +```go +// Хорошо: +// Options настраивает сервис управления группами. +type Options struct { + // Общая настройка: + Name string + Group *FooGroup + + // Зависимости: + DB *sql.DB + + // Кастомизация: + LargeGroupThreshold int // опционально; по умолчанию: 10 + MinimumMembers int // опционально; по умолчанию: 2 +} +``` + +**Лучшая практика:** Если у вас есть документирующие комментарии для +неэкспортируемого кода, следуйте тому же обычаю, как если бы он был +экспортируемым (а именно, начиная комментарий с неэкспортируемого имени). Это +упрощает его экспорт позже простой заменой неэкспортируемого имени на новое +экспортируемое в комментариях и коде. + +<a id="comment-sentences"></a> + +### Предложения в комментариях + +<a id="TOC-CommentSentences"></a> + +Комментарии, которые являются полными предложениями, должны начинаться с +заглавной буквы и заканчиваться пунктуацией, как стандартные английские +предложения. (В качестве исключения можно начать предложение с +некапитализированного имени идентификатора, если оно иначе понятно. Такие +случаи, вероятно, лучше делать только в начале абзаца.) + +Комментарии, которые являются фрагментами предложений, не имеют таких требований +к пунктуации или капитализации. + +[Документирующие комментарии] всегда должны быть полными предложениями, и +поэтому всегда должны начинаться с заглавной буквы и заканчиваться пунктуацией. +Простые комментарии в конце строки (особенно для полей структуры) могут быть +простыми фразами, которые предполагают, что имя поля является подлежащим. + +```go +// Хорошо: +// Server обрабатывает подачу цитат из собрания сочинений Шекспира. +type Server struct { + // BaseDir указывает на базовый каталог, в котором хранятся работы Шекспира. + // + // Ожидается следующая структура каталога: + // {BaseDir}/manifest.json + // {BaseDir}/{name}/{name}-part{number}.txt + BaseDir string + + WelcomeMessage string // отображается при входе пользователя + ProtocolVersion string // проверяется для входящих запросов + PageLength int // строк на странице при печати (опционально; по умолчанию: 20) +} +``` + +[Документирующие комментарии]: #doc-comments + +<a id="examples"></a> + +### Примеры + +<a id="TOC-Examples"></a> + +Пакеты должны четко документировать предполагаемое использование. Постарайтесь +предоставить [исполняемый пример]; примеры появляются в Godoc. Исполняемые +примеры принадлежат тестовому файлу, а не файлу исходного кода продакшена. +Смотрите этот пример ([Godoc], [исходный код]). + +[исполняемый пример]: http://blog.golang.org/examples +[Godoc]: https://pkg.go.dev/time#example-Duration +[исходный код]: + https://cs.opensource.google/go/go/+/HEAD:src/time/example_test.go + +Если предоставить исполняемый пример нецелесообразно, пример кода может быть +предоставлен внутри комментариев к коду. Как и другие фрагменты кода и командной +строки в комментариях, он должен следовать стандартным соглашениям +форматирования. + +<a id="named-result-parameters"></a> + +### Именованные возвращаемые параметры + +<a id="TOC-NamedResultParameters"></a> + +При именовании параметров учитывайте, как сигнатуры функций отображаются в +Godoc. Имя самой функции и тип возвращаемых параметров часто достаточно ясны. + +```go +// Хорошо: +func (n *Node) Parent1() *Node +func (n *Node) Parent2() (*Node, error) +``` + +Если функция возвращает два или более параметра одного типа, добавление имен +может быть полезным. + +```go +// Хорошо: +func (n *Node) Children() (left, right *Node, err error) +``` + +Если вызывающая сторона должна выполнить действие над определенными +возвращаемыми параметрами, их именование может помочь подсказать, какое это +действие: + +```go +// Хорошо: +// WithTimeout возвращает контекст, который будет отменен не позднее, чем через длительность d +// от текущего момента. +// +// Вызывающая сторона должна обеспечить вызов возвращенной функции cancel, когда +// контекст больше не нужен, чтобы предотвратить утечку ресурсов. +func WithTimeout(parent Context, d time.Duration) (ctx Context, cancel func()) +``` + +В приведенном выше коде отмена — это конкретное действие, которое должна +выполнить вызывающая сторона. Однако, если бы возвращаемые параметры были +записаны просто как `(Context, func())`, было бы неясно, что подразумевается под +"функцией отмены". + +Не используйте именованные возвращаемые параметры, когда имена создают [ненужные +повторы](#repetitive-with-type). + +```go +// Плохо: +func (n *Node) Parent1() (node *Node) +func (n *Node) Parent2() (node *Node, err error) +``` + +Не называйте возвращаемые параметры, чтобы избежать объявления переменной внутри +функции. Эта практика приводит к излишней многословности API в обмен на +незначительную краткость реализации. + +[Голые возвраты (Naked returns)] допустимы только в маленькой функции. Как +только это функция среднего размера, будьте явны со своими возвращаемыми +значениями. Аналогично, не называйте возвращаемые параметры только потому, что +это позволяет вам использовать голые возвраты. [Ясность](https://neonxp.ru/pages/gostyleguide/google/guide/#clarity) всегда +важнее, чем сэкономить несколько строк в вашей функции. + +Всегда приемлемо назвать возвращаемый параметр, если его значение должно быть +изменено в отложенном замыкании. + +> **Совет:** Типы часто могут быть понятнее, чем имена в сигнатурах функций. +> [GoTip #38: Функции как именованные типы] демонстрирует это. +> +> В [`WithTimeout`] выше, реальный код использует [`CancelFunc`] вместо простого +> `func()` в списке возвращаемых параметров и требует небольших усилий для +> документирования. + +[Голые возвраты (Naked returns)]: https://tour.golang.org/basics/7 +[GoTip #38: Функции как именованные типы]: + https://google.github.io/styleguide/go/index.html#gotip +[`WithTimeout`]: https://pkg.go.dev/context#WithTimeout +[`CancelFunc`]: https://pkg.go.dev/context#CancelFunc + +<a id="package-comments"></a> + +### Комментарии к пакетам + +<a id="TOC-PackageComments"></a> + +Комментарии к пакету должны появляться непосредственно перед объявлением пакета, +без пустой строки между комментарием и именем пакета. Пример: + +```go +// Хорошо: +// Package math предоставляет базовые константы и математические функции. +// +// Этот пакет не гарантирует битовой идентичности результатов на разных архитектурах. +package math +``` + +Должен быть ровно один комментарий к пакету на пакет. Если пакет состоит из +нескольких файлов, ровно в одном из файлов должен быть комментарий к пакету. + +Комментарии для пакетов `main` имеют немного другую форму, где имя правила +`go_binary` в BUILD-файле заменяет имя пакета. + +```go +// Хорошо: +// Команда seed_generator — это утилита, которая генерирует файл сида Finch +// из набора JSON-конфигураций исследований. +package main +``` + +Другие стили комментариев допустимы, если имя бинарника точно такое же, как +написано в BUILD-файле. Когда имя бинарника — первое слово, его заглавная буква +обязательна, даже если оно не совпадает с написанием вызова в командной строке. + +```go +// Хорошо: +// Binary seed_generator ... +// Command seed_generator ... +// Program seed_generator ... +// The seed_generator command ... +// The seed_generator program ... +// Seed_generator ... +``` + +Советы: + +* Примеры вызовов командной строки и использования API могут быть полезной + документацией. Для форматирования Godoc сделайте отступ для строк + комментария, содержащих код. + +* Если нет очевидного основного файла или если комментарий к пакету необычайно + длинный, допустимо поместить документирующий комментарий в файл с именем + `doc.go`, содержащий только комментарий и объявление пакета. + +* Многострочные комментарии могут использоваться вместо нескольких + однострочных. Это в первую очередь полезно, если документация содержит + разделы, которые могут быть полезны для копирования и вставки из исходного + файла, как, например, примеры командной строки (для бинарников) и примеры + шаблонов. + + ```go + // Хорошо: + /* + Команда seed_generator — это утилита, которая генерирует файл сида Finch + из набора JSON-конфигураций исследований. + + seed_generator *.json | base64 > finch-seed.base64 + */ + package template + ``` + +* Комментарии, предназначенные для сопровождающих и относящиеся ко всему + файлу, обычно размещаются после объявлений импорта. Они не отображаются в + Godoc и не подчиняются приведенным выше правилам для комментариев к пакетам. + +<a id="imports"></a> + +## Импорты + +<a id="TOC-Imports"></a> + +<a id="import-renaming"></a> + +### Переименование импортов + +Импорты пакетов обычно не должны переименовываться, но есть случаи, когда они +должны быть переименованы или когда переименование улучшает читаемость. + +Локальные имена для импортированных пакетов должны следовать [рекомендациям по +именованию пакетов](#package-names), включая запрет на использование +подчеркиваний и заглавных букв. Старайтесь быть +[последовательными](https://neonxp.ru/pages/gostyleguide/google/guide/#consistency), всегда используя одно и то же локальное +имя для одного и того же импортированного пакета. + +Импортированный пакет *должен* быть переименован, чтобы избежать конфликта имен +с другими импортами. (Следствие из этого: [хорошие имена +пакетов](#package-names) не должны требовать переименования.) В случае конфликта +имен предпочтительнее переименовать наиболее локальный или специфичный для +проекта импорт. + +Сгенерированные пакеты протоколов буфера *должны* быть переименованы, чтобы +удалить подчеркивания из их имен, и их локальные имена должны иметь суффикс +`pb`. Подробнее см. [лучшие практики для proto и заглушек +(stubs)](https://neonxp.ru/pages/gostyleguide/google/best-practices/#import-protos). + +```go +// Хорошо: +import ( + foosvcpb "path/to/package/foo_service_go_proto" +) +``` + +Наконец, импортированный, несгенерированный пакет *может* быть переименован, +если он имеет неинформативное имя (например, `util` или `v1`) Делайте это +экономно: не переименовывайте пакет, если код, окружающий использование пакета, +передает достаточно контекста. По возможности предпочитайте рефакторинг самого +пакета с более подходящим именем. + +```go +// Хорошо: +import ( + core "github.com/kubernetes/api/core/v1" + meta "github.com/kubernetes/apimachinery/pkg/apis/meta/v1beta1" +) +``` + +Если вам нужно импортировать пакет, имя которого конфликтует с распространенным +локальным именем переменной, которое вы хотите использовать (например, `url`, +`ssh`), и вы хотите переименовать пакет, предпочтительный способ сделать это — +использовать суффикс `pkg` (например, `urlpkg`). Обратите внимание, что +возможно затенение пакета локальной переменной; это переименование необходимо +только если пакет все еще нужно использовать, когда такая переменная находится в +области видимости. + +<a id="import-grouping"></a> + +### Группировка импортов + +Импорты должны быть организованы в следующие группы по порядку: + +1. Пакеты стандартной библиотеки + +1. Другие пакеты (проекта и вендорные) + +1. Импорты Protocol Buffer (например, `fpb "path/to/foo_go_proto"`) + +1. Импорт для [побочных эффектов (side + effects)](https://go.dev/doc/effective_go#blank_import) (например, `_ + "path/to/package"`) + +```go +// Хорошо: +package main + +import ( + "fmt" + "hash/adler32" + "os" + + "github.com/dsnet/compress/flate" + "golang.org/x/text/encoding" + "google.golang.org/protobuf/proto" + + foopb "myproj/foo/proto/proto" + + _ "myproj/rpc/protocols/dial" + _ "myproj/security/auth/authhooks" +) +``` + +<a id="import-blank"></a> + +### "Пустой" импорт (`import _`) + +<a id="TOC-ImportBlank"></a> + +Пакеты, которые импортируются только для их побочных эффектов (используя +синтаксис `import _ "package"`), могут импортироваться только в главном пакете +(`main`) или в тестах, которые требуют их. + +Некоторые примеры таких пакетов: + +* [time/tzdata](https://pkg.go.dev/time/tzdata) + +* [image/jpeg](https://pkg.go.dev/image/jpeg) в коде обработки изображений + +Избегайте пустых импортов в библиотечных пакетах, даже если библиотека косвенно +зависит от них. Ограничение импортов с побочными эффектами главным пакетом +помогает контролировать зависимости и позволяет писать тесты, которые полагаются +на другой импорт без конфликта или лишних затрат на сборку. + +Следующие исключения являются единственными для этого правила: + +* Вы можете использовать пустой импорт, чтобы обойти проверку на запрещенные + импорты в [статическом анализаторе nogo]. + +* Вы можете использовать пустой импорт пакета + [embed](https://pkg.go.dev/embed) в исходном файле, который использует + директиву компилятора `//go:embed`. + +**Совет:** Если вы создаете библиотечный пакет, который косвенно зависит от +импорта с побочным эффектом в продакшене, задокументируйте предполагаемое +использование. + +[статическом анализаторе nogo]: + https://github.com/bazelbuild/rules_go/blob/master/go/nogo.rst + +<a id="import-dot"></a> + +### "Точечный" импорт (`import .`) + +<a id="TOC-ImportDot"></a> + +Форма `import .` — это языковая возможность, позволяющая переносить +идентификаторы, экспортированные из другого пакета, в текущий пакет без +квалификации. Подробнее см. [спецификацию +языка](https://go.dev/ref/spec#Import_declarations). + +Не **используйте** эту возможность в кодовой базе Google; это затрудняет +понимание, откуда взялась функциональность. + +```go +// Плохо: +package foo_test + +import ( + "bar/testutil" // также импортирует "foo" + . "foo" +) + +var myThing = Bar() // Bar определен в пакете foo; квалификация не нужна. +``` + +```go +// Хорошо: +package foo_test + +import ( + "bar/testutil" // также импортирует "foo" + "foo" +) + +var myThing = foo.Bar() +``` + +<a id="errors"></a> + +## Ошибки + +<a id="returning-errors"></a> + +### Возврат ошибок + +<a id="TOC-ReturningErrors"></a> + +Используйте `error` для сигнализации о том, что функция может завершиться +неудачей. По соглашению, `error` — последний параметр результата. + +```go +// Хорошо: +func Good() error { /* ... */ } +``` + +Возврат `nil` в качестве ошибки — идиоматический способ сигнализировать об +успешной операции, которая в противном случае могла бы завершиться неудачей. +Если функция возвращает ошибку, вызывающие стороны должны рассматривать все +не-ошибочные возвращаемые значения как неопределенные, если явно не +задокументировано иначе. Обычно не-ошибочные возвращаемые значения являются их +нулевыми значениями, но на это нельзя полагаться. + +```go +// Хорошо: +func GoodLookup() (*Result, error) { + // ... + if err != nil { + return nil, err + } + return res, nil +} +``` + +Экспортируемые функции, возвращающие ошибки, должны возвращать их, используя тип +`error`. Конкретные типы ошибок подвержены тонким ошибкам: конкретный `nil` +указатель может быть завернут в интерфейс и таким образом стать ненулевым +значением (см. [FAQ по Go на эту тему][nil error]). + +```go +// Плохо: +func Bad() *os.PathError { /*...*/ } +``` + +**Совет:** Функция, принимающая аргумент [`context.Context`], обычно должна +возвращать `error`, чтобы вызывающая сторона могла определить, была ли отменена +контекст во время выполнения функции. + +[nil error]: https://golang.org/doc/faq#nil_error + +<a id="error-strings"></a> + +### Строки ошибок + +<a id="TOC-ErrorStrings"></a> + +Строки ошибок не должны начинаться с заглавной буквы (если только не начинаются +с экспортируемого имени, имени собственного или аббревиатуры) и не должны +заканчиваться пунктуацией. Это потому что строки ошибок обычно появляются внутри +другого контекста перед выводом пользователю. + +```go +// Плохо: +err := fmt.Errorf("Something bad happened.") +``` + +```go +// Хорошо: +err := fmt.Errorf("something bad happened") +``` + +С другой стороны, стиль для полного отображаемого сообщения (логирование, +неудача теста, ответ API или другой UI) зависит от ситуации, но обычно должен +быть написан с заглавной буквы. + +```go +// Хорошо: +log.Infof("Operation aborted: %v", err) +log.Errorf("Operation aborted: %v", err) +t.Errorf("Op(%q) failed unexpectedly; err=%v", args, err) +``` + +<a id="handle-errors"></a> + +### Обработка ошибок + +<a id="TOC-HandleErrors"></a> + +Код, который сталкивается с ошибкой, должен принять осознанное решение о том, +как ее обработать. Обычно нецелесообразно отбрасывать ошибки, используя +переменные `_`. Если функция возвращает ошибку, сделайте одно из следующих +действий: + +* Обработайте и исправьте ошибку немедленно. +* Верните ошибку вызывающей стороне. +* В исключительных ситуациях вызовите [`log.Fatal`] или (если абсолютно + необходимо) `panic`. + +**Примечание:** `log.Fatalf` здесь не из стандартной библиотеки log. См. +[#logging]. + +В редких обстоятельствах, когда уместно игнорировать или отбросить ошибку +(например, вызов [`(*bytes.Buffer).Write`], который, как задокументировано, +никогда не завершается неудачей), сопровождающий комментарий должен объяснять, +почему это безопасно. + +```go +// Хорошо: +var b *bytes.Buffer + +n, _ := b.Write(p) // никогда не возвращает ненулевую ошибку +``` + +Для дальнейшего обсуждения и примеров обработки ошибок см. [Effective +Go](http://golang.org/doc/effective_go.html#errors) и [лучшие +практики](https://neonxp.ru/pages/gostyleguide/google/best-practices/.md#error-handling). + +[`(*bytes.Buffer).Write`]: https://pkg.go.dev/bytes#Buffer.Write + +<a id="in-band-errors"></a> + +### Ошибки "в потоке" (In-band errors) + +<a id="TOC-In-Band-Errors"></a> + +В C и подобных языках распространено, что функции возвращают значения, такие как +-1, null или пустую строку, чтобы сигнализировать об ошибках или отсутствующих +результатах. Это известно как обработка ошибок "в потоке". + +```go +// Плохо: +// Lookup возвращает значение для ключа или -1, если нет соответствия для ключа. +func Lookup(key string) int +``` + +Неспособность проверить значение ошибки "в потоке" может привести к ошибкам и +может приписать ошибки не той функции. + +```go +// Плохо: +// Следующая строка возвращает ошибку, что Parse завершился неудачей для входного значения, +// тогда как на самом деле неудача в том, что нет соответствия для missingKey. +return Parse(Lookup(missingKey)) +``` + +Поддержка Go нескольких возвращаемых значений предоставляет лучшее решение (см. +[раздел Effective Go о множественных возвращаемых значениях]). Вместо того +чтобы требовать от клиентов проверять значение ошибки "в потоке", функция должна +возвращать дополнительное значение, чтобы указать, являются ли ее другие +возвращаемые значения валидными. Это возвращаемое значение может быть ошибкой +или булевым значением, когда объяснение не нужно, и должно быть последним +возвращаемым значением. + +```go +// Хорошо: +// Lookup возвращает значение для ключа или ok=false, если нет соответствия для ключа. +func Lookup(key string) (value string, ok bool) +``` + +Такой API предотвращает ошибочное написание вызывающей стороной +`Parse(Lookup(key))`, что вызывает ошибку времени компиляции, поскольку +`Lookup(key)` имеет 2 вывода. + +Возврат ошибок таким образом способствует более надежной и явной обработке +ошибок: + +```go +// Хорошо: +value, ok := Lookup(key) +if !ok { + return fmt.Errorf("no value for %q", key) +} +return Parse(value) +``` + +Некоторые функции стандартной библиотеки, например в пакете `strings`, +возвращают значения ошибок "в потоке". Это значительно упрощает код для +манипуляций со строками за счет необходимости большей внимательности от +программиста. В целом, код Go в кодовой базе Google должен возвращать +дополнительные значения для ошибок. + +[раздел Effective Go о множественных возвращаемых значениях]: + http://golang.org/doc/effective_go.html#multiple-returns + +<a id="indent-error-flow"></a> + +### Отступы для потока ошибок + +<a id="TOC-IndentErrorFlow"></a> + +Обрабатывайте ошибки перед продолжением выполнения остального кода. Это улучшает +читаемость кода, позволяя читателю быстро найти нормальный путь выполнения. Эта +же логика применяется к любому блоку, который проверяет условие, а затем +завершается терминальным условием (например, `return`, `panic`, `log.Fatal`). + +Код, который выполняется, если терминальное условие не выполнено, должен +появляться после блока `if` и не должен быть вложен в предложение `else`. + +```go +// Хорошо: +if err != nil { + // обработка ошибки + return // или continue, и т.д. +} +// нормальный код +``` + +```go +// Плохо: +if err != nil { + // обработка ошибки +} else { + // нормальный код, который выглядит ненормальным из-за отступа +} +``` + +> **Совет:** Если вы используете переменную более чем для нескольких строк кода, +> как правило, не стоит использовать стиль `if` с инициализатором. В этих +> случаях обычно лучше вынести объявление наружу и использовать стандартный `if` +> оператор: +> +> ```go +> // Хорошо: +> x, err := f() +> if err != nil { +> // обработка ошибки +> return +> } +> // много кода, который использует x +> // на нескольких строках +> ``` +> +> ```go +> // Плохо: +> if x, err := f(); err != nil { +> // обработка ошибки +> return +> } else { +> // много кода, который использует x +> // на нескольких строках +> } +> ``` + +Подробнее см. [Go Tip #1: Линия видимости (Line of Sight)] и [TotT: Reduce Code +Complexity by Reducing +Nesting](https://testing.googleblog.com/2017/06/code-health-reduce-nesting-reduce.html). + +[Go Tip #1: Линия видимости (Line of Sight)]: + https://google.github.io/styleguide/go/index.html#gotip + +<a id="language"></a> + +## Язык + +<a id="literal-formatting"></a> + +### Форматирование литералов + +Go обладает исключительно мощным [синтаксисом составных литералов], с помощью +которого можно выражать глубоко вложенные, сложные значения одним выражением. +По возможности следует использовать этот синтаксис литералов вместо построения +значений поле за полем. Форматирование `gofmt` для литералов, как правило, +довольно хорошее, но существуют некоторые дополнительные правила для сохранения +читаемости и поддерживаемости этих литералов. + +[синтаксисом составных литералов]: + https://golang.org/ref/spec#Composite_literals + +<a id="literal-field-names"></a> + +#### Имена полей + +Литералы структур должны указывать **имена полей** для типов, определенных вне +текущего пакета. + +* Включайте имена полей для типов из других пакетов. + + ```go + // Хорошо: + // https://pkg.go.dev/encoding/csv#Reader + r := csv.Reader{ + Comma: ',', + Comment: '#', + FieldsPerRecord: 4, + } + ``` + + Положение полей в структуре и полный набор полей (оба из которых необходимо + указать правильно, когда имена полей опущены) обычно не считаются частью + публичного API структуры; указание имени поля необходимо, чтобы избежать + ненужной связи. + + ```go + // Плохо: + r := csv.Reader{',', '#', 4, false, false, false, false} + ``` + +* Для локальных типов пакета имена полей не обязательны. + + ```go + // Хорошо: + okay := Type{42} + also := internalType{4, 2} + ``` + + Имена полей все же следует использовать, если это делает код яснее, и это + очень распространено. Например, структуру с большим количеством полей почти + всегда следует инициализировать с указанием имен полей. + + <!-- TODO: Maybe a better example here that doesn't have many fields. --> + + ```go + // Хорошо: + okay := StructWithLotsOfFields{ + field1: 1, + field2: "two", + field3: 3.14, + field4: true, + } + ``` + +<a id="literal-matching-braces"></a> + +#### Совпадение скобок + +Закрывающая часть пары фигурных скобок всегда должна появляться на строке с +таким же количеством отступов, как и открывающая скобка. Однострочные литералы +обязательно имеют это свойство. Когда литерал занимает несколько строк, +сохранение этого свойства сохраняет соответствие скобок для литералов таким же, +как соответствие скобок для обычных синтаксических конструкций Go, таких как +функции и операторы `if`. + +Самая распространенная ошибка в этой области — ставить закрывающую скобку на той +же строке, что и значение в многострочном литерале структуры. В этих случаях +строка должна заканчиваться запятой, а закрывающая скобка должна появляться на +следующей строке. + +```go +// Хорошо: +good := []*Type\{\{Key: "value"\}\} +``` + +```go +// Хорошо: +good := []*Type{ + {Key: "multi"}, + {Key: "line"}, +} +``` + +```go +// Плохо: +bad := []*Type{ + {Key: "multi"}, + {Key: "line"}} +``` + +```go +// Плохо: +bad := []*Type{ + { + Key: "value"}, +} +``` + +<a id="literal-cuddled-braces"></a> + +#### "Объединенные" скобки (Cuddled braces) + +Удаление пробела между фигурными скобками (так называемое "объединение" их) для +литералов срезов и массивов допустимо только когда оба следующих условия +истинны. + +* [Отступы совпадают](#literal-matching-braces) +* Внутренние значения также являются литералами или сборщиками protobuf (то + есть не переменной или другим выражением) + +```go +// Хорошо: +good := []*Type{ + { // Не объединены + Field: "value", + }, + { + Field: "value", + }, +} +``` + +```go +// Хорошо: +good := []*Type{\{ // Правильно объединены + Field: "value", +}, { + Field: "value", +}\} +``` + +```go +// Хорошо: +good := []*Type{ + first, // Не могут быть объединены + {Field: "second"}, +} +``` + +```go +// Хорошо: +okay := []*pb.Type{pb.Type_builder{ + Field: "first", // Сборщики Proto могут быть объединены для экономии вертикального пространства +}.Build(), pb.Type_builder{ + Field: "second", +}.Build()} +``` + +```go +// Плохо: +bad := []*Type{ + first, + { + Field: "second", + }\} +``` + +<a id="literal-repeated-type-names"></a> + +#### Повторяющиеся имена типов + +Повторяющиеся имена типов могут быть опущены в литералах срезов и карт (map). +Это может помочь уменьшить загромождение. Приемлемый случай для явного +повторения имен типов — это работа со сложным типом, который не является +распространенным в вашем проекте, когда повторяющиеся имена типов находятся на +строках, далеко отстоящих друг от друга, и могут напоминать читателю о +контексте. + +```go +// Хорошо: +good := []*Type{ + {A: 42}, + {A: 43}, +} +``` + +```go +// Плохо: +repetitive := []*Type{ + &Type{A: 42}, + &Type{A: 43}, +} +``` + +```go +// Хорошо: +good := map[Type1]*Type2{ + {A: 1}: {B: 2}, + {A: 3}: {B: 4}, +} +``` + +```go +// Плохо: +repetitive := map[Type1]*Type2{ + Type1{A: 1}: &Type2{B: 2}, + Type1{A: 3}: &Type2{B: 4}, +} +``` + +**Совет:** Если вы хотите удалить повторяющиеся имена типов в литералах +структур, вы можете запустить `gofmt -s`. + +<a id="literal-zero-value-fields"></a> + +#### Поля с нулевыми значениями + +[Нулевые] поля могут быть опущены в литералах структур, когда ясность не +теряется в результате. + +Хорошо спроектированные API часто используют конструкцию с нулевыми значениями +для улучшенной читаемости. Например, опускание трех полей с нулевыми значениями +из следующей структуры привлекает внимание к единственной опции, которая +указана. + +[Нулевые]: https://golang.org/ref/spec#The_zero_value + +```go +// Плохо: +import ( + "github.com/golang/leveldb" + "github.com/golang/leveldb/db" +) + +ldb := leveldb.Open("/my/table", &db.Options{ + BlockSize: 1<<16, + ErrorIfDBExists: true, + + // Все эти поля имеют свои нулевые значения. + BlockRestartInterval: 0, + Comparer: nil, + Compression: nil, + FileSystem: nil, + FilterPolicy: nil, + MaxOpenFiles: 0, + WriteBufferSize: 0, + VerifyChecksums: false, +}) +``` + +```go +// Хорошо: +import ( + "github.com/golang/leveldb" + "github.com/golang/leveldb/db" +) + +ldb := leveldb.Open("/my/table", &db.Options{ + BlockSize: 1<<16, + ErrorIfDBExists: true, +}) +``` + +Структуры в табличных тестах часто выигрывают от [явных имен полей], особенно +когда тестовая структура нетривиальна. Это позволяет автору опускать поля с +нулевыми значениями полностью, когда рассматриваемые поля не относятся к +тестовому случаю. Например, успешные тестовые случаи должны опускать любые поля, +связанные с ошибками или неудачами. В случаях, когда нулевое значение +необходимо для понимания тестового случая, таких как тестирование нулевых или +`nil` входных данных, имена полей должны быть указаны. + +[явных имен полей]: #literal-field-names + +**Лаконично** + +```go +tests := []struct { + input string + wantPieces []string + wantErr error +}{ + { + input: "1.2.3.4", + wantPieces: []string{"1", "2", "3", "4"}, + }, + { + input: "hostname", + wantErr: ErrBadHostname, + }, +} +``` + +**Явно** + +```go +tests := []struct { + input string + wantIPv4 bool + wantIPv6 bool + wantErr bool +}{ + { + input: "1.2.3.4", + wantIPv4: true, + wantIPv6: false, + }, + { + input: "1:2::3:4", + wantIPv4: false, + wantIPv6: true, + }, + { + input: "hostname", + wantIPv4: false, + wantIPv6: false, + wantErr: true, + }, +} +``` + +<a id="nil-slices"></a> + +### Nil-срезы + +Для большинства целей нет функциональной разницы между `nil` и пустым срезом. +Встроенные функции, такие как `len` и `cap`, ведут себя ожидаемо на `nil` +срезах. + +```go +// Хорошо: +import "fmt" + +var s []int // nil + +fmt.Println(s) // [] +fmt.Println(len(s)) // 0 +fmt.Println(cap(s)) // 0 +for range s {...} // нет операции + +s = append(s, 42) +fmt.Println(s) // [42] +``` + +Если вы объявляете пустой срез как локальную переменную (особенно если она может +быть источником возвращаемого значения), предпочитайте инициализацию nil, чтобы +снизить риск ошибок со стороны вызывающих. + +```go +// Хорошо: +var t []string +``` + +```go +// Плохо: +t := []string{} +``` + +Не создавайте API, которые вынуждают своих клиентов делать различие между nil и +пустым срезом. + +```go +// Хорошо: +// Ping проверяет доступность своих целей. +// Возвращает хосты, которые успешно ответили. +func Ping(hosts []string) ([]string, error) { ... } +``` + +```go +// Плохо: +// Ping проверяет доступность своих целей и возвращает список хостов, +// которые успешно ответили. Может быть пустым, если вход был пустым. +// nil означает, что произошла системная ошибка. +func Ping(hosts []string) []string { ... } +``` + +При проектировании интерфейсов избегайте различия между `nil` срезом и ненулевым +срезом нулевой длины, так как это может привести к тонким программным ошибкам. +Обычно это достигается с помощью `len` для проверки на пустоту, а не `== nil`. + +Эта реализация принимает как `nil`, так и срезы нулевой длины как "пустые": + +```go +// Хорошо: +// describeInts описывает s с заданным префиксом, если s не пуст. +func describeInts(prefix string, s []int) { + if len(s) == 0 { + return + } + fmt.Println(prefix, s) +} +``` + +Вместо того чтобы полагаться на различие как часть API: + +```go +// Плохо: +func maybeInts() []int { /* ... */ } + +// describeInts описывает s с заданным префиксом; передайте nil, чтобы полностью пропустить. +func describeInts(prefix string, s []int) { + // Поведение этой функции непреднамеренно меняется в зависимости от того, что + // возвращает maybeInts() в 'пустых' случаях (nil или []int{}). + if s == nil { + return + } + fmt.Println(prefix, s) +} + +describeInts("Here are some ints:", maybeInts()) +``` + +См. [ошибки "в потоке"] для дальнейшего обсуждения. + +[ошибки "в потоке"]: #in-band-errors + +<a id="indentation-confusion"></a> + +### Путаница с отступами + +Избегайте введения разрыва строки, если это приведет к выравниванию остальной +части строки с вложенным блоком кода. Если этого невозможно избежать, оставьте +пробел, чтобы отделить код в блоке от перенесенной строки. + +```go +// Плохо: +if longCondition1 && longCondition2 && + // Условия 3 и 4 имеют тот же отступ, что и код внутри if. + longCondition3 && longCondition4 { + log.Info("all conditions met") +} +``` + +См. следующие разделы для конкретных рекомендаций и примеров: + +* [Форматирование функций](#func-formatting) +* [Условные операторы и циклы](#conditional-formatting) +* [Форматирование литералов](#literal-formatting) + +<a id="func-formatting"></a> + +### Форматирование функций + +Сигнатура объявления функции или метода должна оставаться на одной строке, чтобы +избежать [путаницы с отступами](#indentation-confusion). + +Списки аргументов функций могут создавать одни из самых длинных строк в файле +исходного кода Go. Однако они предшествуют изменению отступа, и поэтому трудно +разорвать строку так, чтобы последующие строки не выглядели частью тела функции +запутанным образом: + +```go +// Плохо: +func (r *SomeType) SomeLongFunctionName(foo1, foo2, foo3 string, + foo4, foo5, foo6 int) { + foo7 := bar(foo1) + // ... +} +``` + +См. [лучшие практики](https://neonxp.ru/pages/gostyleguide/google/best-practices/#funcargs) для нескольких вариантов +сокращения мест вызовов функций, которые в противном случае имели бы много +аргументов. + +Строки часто можно сократить, вынося локальные переменные. + +```go +// Хорошо: +local := helper(some, parameters, here) +good := foo.Call(list, of, parameters, local) +``` + +Аналогично, вызовы функций и методов не должны разделяться исключительно на +основе длины строки. + +```go +// Хорошо: +good := foo.Call(long, list, of, parameters, all, on, one, line) +``` + +```go +// Плохо: +bad := foo.Call(long, list, of, parameters, + with, arbitrary, line, breaks) +``` + +По возможности избегайте добавления встроенных комментариев к конкретным +аргументам функций. Вместо этого используйте [структуру опций (option +struct)](https://neonxp.ru/pages/gostyleguide/google/best-practices/#option-structure) или добавьте больше деталей в +документацию функции. + +```go +// Хорошо: +good := server.New(ctx, server.Options{Port: 42}) +``` + +```go +// Плохо: +bad := server.New( + ctx, + 42, // Port +) +``` + +Если API нельзя изменить или если локальный вызов необычен (независимо от того, +слишком длинный вызов или нет), всегда допустимо добавлять разрывы строк, если +это помогает понять вызов. + +```go +// Хорошо: +canvas.RenderHeptagon(fillColor, + x0, y0, vertexColor0, + x1, y1, vertexColor1, + x2, y2, vertexColor2, + x3, y3, vertexColor3, + x4, y4, vertexColor4, + x5, y5, vertexColor5, + x6, y6, vertexColor6, +) +``` + +Обратите внимание, что строки в приведенном выше примере не переносятся на +определенную границу столбца, а группируются на основе координат вершин и цвета. + +Длинные строковые литералы внутри функций не должны разбиваться ради длины +строки. Для функций, включающих такие строки, разрыв строки можно добавить +после формата строки, а аргументы могут быть предоставлены на следующей или +последующих строках. Решение о том, где должны быть разрывы строк, лучше всего +принимать на основе семантических групп входных данных, а не исключительно на +основе длины строки. + +```go +// Хорошо: +log.Warningf("Database key (%q, %d, %q) incompatible in transaction started by (%q, %d, %q)", + currentCustomer, currentOffset, currentKey, + txCustomer, txOffset, txKey) +``` + +```go +// Плохо: +log.Warningf("Database key (%q, %d, %q) incompatible in"+ + " transaction started by (%q, %d, %q)", + currentCustomer, currentOffset, currentKey, txCustomer, + txOffset, txKey) +``` + +<a id="conditional-formatting"></a> + +### Условные операторы и циклы + +Оператор `if` не должен разбиваться на строки; многострочные условия `if` могут +привести к [путанице с отступами](#indentation-confusion). + +```go +// Плохо: +// Второй оператор if выровнен с кодом внутри блока if, что вызывает +// путаницу с отступами. +if db.CurrentStatusIs(db.InTransaction) && + db.ValuesEqual(db.TransactionKey(), row.Key()) { + return db.Errorf(db.TransactionError, "query failed: row (%v): key does not match transaction key", row) +} +``` + +Если короткое замыкание не требуется, булевы операнды могут быть извлечены +напрямую: + +```go +// Хорошо: +inTransaction := db.CurrentStatusIs(db.InTransaction) +keysMatch := db.ValuesEqual(db.TransactionKey(), row.Key()) +if inTransaction && keysMatch { + return db.Error(db.TransactionError, "query failed: row (%v): key does not match transaction key", row) +} +``` + +Также могут быть другие локальные переменные, которые можно извлечь, особенно +если условие уже повторяется: + +```go +// Хорошо: +uid := user.GetUniqueUserID() +if db.UserIsAdmin(uid) || db.UserHasPermission(uid, perms.ViewServerConfig) || db.UserHasPermission(uid, perms.CreateGroup) { + // ... +} +``` + +```go +// Плохо: +if db.UserIsAdmin(user.GetUniqueUserID()) || db.UserHasPermission(user.GetUniqueUserID(), perms.ViewServerConfig) || db.UserHasPermission(user.GetUniqueUserID(), perms.CreateGroup) { + // ... +} +``` + +Операторы `if`, содержащие замыкания или многострочные литералы структур, должны +обеспечить, чтобы [скобки совпадали](#literal-matching-braces), чтобы избежать +[путаницы с отступами](#indentation-confusion). + +```go +// Хорошо: +if err := db.RunInTransaction(func(tx *db.TX) error { + return tx.Execute(userUpdate, x, y, z) +}); err != nil { + return fmt.Errorf("user update failed: %s", err) +} +``` + +```go +// Хорошо: +if _, err := client.Update(ctx, &upb.UserUpdateRequest{ + ID: userID, + User: user, +}); err != nil { + return fmt.Errorf("user update failed: %s", err) +} +``` + +Аналогично, не пытайтесь вставлять искусственные разрывы строк в операторы +`for`. Вы всегда можете позволить строке быть просто длинной, если нет +элегантного способа ее рефакторить: + +```go +// Хорошо: +for i, max := 0, collection.Size(); i < max && !collection.HasPendingWriters(); i++ { + // ... +} +``` + +Однако часто он есть: + +```go +// Хорошо: +for i, max := 0, collection.Size(); i < max; i++ { + if collection.HasPendingWriters() { + break + } + // ... +} +``` + +Операторы `switch` и `case` также должны оставаться на одной строке. + +```go +// Хорошо: +switch good := db.TransactionStatus(); good { +case db.TransactionStarting, db.TransactionActive, db.TransactionWaiting: + // ... +case db.TransactionCommitted, db.NoTransaction: + // ... +default: + // ... +} +``` + +```go +// Плохо: +switch bad := db.TransactionStatus(); bad { +case db.TransactionStarting, + db.TransactionActive, + db.TransactionWaiting: + // ... +case db.TransactionCommitted, + db.NoTransaction: + // ... +default: + // ... +} +``` + +Если строка чрезмерно длинная, сделайте отступ для всех вариантов (`case`) и +отделите их пустой строкой, чтобы избежать [путаницы с +отступами](#indentation-confusion): + +```go +// Хорошо: +switch db.TransactionStatus() { +case + db.TransactionStarting, + db.TransactionActive, + db.TransactionWaiting, + db.TransactionCommitted: + + // ... +case db.NoTransaction: + // ... +default: + // ... +} +``` + +В условных выражениях, сравнивающих переменную с константой, помещайте значение +переменной слева от оператора равенства: + +```go +// Хорошо: +if result == "foo" { + // ... +} +``` + +Вместо менее ясной формулировки, где константа идет первой (["условия в стиле +Йоды"](https://en.wikipedia.org/wiki/Yoda_conditions)): + +```go +// Плохо: +if "foo" == result { + // ... +} +``` + +<a id="copying"></a> + +### Копирование + +<a id="TOC-Copying"></a> + +Чтобы избежать неожиданного псевдонимного доступа (aliasing) и подобных ошибок, +будьте осторожны при копировании структуры из другого пакета. Например, объекты +синхронизации, такие как `sync.Mutex`, не должны копироваться. + +Тип `bytes.Buffer` содержит срез `[]byte` и, в качестве оптимизации для +небольших строк, небольшой массив байтов, на который может ссылаться срез. Если +вы скопируете `Buffer`, срез в копии может стать псевдонимом массива в +оригинале, вызывая неожиданные эффекты при последующих вызовах методов. + +В целом, не копируйте значение типа `T`, если его методы ассоциированы с +указательным типом, `*T`. + +```go +// Плохо: +b1 := bytes.Buffer{} +b2 := b1 +``` + +Вызов метода, принимающего получателя по значению, может скрыть копирование. +Когда вы создаете API, вам обычно следует принимать и возвращать типы-указатели, +если ваши структуры содержат поля, которые не должны копироваться. + +Эти примеры допустимы: + +```go +// Хорошо: +type Record struct { + buf bytes.Buffer + // другие поля опущены +} + +func New() *Record {...} + +func (r *Record) Process(...) {...} + +func Consumer(r *Record) {...} +``` + +Но эти обычно ошибочны: + +```go +// Плохо: +type Record struct { + buf bytes.Buffer + // другие поля опущены +} + + +func (r Record) Process(...) {...} // Создает копию r.buf + +func Consumer(r Record) {...} // Создает копию r.buf +``` + +Эти рекомендации также применимы к копированию `sync.Mutex`. + +<a id="dont-panic"></a> + +### Не используйте panic + +<a id="TOC-Don-t-Panic"></a> + +Не используйте `panic` для обычной обработки ошибок. Вместо этого используйте +`error` и множественные возвращаемые значения. См. [раздел Effective Go об +ошибках]. + +В `package main` и коде инициализации рассмотрите [`log.Exit`] для ошибок, +которые должны завершать программу (например, неверная конфигурация), поскольку +во многих из этих случаев трассировка стека не поможет читателю. Обратите +внимание, что [`log.Exit`] вызывает [`os.Exit`] и отложенные (deferred) функции +не будут выполнены. + +Для ошибок, указывающих на "невозможные" условия, а именно на ошибки, которые +всегда должны быть обнаружены при проверке кода и/или тестировании, функция +может разумно вернуть ошибку или вызвать [`log.Fatal`]. + +Также см. [когда panic допустим](https://neonxp.ru/pages/gostyleguide/google/best-practices/.md#when-to-panic). + +**Примечание:** `log.Fatalf` здесь не из стандартной библиотеки log. См. +[#logging]. + +[раздел Effective Go об ошибках]: http://golang.org/doc/effective_go.html#errors +[`os.Exit`]: https://pkg.go.dev/os#Exit + +<a id="must-functions"></a> + +### Функции Must + +Вспомогательные функции настройки, которые останавливают программу при неудаче, +следуют соглашению об именовании `MustXYZ` (или `mustXYZ`). Как правило, их +следует вызывать только на раннем этапе запуска программы, а не для таких вещей, +как пользовательский ввод, где предпочтительна обычная обработка ошибок Go. + +Это часто возникает для функций, вызываемых для инициализации переменных уровня +пакета исключительно во время [инициализации +пакета](https://golang.org/ref/spec#Package_initialization) (например, +[template.Must](https://golang.org/pkg/text/template/#Must) и +[regexp.MustCompile](https://golang.org/pkg/regexp/#MustCompile)). + +```go +// Хорошо: +func MustParse(version string) *Version { + v, err := Parse(version) + if err != nil { + panic(fmt.Sprintf("MustParse(%q) = _, %v", version, err)) + } + return v +} + +// "Константа" уровня пакета. Если бы мы хотели использовать `Parse`, нам пришлось бы +// устанавливать значение в `init`. +var DefaultVersion = MustParse("1.2.3") +``` + +То же соглашение может использоваться во вспомогательных функциях тестирования, +которые останавливают только текущий тест (используя `t.Fatal`). Такие помощники +часто удобны при создании тестовых значений, например, в полях структур +[табличных тестов](#table-driven-tests), поскольку функции, возвращающие ошибки, +не могут быть напрямую присвоены полю структуры. + +```go +// Хорошо: +func mustMarshalAny(t *testing.T, m proto.Message) *anypb.Any { + t.Helper() + any, err := anypb.New(m) + if err != nil { + t.Fatalf("mustMarshalAny(t, m) = %v; want %v", err, nil) + } + return any +} + +func TestCreateObject(t *testing.T) { + tests := []struct{ + desc string + data *anypb.Any + }{ + { + desc: "my test case", + // Создание значений непосредственно внутри тестовых случаев табличного теста. + data: mustMarshalAny(t, mypb.Object{}), + }, + // ... + } + // ... +} +``` + +В обоих этих случаях ценность этого шаблона в том, что помощники могут быть +вызваны в "знаковом" контексте. Этих помощников не следует вызывать в местах, +где трудно обеспечить перехват ошибки, или в контексте, где ошибка должна быть +[проверена](#handle-errors) (например, во многих обработчиках запросов). Для +постоянных входных данных это позволяет тестам легко убедиться, что аргументы +`Must` корректны, а для непостоянных входных данных это позволяет тестам +проверять, что ошибки [правильно обработаны или +распространены](https://neonxp.ru/pages/gostyleguide/google/best-practices/#error-handling). + +Где функции `Must` используются в тесте, их обычно следует [помечать как +тестовый помощник](#mark-test-helpers) и вызывать `t.Fatal` при ошибке (см. +[обработка ошибок во вспомогательных тестовых +функциях](https://neonxp.ru/pages/gostyleguide/google/best-practices/#test-helper-error-handling) для дополнительных +соображений по использованию этого). + +Их не следует использовать, когда [обычная обработка +ошибок](https://neonxp.ru/pages/gostyleguide/google/best-practices/#error-handling) возможна (включая некоторый рефакторинг): + +```go +// Плохо: +func Version(o *servicepb.Object) (*version.Version, error) { + // Вернуть ошибку вместо использования функций Must. + v := version.MustParse(o.GetVersionString()) + return dealiasVersion(v) +} +``` + +<a id="goroutine-lifetimes"></a> + +### Время жизни горутин + +<a id="TOC-GoroutineLifetimes"></a> + +Когда вы порождаете горутины, сделайте ясным, когда или завершаются ли они. + +Горутины могут "подтекать", блокируясь на отправке или получении по каналу. +Сборщик мусора не завершит горутину, заблокированную на канале, даже если +никакая другая горутина не имеет ссылки на этот канал. + +Даже когда горутины не подтекают, оставлять их активными, когда они больше не +нужны, может вызвать другие тонкие и трудные для диагностики проблемы. Отправка +в закрытый канал вызывает панику. + +```go +// Плохо: +ch := make(chan int) +ch <- 42 +close(ch) +ch <- 13 // panic +``` + +Изменение все еще используемых входных данных "после того, как результат не +нужен", может привести к гонкам данных. Оставление горутин активными на +произвольное время может привести к непредсказуемому использованию памяти. + +Параллельный код должен быть написан так, чтобы время жизни горутин было +очевидным. Обычно это означает сохранение кода, связанного с синхронизацией, в +пределах области видимости функции и вынесение логики в [синхронные функции]. +Если параллелизм все еще не очевиден, важно задокументировать, когда и почему +горутины завершаются. + +Код, следующий лучшим практикам использования контекста, часто помогает +прояснить это. Обычно это управляется с помощью [`context.Context`]: + +```go +// Хорошо: +func (w *Worker) Run(ctx context.Context) error { + var wg sync.WaitGroup + // ... + for item := range w.q { + // process возвращается самое позднее при отмене контекста. + wg.Add(1) + go func() { + defer wg.Done() + process(ctx, item) + }() + } + // ... + wg.Wait() // Предотвращает переживание порожденными горутинами этой функции. +} +``` + +Существуют другие варианты вышеизложенного, использующие простые сигнальные +каналы, такие как `chan struct{}`, синхронизированные переменные, [условные +переменные][rethinking-slides] и многое другое. Важная часть заключается в том, +что конец горутины очевиден для последующих сопровождающих. + +В отличие от этого, следующий код небрежен в отношении того, когда его +порожденные горутины завершаются: + +```go +// Плохо: +func (w *Worker) Run() { + // ... + for item := range w.q { + // process возвращается, когда завершается, если вообще завершится, возможно, не обрабатывая корректно + // переход состояния или завершение самой программы Go. + go process(item) + } + // ... +} +``` + +Этот код может выглядеть нормально, но у него есть несколько скрытых проблем: + +* Код, вероятно, имеет неопределенное поведение в продакшене, и программа + может не завершиться чисто, даже если операционная система освободит + ресурсы. + +* Код трудно осмысленно протестировать из-за неопределенного жизненного цикла. + +* Код может подтекать ресурсами, как описано выше. + +См. также: + +* [Никогда не запускайте горутину, не зная, как она остановится][cheney-stop] +* Переосмысление классических паттернов параллелизма: + [слайды][rethinking-slides], [видео][rethinking-video] +* [Когда программы на Go завершаются] +* [Соглашения по документации: Контексты] + +[синхронные функции]: #synchronous-functions +[cheney-stop]: + https://dave.cheney.net/2016/12/22/never-start-a-goroutine-without-knowing-how-it-will-stop +[rethinking-slides]: + https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view +[rethinking-video]: https://www.youtube.com/watch?v=5zXAHh5tJqQ +[Когда программы на Go завершаются]: https://changelog.com/gotime/165 +[Соглашения по документации: Контексты]: + best-practices.md#documentation-conventions-contexts + +<a id="interfaces"></a> + +### Интерфейсы + +<a id="TOC-Interfaces"></a> + +Интерфейсы Go обычно принадлежат пакету, который *потребляет* значения типа +интерфейса, а не пакету, который *реализует* тип интерфейса. Реализующий пакет +должен возвращать конкретные (обычно указатель или структура) типы. Таким +образом, новые методы могут быть добавлены к реализациям без необходимости +обширного рефакторинга. Подробнее см. [GoTip #49: Принимайте интерфейсы, +возвращайте конкретные типы]. + +Не экспортируйте [тестовый дубль (test double)][double types] реализации +интерфейса из API, который его потребляет. Вместо этого спроектируйте API так, +чтобы его можно было тестировать с помощью [публичного API] [реальной +реализации]. См. [GoTip #42: Создание заглушки для тестирования] для +подробностей. Даже когда нецелесообразно использовать реальную реализацию, может +не потребоваться вводить интерфейс, полностью покрывающий все методы в реальном +типе; потребитель может создать интерфейс, содержащий только нужные ему методы, +как показано в [GoTip #78: Минимально жизнеспособные интерфейсы]. + +Для тестирования пакетов, использующих Stubby RPC клиенты, используйте реальное +клиентское соединение. Если реальный сервер не может быть запущен в тесте, +внутренняя практика Google — получить реальное клиентское соединение с локальным +[тестовым дублем] с использованием внутреннего пакета rpctest (скоро будет!). + +Не определяйте интерфейсы до того, как они используются (см. [TotT: Code +Health: Eliminate YAGNI Smells][tott-438] ). Без реалистичного примера +использования слишком сложно увидеть, нужен ли интерфейс вообще, не говоря уже о +том, какие методы он должен содержать. + +Не используйте параметры типа интерфейса, если пользователям пакета не нужно +передавать различные типы для них. + +Не экспортируйте интерфейсы, которые не нужны пользователям пакета. + +**TODO:** Написать более подробный документ об интерфейсах и дать на него ссылку +здесь. + +[GoTip #42: Создание заглушки для тестирования]: + https://google.github.io/styleguide/go/index.html#gotip +[GoTip #49: Принимайте интерфейсы, возвращайте конкретные типы]: + https://google.github.io/styleguide/go/index.html#gotip +[GoTip #78: Минимально жизнеспособные интерфейсы]: + https://google.github.io/styleguide/go/index.html#gotip +[реальной реализации]: best-practices#use-real-transports +[публичному API]: + https://abseil.io/resources/swe-book/html/ch12.html#test_via_public_apis +[double types]: + https://abseil.io/resources/swe-book/html/ch13.html#techniques_for_using_test_doubles +[тестовым дублем]: + https://abseil.io/resources/swe-book/html/ch13.html#basic_concepts +[tott-438]: + https://testing.googleblog.com/2017/08/code-health-eliminate-yagni-smells.html + +```go +// Хорошо: +package consumer // consumer.go + +type Thinger interface { Thing() bool } + +func Foo(t Thinger) string { ... } +``` + +```go +// Хорошо: +package consumer // consumer_test.go + +type fakeThinger struct{ ... } +func (t fakeThinger) Thing() bool { ... } +... +if Foo(fakeThinger{...}) == "x" { ... } +``` + +```go +// Плохо: +package producer + +type Thinger interface { Thing() bool } + +type defaultThinger struct{ ... } +func (t defaultThinger) Thing() bool { ... } + +func NewThinger() Thinger { return defaultThinger{ ... } } +``` + +```go +// Хорошо: +package producer + +type Thinger struct{ ... } +func (t Thinger) Thing() bool { ... } + +func NewThinger() Thinger { return Thinger{ ... } } +``` + +<a id="generics"></a> + +### Дженерики (Generics) + +Дженерики (формально называемые "[Параметрами типа]") разрешены там, где они +удовлетворяют вашим бизнес-требованиям. Во многих приложениях обычный подход с +использованием существующих языковых возможностей (срезы, карты, интерфейсы и +т.д.) работает так же хорошо без добавленной сложности, поэтому будьте осторожны +с преждевременным использованием. См. обсуждение [наименьшего механизма (least +mechanism)](https://neonxp.ru/pages/gostyleguide/google/guide/#least-mechanism). + +При введении экспортируемого API, использующего дженерики, убедитесь, что он +должным образом задокументирован. Настоятельно рекомендуется включать +мотивирующие исполняемые [примеры]. + +Не используйте дженерики только потому, что вы реализуете алгоритм или структуру +данных, которой не важен тип элементов. Если на практике инстанцируется только +один тип, начните с того, чтобы ваш код работал с этим типом без использования +дженериков вообще. Добавить полиморфизм позже будет проще по сравнению с +удалением абстракции, которая оказывается ненужной. + +Не используйте дженерики для создания предметно-ориентированных языков (DSL). В +частности, воздержитесь от введения фреймворков обработки ошибок, которые могут +значительно обременять читателей. Вместо этого предпочитайте установленные +методы [обработки ошибок](#errors). Для тестирования особенно остерегайтесь +введения [библиотек утверждений (assertion libraries)](#assert) или фреймворков, +которые приводят к менее полезным [сообщениям об ошибках +тестов](#useful-test-failures). + +В общем: + +* [Пишите код, не проектируйте типы]. Из выступления на GopherCon от Роберта + Грисемера и Яна Лэнса Тейлора. +* Если у вас есть несколько типов, которые разделяют полезный объединяющий + интерфейс, рассмотрите моделирование решения с использованием этого + интерфейса. Дженерики могут не понадобиться. +* В противном случае вместо того, чтобы полагаться на тип `any` и чрезмерное + [переключение типов (type switching)](https://tour.golang.org/methods/16), + рассмотрите дженерики. + +См. также: + +* [Использование дженериков в Go], выступление Яна Лэнса Тейлора + +* [Учебник по дженерикам] на сайте Go + +[Учебник по дженерикам]: https://go.dev/doc/tutorial/generics +[Параметрами типа]: https://go.dev/design/43651-type-parameters +[Использование дженериков в Go]: https://www.youtube.com/watch?v=nr8EpUO9jhw +[Пишите код, не проектируйте типы]: + https://www.youtube.com/watch?v=Pa_e9EeCdy8&t=1250s + +<a id="pass-values"></a> + +### Передача по значению + +<a id="TOC-PassValues"></a> + +Не передавайте указатели в качестве аргументов функций только для экономии +нескольких байтов. Если функция читает свой аргумент `x` только как `*x` на всем +протяжении, то аргумент не должен быть указателем. Распространенные примеры +этого включают передачу указателя на строку (`*string`) или указателя на +значение интерфейса (`*io.Reader`). В обоих случаях само значение имеет +фиксированный размер и может быть передано напрямую. + +Этот совет не применим к большим структурам или даже малым структурам, которые +могут увеличиться в размере. В частности, сообщения protocol buffer обычно +следует обрабатывать по указателю, а не по значению. Тип-указатель удовлетворяет +интерфейсу `proto.Message` (принимаемому `proto.Marshal`, `protocmp.Transform`, +и т.д.), а сообщения protocol buffer могут быть довольно большими и часто +увеличиваются со временем. + +<a id="receiver-type"></a> + +### Тип получателя (Receiver) + +<a id="TOC-ReceiverType"></a> + +[Получатель метода] может быть передан либо по значению, либо по указателю, как +если бы он был обычным параметром функции. Выбор между ними основан на том, в +составе какого [набора методов] метод должен находиться. + +[Получатель метода]: https://golang.org/ref/spec#Method_declarations +[набора методов]: https://golang.org/ref/spec#Method_sets + +**Корректность важнее скорости или простоты.** Бывают случаи, когда вы должны +использовать значение-указатель. В других случаях выбирайте указатели для +больших типов или в качестве будущего доказательства, если у вас нет хорошего +понимания того, как будет расти код, и используйте значения для простых [простых +старых данных]. + +Список ниже подробно описывает каждый случай: + +* Если получатель является срезом и метод не переслаивает (reslice) и не + перераспределяет (reallocate) срез, используйте значение, а не указатель. + + ```go + // Хорошо: + type Buffer []byte + + func (b Buffer) Len() int { return len(b) } + ``` + +* Если методу нужно мутировать получателя, получатель должен быть указателем. + + ```go + // Хорошо: + type Counter int + + func (c *Counter) Inc() { *c++ } + + // См. https://pkg.go.dev/container/heap. + type Queue []Item + + func (q *Queue) Push(x Item) { *q = append([]Item{x}, *q...) } + ``` + +* Если получатель является структурой, содержащей поля, которые [не могут быть + безопасно скопированы](#copying), используйте получатель-указатель. + Распространенные примеры — [`sync.Mutex`] и другие типы синхронизации. + + ```go + // Хорошо: + type Counter struct { + mu sync.Mutex + total int + } + + func (c *Counter) Inc() { + c.mu.Lock() + defer c.mu.Unlock() + c.total++ + } + ``` + + **Совет:** Проверьте [Godoc] типа для информации о том, безопасно ли или + небезопасно его копировать. + +* Если получатель является "большой" структурой или массивом, + получатель-указатель может быть более эффективным. Передача структуры + эквивалентна передаче всех ее полей или элементов в качестве аргументов + методу. Если это кажется слишком большим для [передачи по + значению](#pass-values), указатель — хороший выбор. + +* Для методов, которые будут вызываться или выполняться параллельно с другими + функциями, которые изменяют получателя, используйте значение, если эти + изменения не должны быть видны вашему методу; в противном случае используйте + указатель. + +* Если получатель является структурой или массивом, любой элемент которого + является указателем на что-то, что может быть изменено, предпочтите + получателя-указателя, чтобы сделать намерение изменяемости понятным для + читателя. + + ```go + // Хорошо: + type Counter struct { + m *Metric + } + + func (c *Counter) Inc() { + c.m.Add(1) + } + ``` + +* Если получатель является [встроенным типом], таким как целое число или + строка, который не нужно изменять, используйте значение. + + ```go + // Хорошо: + type User string + + func (u User) String() { return string(u) } + ``` + +* Если получатель является картой (map), функцией или каналом, используйте + значение, а не указатель. + + ```go + // Хорошо: + // См. https://pkg.go.dev/net/http#Header. + type Header map[string][]string + + func (h Header) Add(key, value string) { /* опущено */ } + ``` + +* Если получатель является "маленьким" массивом или структурой, которые + естественным образом являются типом значения без изменяемых полей и без + указателей, получатель по значению обычно является правильным выбором. + + ```go + // Хорошо: + // См. https://pkg.go.dev/time#Time. + type Time struct { /* опущено */ } + + func (t Time) Add(d Duration) Time { /* опущено */ } + ``` + +* В случае сомнений используйте получатель-указатель. + +В качестве общего руководства предпочитайте, чтобы методы для типа были либо все +указательными методами, либо всеми методами по значению. + +**Примечание:** Существует много дезинформации о том, может ли передача значения +или указателя в функцию повлиять на производительность. Компилятор может выбрать +передачу указателей на значения в стеке, а также копирование значений в стеке, +но эти соображения не должны перевешивать читаемость и корректность кода в +большинстве обстоятельств. Когда производительность действительно имеет +значение, важно профилировать оба подхода с реалистичным бенчмарком, прежде чем +решать, что один подход превосходит другой. + +[простых старых данных]: https://en.wikipedia.org/wiki/Passive_data_structure +[`sync.Mutex`]: https://pkg.go.dev/sync#Mutex +[встроенным типом]: https://pkg.go.dev/builtin + +<a id="switch-break"></a> + +### `switch` и `break` + +<a id="TOC-SwitchBreak"></a> + +Не используйте операторы `break` без целевых меток в конце предложений `switch`; +они избыточны. В отличие от C и Java, предложения `switch` в Go автоматически +завершаются, и для достижения поведения в стиле C требуется оператор +`fallthrough`. Используйте комментарий, а не `break`, если хотите прояснить цель +пустого предложения. + +```go +// Хорошо: +switch x { +case "A", "B": + buf.WriteString(x) +case "C": + // обрабатывается вне оператора switch +default: + return fmt.Errorf("unknown value: %q", x) +} +``` + +```go +// Плохо: +switch x { +case "A", "B": + buf.WriteString(x) + break // этот break избыточен +case "C": + break // этот break избыточен +default: + return fmt.Errorf("unknown value: %q", x) +} +``` + +> **Примечание:** Если предложение `switch` находится внутри цикла `for`, +> использование `break` внутри `switch` не выходит из охватывающего цикла `for`. +> +> ```go +> for { +> switch x { +> case "A": +> break // выходит из switch, не из цикла +> } +> } +> ``` +> +> Чтобы выйти из охватывающего цикла, используйте метку на операторе `for`: +> +> ```go +> loop: +> for { +> switch x { +> case "A": +> break loop // выходит из цикла +> } +> } +> ``` + +<a id="synchronous-functions"></a> + +### Синхронные функции + +<a id="TOC-SynchronousFunctions"></a> + +Синхронные функции возвращают свои результаты напрямую и завершают любые +обратные вызовы или операции с каналами перед возвратом. Предпочитайте +синхронные функции асинхронным. + +Синхронные функции сохраняют горутины локализованными внутри вызова. Это +помогает рассуждать об их времени жизни и избегать утечек и гонок данных. +Синхронные функции также легче тестировать, поскольку вызывающая сторона может +передать входные данные и проверить выходные без необходимости опроса или +синхронизации. + +При необходимости вызывающая сторона может добавить параллелизм, вызвав функцию +в отдельной горутине. Однако довольно сложно (иногда невозможно) убрать ненужный +параллелизм на стороне вызывающего. + +См. также: + +* "Переосмысление классических паттернов параллелизма", выступление Брайана + Миллса: [слайды][rethinking-slides], [видео][rethinking-video] + +<a id="type-aliases"></a> + +### Псевдонимы типов (Type aliases) + +<a id="TOC-TypeAliases"></a> + +Используйте *определение типа*, `type T1 T2`, чтобы определить новый тип. +Используйте [*псевдоним типа*], `type T1 = T2`, чтобы ссылаться на существующий +тип без определения нового типа. Псевдонимы типов редки; их основное +использование — помочь в переносе пакетов в новые места исходного кода. Не +используйте псевдонимы типов, когда это не нужно. + +[*псевдоним типа*]: http://golang.org/ref/spec#Type_declarations + +<a id="use-percent-q"></a> + +### Используйте %q + +<a id="TOC-UsePercentQ"></a> + +Функции форматирования Go (`fmt.Printf` и т.д.) имеют глагол `%q`, который +печатает строки внутри двойных кавычек. + +```go +// Хорошо: +fmt.Printf("value %q looks like English text", someText) +``` + +Предпочитайте использование `%q` вместо эквивалентного ручного способа с +использованием `%s`: + +```go +// Плохо: +fmt.Printf("value \"%s\" looks like English text", someText) +// Также избегайте вручную оборачивать строки одинарными кавычками: +fmt.Printf("value '%s' looks like English text", someText) +``` + +Использование `%q` рекомендуется в выводе, предназначенном для людей, где +входное значение может быть пустым или содержать управляющие символы. Тихую +пустую строку заметить очень сложно, но `""` явно выделяется как таковая. + +<a id="use-any"></a> + +### Используйте any + +Go 1.18 вводит тип `any` как [псевдоним] для `interface{}`. Поскольку это +псевдоним, `any` эквивалентен `interface{}` во многих ситуациях, а в других +легко взаимозаменяем через явное преобразование. Предпочитайте использовать +`any` в новом коде. + +[псевдоним]: + https://go.googlesource.com/proposal/+/master/design/18130-type-alias.md + +## Общие библиотеки + +<a id="flags"></a> + +### Флаги + +<a id="TOC-Flags"></a> + +Программы Go в кодовой базе Google используют внутренний вариант [стандартного +пакета `flag`]. Он имеет схожий интерфейс, но хорошо взаимодействует с +внутренними системами Google. Имена флагов в бинарных файлах Go должны +предпочитать использование подчеркиваний для разделения слов, хотя переменные, +хранящие значение флага, должны следовать стандартному стилю именования Go +([mixed caps]). Конкретно, имя флага должно быть в змеином регистре +(snake_case), а имя переменной должно быть эквивалентным именем в верблюжьем +регистре (camelCase). + +```go +// Хорошо: +var ( + pollInterval = flag.Duration("poll_interval", time.Minute, "Interval to use for polling.") +) +``` + +```go +// Плохо: +var ( + poll_interval = flag.Int("pollIntervalSeconds", 60, "Interval to use for polling in seconds.") +) +``` + +Флаги должны определяться только в `package main` или эквивалентном. + +Универсальные пакеты должны настраиваться с использованием API Go, а не путем +пробивания до интерфейса командной строки; не позволяйте импорту библиотеки +экспортировать новые флаги как побочный эффект. То есть предпочитайте явные +аргументы функций или присваивание полей структуры или, что гораздо реже и под +самым строгим контролем, экспортируемые глобальные переменные. В крайне редком +случае, когда необходимо нарушить это правило, имя флага должно четко указывать +пакет, который он настраивает. + +Если ваши флаги являются глобальными переменными, поместите их в свою +собственную группу `var`, следующую после раздела импортов. + +Существуют дополнительные обсуждения о лучших практиках создания [сложных CLI] с +подкомандами. + +См. также: + +* [Tip of the Week #45: Avoid Flags, Especially in Library Code][totw-45] +* [Go Tip #10: Configuration Structs and + Flags](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #80: Dependency Injection + Principles](https://google.github.io/styleguide/go/index.html#gotip) + +[стандартного пакета `flag`]: https://golang.org/pkg/flag/ +[mixed caps]: guide#mixed-caps +[сложных CLI]: best-practices#complex-clis +[totw-45]: https://abseil.io/tips/45 + +<a id="logging"></a> + +### Логирование + +Программы Go в кодовой базе Google используют вариант стандартного [`log`] +пакета. Он имеет схожий, но более мощный интерфейс и хорошо взаимодействует с +внутренними системами Google. Пакет с открытым исходным кодом этой библиотеки +доступен как [пакет `glog`], и проекты Google с открытым исходным кодом могут +использовать его, но в этом руководстве он повсюду упоминается как `log`. + +**Примечание:** Для аномальных завершений программы эта библиотека использует +`log.Fatal` для аварийного завершения с трассировкой стека и `log.Exit` для +остановки без нее. Здесь нет `log.Panic` функции, как в стандартной библиотеке. + +**Совет:** `log.Info(v)` эквивалентно `log.Infof("%v", v)`, и то же самое +относится к другим уровням логирования. Предпочитайте версию без форматирования, +когда вам нечего форматировать. + +См. также: + +* Лучшие практики по [логированию ошибок](https://neonxp.ru/pages/gostyleguide/google/best-practices/#error-logging) и + [пользовательским уровням детализации (verbosity + levels)](https://neonxp.ru/pages/gostyleguide/google/best-practices/#vlog) +* Когда и как использовать пакет log для [остановки + программы](https://neonxp.ru/pages/gostyleguide/google/best-practices/#checks-and-panics) + +[`log`]: https://pkg.go.dev/log +[`log/slog`]: https://pkg.go.dev/log/slog +[пакет `glog`]: https://pkg.go.dev/github.com/golang/glog +[`log.Exit`]: https://pkg.go.dev/github.com/golang/glog#Exit +[`log.Fatal`]: https://pkg.go.dev/github.com/golang/glog#Fatal + +<a id="contexts"></a> + +### Контексты (Contexts) + +<a id="TOC-Contexts"></a> + +Значения типа [`context.Context`] несут учетные данные безопасности, информацию +трассировки, сроки и сигналы отмены через границы API и процессов. В отличие от +C++ и Java, которые в кодовой базе Google используют локальное хранилище потоков +(thread-local storage), программы Go передают контексты явно вдоль всей цепочки +вызовов функций от входящих RPC и HTTP запросов к исходящим запросам. + +[`context.Context`]: https://pkg.go.dev/context + +При передаче в функцию или метод [`context.Context`] всегда является первым +параметром. + +```go +func F(ctx context.Context /* другие аргументы */) {} +``` + +Исключения: + +* В HTTP обработчике, где контекст берется из + [`req.Context()`](https://pkg.go.dev/net/http#Request.Context). +* В потоковых RPC методах, где контекст берется из потока. + + Код, использующий потоковый gRPC, получает контекст из метода `Context()` в + сгенерированном типе сервера, который реализует `grpc.ServerStream`. См. + [документацию по сгенерированному коду + gRPC](https://grpc.io/docs/languages/go/generated-code/). + +* В точках входа (entrypoint functions) (см. ниже примеры таких функций) + используйте [`context.Background()`] или, для тестов, + [`tb.Context()`](https://pkg.go.dev/testing#TB.Context). + + * В бинарных целях: `main` + * В общем коде и библиотеках: `init` + * В тестах: `TestXXX`, `BenchmarkXXX`, `FuzzXXX` + +> **Примечание**: Очень редко код в середине цепочки вызовов требует создания +> базового контекста самостоятельно с помощью [`context.Background()`]. Всегда +> предпочитайте брать контекст у вашего вызывающего, если это не неправильный +> контекст. +> +> Вы можете столкнуться с серверными библиотеками (реализация Stubby, gRPC или +> HTTP во фреймворке сервера Google для Go), которые создают новый объект +> контекста для каждого запроса. Эти контексты сразу заполняются информацией из +> входящего запроса, так что при передаче в обработчик запроса прикрепленные +> значения контекста были распространены к нему через сетевую границу от +> клиента-вызывающего. Более того, время жизни этих контекстов ограничено +> временем запроса: когда запрос завершен, контекст отменяется. +> +> Если вы не реализуете серверный фреймворк, вы не должны создавать контексты с +> помощью [`context.Background()`] в библиотечном коде. Вместо этого +> предпочитайте использование отсоединения контекста (context detachment), +> которое упоминается ниже, если доступен существующий контекст. Если вы +> думаете, что вам действительно нужно [`context.Background()`] вне функций +> точек входа, обсудите это со списком рассылки Google Go style, прежде чем +> приступать к реализации. + +Соглашение о том, что [`context.Context`] идет первым в функциях, также +применимо к тестовым помощникам. + +```go +// Хорошо: +func readTestFile(ctx context.Context, t *testing.T, path string) string {} +``` + +Не добавляйте поле context в тип структуры. Вместо этого добавьте параметр +context в каждый метод типа, которому нужно передавать его дальше. Единственное +исключение — для методов, чья сигнатура должна соответствовать интерфейсу в +стандартной библиотеке или в сторонней библиотеке вне контроля Google. Такие +случаи очень редки и должны быть обсуждены со списком рассылки Google Go style +до реализации и проверки на читаемость. + +**Примечание:** Go 1.24 добавил метод [`(testing.TB).Context()`]. В тестах +предпочитайте использовать [`(testing.TB).Context()`] вместо +[`context.Background()`] для предоставления начального [`context.Context`], +используемого тестом. Вспомогательные функции, настройка окружения или тестовых +дублей и другие функции, вызываемые из тела тестовой функции, которые требуют +контекст, должны иметь его явно переданным. + +[`(testing.TB).Context()`]: https://pkg.go.dev/testing#TB.Context + +Код в кодовой базе Google, который должен порождать фоновые операции, способные +выполняться после отмены родительского контекста, может использовать внутренний +пакет для отсоединения. Следите за [issue +#40221](https://github.com/golang/go/issues/40221) для обсуждений об +альтернативе с открытым исходным кодом. + +Поскольку контексты неизменяемы, нормально передавать один и тот же контекст в +несколько вызовов, которые разделяют один и тот же дедлайн, сигнал отмены, +учетные данные, родительскую трассировку и т.д. + +См. также: + +* [Контексты и структуры] + +[`context.Background()`]: https://pkg.go.dev/context/#Background +[Контексты и структуры]: https://go.dev/blog/context-and-structs + +<a id="custom-contexts"></a> + +#### Пользовательские контексты + +Не создавайте пользовательские типы контекстов и не используйте интерфейсы, +отличные от [`context.Context`], в сигнатурах функций. Для этого правила нет +исключений. + +Представьте, если бы каждая команда имела свой пользовательский контекст. Каждый +вызов функции из пакета `p` в пакет `q` должен был бы определять, как +преобразовать `p.Context` в `q.Context`, для всех пар пакетов `p` и `q`. Это +нецелесообразно и создает ошибки для людей, и это делает автоматизированные +рефакторинги, добавляющие параметры контекста, почти невозможными. + +Если у вас есть данные приложения для передачи, поместите их в параметр, в +получателе, в глобальных переменных или в значении `Context`, если они +действительно принадлежат там. Создание вашего собственного типа контекста +неприемлемо, поскольку оно подрывает способность команды Go заставить программы +Go правильно работать в продакшене. + +<a id="crypto-rand"></a> + +### crypto/rand + +<a id="TOC-CryptoRand"></a> + +Не используйте пакет `math/rand` для генерации ключей, даже одноразовых. Если не +задано начальное значение (seed), генератор полностью предсказуем. При задании +начального значения `time.Nanoseconds()` существует всего несколько бит +энтропии. Вместо этого используйте `Reader` из `crypto/rand`, и если вам нужен +текст, выводите в шестнадцатеричном или base64. + +```go +// Хорошо: +import ( + "crypto/rand" + // "encoding/base64" + // "encoding/hex" + "fmt" + + // ... +) + +func Key() string { + buf := make([]byte, 16) + if _, err := rand.Read(buf); err != nil { + log.Fatalf("Out of randomness, should never happen: %v", err) + } + return fmt.Sprintf("%x", buf) + // или hex.EncodeToString(buf) + // или base64.StdEncoding.EncodeToString(buf) +} +``` + +**Примечание:** `log.Fatalf` здесь не из стандартной библиотеки log. См. +[#logging]. + +<a id="useful-test-failures"></a> + +## Полезные сообщения об ошибках тестов + +<a id="TOC-UsefulTestFailures"></a> + +Должна быть возможность диагностировать неудачу теста без чтения исходного кода +теста. Тесты должны завершаться неудачей с полезными сообщениями, +детализирующими: + +* Что вызвало неудачу +* Какие входные данные привели к ошибке +* Фактический результат +* Что ожидалось + +Конкретные соглашения для достижения этой цели изложены ниже. + +<a id="assert"></a> + +### Библиотеки утверждений (Assertion libraries) + +<a id="TOC-Assert"></a> + +Не создавайте "библиотеки утверждений" в качестве помощников для тестирования. + +Библиотеки утверждений — это библиотеки, которые пытаются объединить проверку и +формирование сообщений об ошибках в тесте (хотя те же ловушки могут применяться +и к другим тестовым помощникам). Подробнее о различии между тестовыми +помощниками и библиотеками утверждений см. [лучшие +практики](https://neonxp.ru/pages/gostyleguide/google/best-practices/#test-functions). + +```go +// Плохо: +var obj BlogPost + +assert.IsNotNil(t, "obj", obj) +assert.StringEq(t, "obj.Type", obj.Type, "blogPost") +assert.IntEq(t, "obj.Comments", obj.Comments, 2) +assert.StringNotEq(t, "obj.Body", obj.Body, "") +``` + +Библиотеки утверждений имеют тенденцию либо останавливать тест рано (если +`assert` вызывает `t.Fatalf` или `panic`), либо опускать соответствующую +информацию о том, что тест получил правильно: + +```go +// Плохо: +package assert + +func IsNotNil(t *testing.T, name string, val any) { + if val == nil { + t.Fatalf("Data %s = nil, want not nil", name) + } +} + +func StringEq(t *testing.T, name, got, want string) { + if got != want { + t.Fatalf("Data %s = %q, want %q", name, got, want) + } +} +``` + +Сложные функции утверждений часто не предоставляют [полезные сообщения об +ошибках] и контекст, существующий внутри тестовой функции. Слишком много функций +утверждений и библиотек приводит к фрагментированному опыту разработчика: какую +библиотеку утверждений использовать, какой стиль формата вывода она должна +выдавать и т.д.? Фрагментация создает ненужную путаницу, особенно для +сопровождающих библиотек и авторов крупномасштабных изменений, которые отвечают +за исправление потенциальных поломок вниз по течению. Вместо создания +предметно-ориентированного языка для тестирования используйте сам Go. + +Библиотеки утверждений часто выносят сравнения и проверки равенства. +Предпочитайте использование стандартных библиотек, таких как [`cmp`] и [`fmt`], +вместо этого: + +```go +// Хорошо: +var got BlogPost + +want := BlogPost{ + Comments: 2, + Body: "Hello, world!", +} + +if !cmp.Equal(got, want) { + t.Errorf("Blog post = %v, want = %v", got, want) +} +``` + +Для более предметно-ориентированных помощников сравнения предпочитайте +возвращать значение или ошибку, которые можно использовать в сообщении об ошибке +теста, вместо передачи `*testing.T` и вызова его методов сообщения об ошибках: + +```go +// Хорошо: +func postLength(p BlogPost) int { return len(p.Body) } + +func TestBlogPost_VeritableRant(t *testing.T) { + post := BlogPost{Body: "I am Gunnery Sergeant Hartman, your senior drill instructor."} + + if got, want := postLength(post), 60; got != want { + t.Errorf("Length of post = %v, want %v", got, want) + } +} +``` + +**Лучшая практика:** Если бы `postLength` была нетривиальной, было бы разумно +тестировать ее напрямую, независимо от любых тестов, которые ее используют. + +См. также: + +* [Сравнение равенства и разницы (diffs)](#types-of-equality) +* [Печать разниц (diffs)](#print-diffs) +* Подробнее о различии между тестовыми помощниками и помощниками утверждений + см. [лучшие практики](https://neonxp.ru/pages/gostyleguide/google/best-practices/#test-functions) +* Раздел [Go FAQ] о [фреймворках тестирования] и их мнение об их отсутствии + +[полезные сообщения об ошибках]: #useful-test-failures +[`fmt`]: https://golang.org/pkg/fmt/ +[помеча как тестовый помощник]: #mark-test-helpers +[Go FAQ]: https://go.dev/doc/faq +[фреймворках тестирования]: https://go.dev/doc/faq#testing_framework + +<a id="identify-the-function"></a> + +### Идентификация функции + +В большинстве тестов сообщения об ошибках должны включать имя функции, которая +завершилась неудачей, даже если это кажется очевидным из имени тестовой функции. +Конкретно, ваше сообщение об ошибке должно быть `YourFunc(%v) = %v, want %v` +вместо просто `got %v, want %v`. + +<a id="identify-the-input"></a> + +### Идентификация входных данных + +В большинстве тестов сообщения об ошибках должны включать входные данные +функции, если они кратки. Если соответствующие свойства входных данных не +очевидны (например, потому что входные данные большие или непрозрачные), вы +должны называть свои тестовые случаи описанием того, что тестируется, и выводить +это описание как часть вашего сообщения об ошибке. + +<a id="got-before-want"></a> + +### Got перед want + +Вывод тестов должен включать фактическое значение, которое вернула функция, +перед печатью значения, которое ожидалось. Стандартный формат для вывода тестов +это `YourFunc(%v) = %v, want %v`. Там, где вы бы написали "actual" и "expected", +предпочитайте использовать слова "got" и "want" соответственно. + +Для разниц (diffs) направленность менее очевидна, и поэтому важно включить ключ, +чтобы помочь в интерпретации неудачи. См. [раздел о печати разниц]. Независимо +от порядка diff, который вы используете в своих сообщениях об ошибках, вы должны +явно указать его как часть сообщения об ошибке, поскольку существующий код +непоследователен в порядке. + +[раздел о печати разниц]: #print-diffs + +<a id="compare-full-structures"></a> + +### Полное сравнение структур + +Если ваша функция возвращает структуру (или любой тип данных с несколькими +полями, такой как срезы, массивы и карты), избегайте написания тестового кода, +который выполняет ручное поле-за-полем сравнение структуры. Вместо этого +сконструируйте данные, которые вы ожидаете, что ваша функция вернет, и +сравнивайте напрямую с помощью [глубокого сравнения (deep comparison)]. + +**Примечание:** Это не применяется, если ваши данные содержат нерелевантные +поля, которые затемняют намерение теста. + +Если вашу структуру нужно сравнивать на приблизительное (или эквивалентное +семантическое) равенство или она содержит поля, которые не могут быть сравнены +на равенство (например, если одно из полей — `io.Reader`), настройка сравнения +[`cmp.Diff`] или [`cmp.Equal`] с опциями [`cmpopts`], такими как +[`cmpopts.IgnoreInterfaces`], может удовлетворить ваши потребности +([пример](https://play.golang.org/p/vrCUNVfxsvF)). + +Если ваша функция возвращает несколько возвращаемых значений, вам не нужно +оборачивать их в структуру перед сравнением. Просто сравните возвращаемые +значения по отдельности и выведите их. + +```go +// Хорошо: +val, multi, tail, err := strconv.UnquoteChar(`\"Fran & Freddie's Diner\"`, '"') +if err != nil { + t.Fatalf(...) +} +if val != `"` { + t.Errorf(...) +} +if multi { + t.Errorf(...) +} +if tail != `Fran & Freddie's Diner"` { + t.Errorf(...) +} +``` + +[глубокого сравнения (deep comparison)]: #types-of-equality +[`cmpopts`]: https://pkg.go.dev/github.com/google/go-cmp/cmp/cmpopts +[`cmpopts.IgnoreInterfaces`]: + https://pkg.go.dev/github.com/google/go-cmp/cmp/cmpopts#IgnoreInterfaces + +<a id="compare-stable-results"></a> + +### Сравнение стабильных результатов + +Избегайте сравнения результатов, которые могут зависеть от стабильности вывода +пакета, которым вы не владеете. Вместо этого тест должен сравнивать семантически +релевантную информацию, которая стабильна и устойчива к изменениям в +зависимостях. Для функциональности, возвращающей форматированную строку или +сериализованные байты, как правило, небезопасно предполагать, что вывод +стабилен. + +Например, [`json.Marshal`] может измениться (и менялся в прошлом) в конкретных +байтах, которые он выдает. Тесты, выполняющие строковое равенство на JSON +строке, могут сломаться, если пакет `json` изменит способ сериализации байтов. +Вместо этого более надежный тест будет анализировать содержимое JSON строки и +убеждаться, что оно семантически эквивалентно некоторой ожидаемой структуре +данных. + +[`json.Marshal`]: https://golang.org/pkg/encoding/json/#Marshal + +<a id="keep-going"></a> + +### Продолжать выполнение + +Тесты должны продолжать работать как можно дольше, даже после неудачи, чтобы +вывести все неудачные проверки за один запуск. Таким образом, разработчик, +который исправляет падающий тест, не должен перезапускать тест после исправления +каждой ошибки, чтобы найти следующую ошибку. + +Предпочитайте вызов `t.Error` вместо `t.Fatal` для сообщения о несоответствии. +При сравнении нескольких различных свойств вывода функции используйте `t.Error` +для каждого из этих сравнений. + +```go +// Хорошо: +gotMean, gotVariance, err := MyDistribution(input) +if err != nil { + t.Fatalf("MyDistribution(%v) returned unexpected error: %v", input, err) +} +if diff := cmp.Diff(wantMean, gotMean); diff != "" { + t.Errorf("MyDistribution(%v) returned unexpected difference in mean value (-want +got):\n%s", input, diff) +} +if diff := cmp.Diff(wantVariance, gotVariance); diff != "" { + t.Errorf("MyDistribution(%v) returned unexpected difference in variance value (-want +got):\n%s", input, diff) +} +``` + +Вызов `t.Fatal` в первую очередь полезен для сообщения о неожиданном условии +(таком как ошибка или несоответствие вывода), когда последующие неудачи были бы +бессмысленны или даже вводили бы исследователя в заблуждение. Обратите внимание, +как код ниже вызывает `t.Fatalf` и *затем* `t.Errorf`: + +```go +// Хорошо: +gotEncoded := Encode(input) +if gotEncoded != wantEncoded { + t.Fatalf("Encode(%q) = %q, want %q", input, gotEncoded, wantEncoded) + // Не имеет смысла декодировать из неожиданного закодированного ввода. +} +gotDecoded, err := Decode(gotEncoded) +if err != nil { + t.Fatalf("Decode(%q) returned unexpected error: %v", gotEncoded, err) +} +if gotDecoded != input { + t.Errorf("Decode(%q) = %q, want %q", gotEncoded, gotDecoded, input) +} +``` + +Для табличных тестов рассмотрите использование подтестов и используйте `t.Fatal` +вместо `t.Error` и `continue`. См. также [GoTip #25: Subtests: Making Your Tests +Lean](https://google.github.io/styleguide/go/index.html#gotip). + +**Лучшая практика:** Для дальнейшего обсуждения о том, когда следует +использовать `t.Fatal`, см. [лучшие практики](https://neonxp.ru/pages/gostyleguide/google/best-practices/#t-fatal). + +<a id="types-of-equality"></a> + +### Сравнение равенства и разницы (diffs) + +Оператор `==` вычисляет равенство, используя [определенные языком сравнения]. +Скалярные значения (числа, булевы и т.д.) сравниваются на основе их значений, но +только некоторые структуры и интерфейсы можно сравнивать таким образом. +Указатели сравниваются на основе того, указывают ли они на одну и ту же +переменную, а не на основе равенства значений, на которые они указывают. + +Пакет [`cmp`] может сравнивать более сложные структуры данных, не обрабатываемые +адекватно `==`, такие как срезы. Используйте [`cmp.Equal`] для сравнения +равенства и [`cmp.Diff`] для получения читаемой человеком разницы между +объектами. + +```go +// Хорошо: +want := &Doc{ + Type: "blogPost", + Comments: 2, + Body: "This is the post body.", + Authors: []string{"isaac", "albert", "emmy"}, +} +if !cmp.Equal(got, want) { + t.Errorf("AddPost() = %+v, want %+v", got, want) +} +``` + +Как универсальная библиотека сравнения, `cmp` может не знать, как сравнивать +определенные типы. Например, она может сравнивать сообщения protobuf только если +передана опция [`protocmp.Transform`]. + +<!-- The order of want and got here is deliberate. See comment in #print-diffs. --> + +```go +// Хорошо: +if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("Foo() returned unexpected difference in protobuf messages (-want +got):\n%s", diff) +} +``` + +Хотя пакет `cmp` не является частью стандартной библиотеки Go, он поддерживается +командой Go и должен обеспечивать стабильные результаты равенства с течением +времени. Он настраивается пользователем и должен удовлетворять большинству +потребностей в сравнении. + +[определенные языком сравнения]: http://golang.org/ref/spec#Comparison_operators +[`cmp`]: https://pkg.go.dev/github.com/google/go-cmp/cmp +[`cmp.Equal`]: https://pkg.go.dev/github.com/google/go-cmp/cmp#Equal +[`cmp.Diff`]: https://pkg.go.dev/github.com/google/go-cmp/cmp#Diff +[`protocmp.Transform`]: + https://pkg.go.dev/google.golang.org/protobuf/testing/protocmp#Transform + +Существующий код может использовать следующие более старые библиотеки и может +продолжать использовать их для согласованности: + +* [`pretty`] создает эстетически приятные отчеты о различиях. Однако он + довольно намеренно считает значения, имеющие одинаковое визуальное + представление, равными. В частности, `pretty` не улавливает различия между + nil срезами и пустыми, не чувствителен к разным реализациям интерфейсов с + идентичными полями, и возможно использовать вложенную карту в качестве + основы для сравнения со значением структуры. Он также сериализует все + значение в строку перед созданием diff, и, как таковой, не является хорошим + выбором для сравнения больших значений. По умолчанию он сравнивает + неэкспортируемые поля, что делает его чувствительным к изменениям в деталях + реализации в ваших зависимостях. По этой причине нецелесообразно + использовать `pretty` на сообщениях protobuf. + +[`pretty`]: https://pkg.go.dev/github.com/kylelemons/godebug/pretty + +Предпочитайте использовать `cmp` для нового кода, и стоит рассмотреть обновление +старого кода для использования `cmp`, где и когда это практично. + +Старый код может использовать функцию стандартной библиотеки `reflect.DeepEqual` +для сравнения сложных структур. `reflect.DeepEqual` не следует использовать для +проверки равенства, так как она чувствительна к изменениям в неэкспортированных +полях и другим деталям реализации. Код, использующий `reflect.DeepEqual`, должен +быть обновлен до одной из вышеупомянутых библиотек. + +**Примечание:** Пакет `cmp` предназначен для тестирования, а не для +использования в продакшене. Как таковой, он может вызывать панику, когда +подозревает, что сравнение выполнено неправильно, чтобы дать инструкции +пользователям, как улучшить тест, чтобы он был менее хрупким. Учитывая +склонность cmp к панике, он непригоден для кода, который используется в +продакшене, поскольку ложная паника может быть фатальной. + +<a id="level-of-detail"></a> + +### Уровень детализации + +Обычное сообщение об ошибке, подходящее для большинства тестов Go, это +`YourFunc(%v) = %v, want %v`. Однако бывают случаи, которые могут потребовать +больше или меньше деталей: + +* Тесты, выполняющие сложные взаимодействия, должны также описывать + взаимодействия. Например, если один и тот же `YourFunc` вызывается + несколько раз, идентифицируйте, какой вызов провалил тест. Если важно знать + любое дополнительное состояние системы, включите это в вывод ошибки (или + хотя бы в логи). +* Если данные представляют собой сложную структуру со значительным шаблонным + кодом, допустимо описать только важные части в сообщении, но не чрезмерно + затемняйте данные. +* Ошибки настройки не требуют такого же уровня детализации. Если тестовый + помощник заполняет таблицу Spanner, но Spanner был выключен, вам, вероятно, + не нужно включать, какие тестовые входные данные вы собирались сохранить в + базе данных. `t.Fatalf("Setup: Failed to set up test database: %s", err)` + обычно достаточно полезно, чтобы решить проблему. + +**Совет:** Заставьте ваш режим отказа срабатывать во время разработки. +Просмотрите, как выглядит сообщение об ошибке, и может ли сопровождающий +эффективно справиться с неудачей. + +Существуют некоторые методы для ясного воспроизведения тестовых входов и +выходов: + +* При выводе строковых данных [`%q` часто полезен](#use-percent-q) для + выделения важности значения и более легкого обнаружения плохих значений. +* При выводе (маленьких) структур `%+v` может быть полезнее, чем `%v`. +* Когда проверка больших значений завершается неудачей, [печать разницы + (diff)](#print-diffs) может облегчить понимание неудачи. + +<a id="print-diffs"></a> + +### Печать разниц (diffs) + +Если ваша функция возвращает большой вывод, читающему может быть трудно найти +различия, когда ваш тест проваливается. Вместо печати и возвращенного значения, +и ожидаемого значения создайте diff. + +Для вычисления разниц для таких значений предпочтительна `cmp.Diff`, особенно +для новых тестов и нового кода, но могут использоваться и другие инструменты. +См. [типы равенства] для рекомендаций относительно сильных и слабых сторон +каждой функции. + +* [`cmp.Diff`] + +* [`pretty.Compare`] + +Вы можете использовать пакет [`diff`] для сравнения многострочных строк или +списков строк. Вы можете использовать это как строительный блок для других видов +разниц. + +[типы равенства]: #types-of-equality +[`diff`]: https://pkg.go.dev/github.com/kylelemons/godebug/diff +[`pretty.Compare`]: + https://pkg.go.dev/github.com/kylelemons/godebug/pretty#Compare + +Добавьте некоторый текст в ваше сообщение об ошибке, объясняющий направление +diff. + +<!-- +The reversed order of want and got in these examples is intentional, as this is +the prevailing order across the Google codebase. The lack of a stance on which +order to use is also intentional, as there is no consensus which is +"most readable." + + +--> + +* Что-то вроде `diff (-want +got)` хорошо, когда вы используете пакеты `cmp`, + `pretty` и `diff` (если вы передаете `(want, got)` в функцию), потому что + `-` и `+`, которые вы добавляете в строку формата, будут соответствовать `-` + и `+`, которые фактически появляются в начале строк diff. Если вы передаете + `(got, want)` в вашу функцию, правильным ключом будет `(-got +want)` вместо + этого. + +* Пакет `messagediff` использует другой формат вывода, поэтому сообщение `diff + (want -> got)` уместно, когда вы его используете (если вы передаете `(want, + got)` в функцию), потому что направление стрелки будет соответствовать + направлению стрелки в строках "modified". + +Diff будет занимать несколько строк, поэтому вы должны напечатать новую строку +перед печатью diff. + +<a id="test-error-semantics"></a> + +### Тестирование семантики ошибок + +Когда модульный тест выполняет строковые сравнения или использует обычный `cmp` +для проверки того, что возвращаются определенные типы ошибок для определенных +входных данных, вы можете обнаружить, что ваши тесты становятся хрупкими, если +какое-либо из этих сообщений об ошибках будет переформулировано в будущем. +Поскольку это может превратить ваш модульный тест в детектор изменений (см. +[TotT: Change-Detector Tests Considered Harmful][tott-350] ), не используйте +строковое сравнение для проверки того, какой тип ошибки возвращает ваша функция. +Однако допустимо использовать строковые сравнения для проверки того, что +сообщения об ошибках, исходящие из тестируемого пакета, удовлетворяют +определенным свойствам, например, что оно включает имя параметра. + +Значения ошибок в Go обычно имеют компонент, предназначенный для человеческого +восприятия, и компонент, предназначенный для семантического управления потоком. +Тесты должны стремиться тестировать только семантическую информацию, которую +можно надежно наблюдать, а не отображаемую информацию, предназначенную для +отладки человеком, поскольку последняя часто подвержена будущим изменениям. +Рекомендации по созданию ошибок со смысловым значением см. [лучшие практики +относительно ошибок](https://neonxp.ru/pages/gostyleguide/google/best-practices/#error-handling). Если ошибка с недостаточной +семантической информацией исходит из зависимости вне вашего контроля, +рассмотрите возможность создания отчета об ошибке (bug) владельцу, чтобы помочь +улучшить API, а не полагаться на разбор сообщения об ошибке. + +В рамках модульных тестов часто важно только то, произошла ли ошибка или нет. +Если да, то достаточно проверить только, была ли ошибка ненулевой, когда вы +ожидали ошибки. Если вы хотите проверить, что ошибка семантически соответствует +какой-то другой ошибке, рассмотрите использование [`errors.Is`] или `cmp` с +[`cmpopts.EquateErrors`]. + +> **Примечание:** Если тест использует [`cmpopts.EquateErrors`], но все его +> значения `wantErr` либо `nil`, либо `cmpopts.AnyError`, то использование `cmp` +> является [ненужным механизмом](https://neonxp.ru/pages/gostyleguide/google/guide/#least-mechanism). Упростите код, сделав +> поле `want` типа `bool`. Затем вы можете использовать простое сравнение с +> `!=`. +> +> ```go +> // Хорошо: +> err := f(test.input) +> if gotErr := err != nil; gotErr != test.wantErr { +> t.Errorf("f(%q) = %v, want error presence = %v", test.input, err, test.wantErr) +> } +> ``` + +См. также [GoTip #13: Designing Errors for +Checking](https://google.github.io/styleguide/go/index.html#gotip). + +[tott-350]: + https://testing.googleblog.com/2015/01/testing-on-toilet-change-detector-tests.html +[`cmpopts.EquateErrors`]: + https://pkg.go.dev/github.com/google/go-cmp/cmp/cmpopts#EquateErrors +[`errors.Is`]: https://pkg.go.dev/errors#Is + +<a id="test-structure"></a> + +## Структура тестов + +<a id="subtests"></a> + +### Подтесты (Subtests) + +Стандартная библиотека тестирования Go предоставляет возможность [определять +подтесты]. Это позволяет гибкость в настройке и очистке, управлении +параллелизмом и фильтрации тестов. Подтесты могут быть полезны (особенно для +табличных тестов), но их использование не обязательно. См. также [пост в блоге +Go о подтестах](https://blog.golang.org/subtests). + +Подтесты не должны зависеть от выполнения других случаев для успеха или +начального состояния, поскольку ожидается, что подтесты могут быть запущены +индивидуально с использованием флагов `go test -run` или выражений [фильтра +тестов] Bazel. + +[определять подтесты]: + https://pkg.go.dev/testing#hdr-Subtests_and_Sub_benchmarks +[фильтра тестов]: https://bazel.build/docs/user-manual#test-filter + +<a id="subtest-names"></a> + +#### Имена подтестов + +Назовите ваш подтест так, чтобы он был читаем в выводе теста и полезен в +командной строке для пользователей фильтрации тестов. Когда вы используете +`t.Run` для создания подтеста, первый аргумент используется как описательное имя +для теста. Чтобы гарантировать, что результаты тестов читаемы для людей, +читающих логи, выбирайте имена подтестов, которые останутся полезными и +читаемыми после экранирования. Думайте об именах подтестов скорее как об +идентификаторе функции, чем о прозаическом описании. + +Тестовый раннер заменяет пробелы подчеркиваниями и экранирует непечатаемые +символы. Чтобы обеспечить точную корреляцию между логами тестов и исходным +кодом, рекомендуется избегать использования этих символов в именах подтестов. + +Если ваши тестовые данные выигрывают от более длинного описания, рассмотрите +возможность поместить описание в отдельное поле (возможно, для печати с помощью +`t.Log` или рядом с сообщениями об ошибках). + +Подтесты могут быть запущены индивидуально с использованием флагов [Go test +runner] или Bazel [фильтра тестов], поэтому выбирайте описательные имена, +которые также легко набирать. + +> **Предупреждение:** Символы слеша особенно недружелюбны в именах подтестов, +> поскольку они имеют [особое значение для фильтров тестов]. +> +> > ```sh +> > # Плохо: +> > # Предполагая TestTime и t.Run("America/New_York", ...) +> > bazel test :mytest --test_filter="Time/New_York" # Ничего не запускает! +> > bazel test :mytest --test_filter="Time//New_York" # Правильно, но неудобно. +> > ``` + +Чтобы [идентифицировать входные данные] функции, включите их в сообщения об +ошибках теста, где они не будут экранированы тестовым раннером. + +```go +// Хорошо: +func TestTranslate(t *testing.T) { + data := []struct { + name, desc, srcLang, dstLang, srcText, wantDstText string + }{ + { + name: "hu=en_bug-1234", + desc: "regression test following bug 1234. contact: cleese", + srcLang: "hu", + srcText: "cigarettát és egy öngyújtót kérek", + dstLang: "en", + wantDstText: "cigarettes and a lighter please", + }, // ... + } + for _, d := range data { + t.Run(d.name, func(t *testing.T) { + got := Translate(d.srcLang, d.dstLang, d.srcText) + if got != d.wantDstText { + t.Errorf("%s\nTranslate(%q, %q, %q) = %q, want %q", + d.desc, d.srcLang, d.dstLang, d.srcText, got, d.wantDstText) + } + }) + } +} +``` + +Вот несколько примеров того, чего следует избегать: + +```go +// Плохо: +// Слишком многословно. +t.Run("check that there is no mention of scratched records or hovercrafts", ...) +// Слеши вызывают проблемы в командной строке. +t.Run("AM/PM confusion", ...) +``` + +См. также [Go Tip #117: Subtest +Names](https://google.github.io/styleguide/go/index.html#gotip). + +[Go test runner]: https://golang.org/cmd/go/#hdr-Testing_flags +[идентифицировать входные данные]: #identify-the-input +[особое значение для фильтров тестов]: + https://blog.golang.org/subtests#:~:text=Perhaps%20a%20bit,match%20any%20tests + +<a id="table-driven-tests"></a> + +### Табличные тесты + +Используйте табличные тесты, когда множество различных тестовых случаев могут +быть протестированы с помощью сходной тестовой логики. + +* При тестировании того, равен ли фактический вывод функции ожидаемому выводу. + Например, множество [тестов `fmt.Sprintf`] или минимальный фрагмент ниже. +* При тестировании того, соответствуют ли выводы функции всегда одному и тому + же набору инвариантов. Например, [тесты для `net.Dial`]. + +[тестов `fmt.Sprintf`]: + https://cs.opensource.google/go/go/+/master:src/fmt/fmt_test.go +[тестов для `net.Dial`]: + https://cs.opensource.google/go/go/+/master:src/net/dial_test.go;l=318;drc=5b606a9d2b7649532fe25794fa6b99bd24e7697c + +Вот минимальная структура табличного теста. При необходимости вы можете +использовать другие имена или добавить дополнительные средства, такие как +подтесты или функции настройки и очистки. Всегда помните о [полезных сообщениях +об ошибках тестов](#useful-test-failures). + +```go +// Хорошо: +func TestCompare(t *testing.T) { + compareTests := []struct { + a, b string + want int + }{ + {"", "", 0}, + {"a", "", 1}, + {"", "a", -1}, + {"abc", "abc", 0}, + {"ab", "abc", -1}, + {"abc", "ab", 1}, + {"x", "ab", 1}, + {"ab", "x", -1}, + {"x", "a", 1}, + {"b", "x", -1}, + // тестирование chunked-реализации runtime·memeq + {"abcdefgh", "abcdefgh", 0}, + {"abcdefghi", "abcdefghi", 0}, + {"abcdefghi", "abcdefghj", -1}, + } + + for _, test := range compareTests { + got := Compare(test.a, test.b) + if got != test.want { + t.Errorf("Compare(%q, %q) = %v, want %v", test.a, test.b, got, test.want) + } + } +} +``` + +**Примечание**: Сообщения об ошибках в приведенном выше примере соответствуют +рекомендациям [идентифицировать функцию](#identify-the-function) и +[идентифицировать входные данные](#identify-the-input). Нет необходимости +[идентифицировать строку численно](#table-tests-identifying-the-row). + +Когда некоторые тестовые случаи нужно проверять с помощью логики, отличной от +других тестовых случаев, уместно написать несколько тестовых функций, как +объясняется в [GoTip #50: Disjoint Table Tests]. + +Когда дополнительные тестовые случаи просты (например, базовая проверка ошибок) +и не вводят условный поток кода в теле цикла табличного теста, допустимо +включить этот случай в существующий тест, хотя будьте осторожны, используя такую +логику. То, что начинается просто сегодня, может органически вырасти во что-то +неподдерживаемое. + +Например: + +```go +func TestDivide(t *testing.T) { + tests := []struct { + dividend, divisor int + want int + wantErr bool + }{ + { + dividend: 4, + divisor: 2, + want: 2, + }, + { + dividend: 10, + divisor: 2, + want: 5, + }, + { + dividend: 1, + divisor: 0, + wantErr: true, + }, + } + + for _, test := range tests { + got, err := Divide(test.dividend, test.divisor) + if (err != nil) != test.wantErr { + t.Errorf("Divide(%d, %d) error = %v, want error presence = %t", test.dividend, test.divisor, err, test.wantErr) + } + + // В этом примере мы тестируем значение результата только когда тестируемая функция не завершилась неудачей. + if err != nil { + continue + } + + if got != test.want { + t.Errorf("Divide(%d, %d) = %d, want %d", test.dividend, test.divisor, got, test.want) + } + } +} +``` + +Более сложная логика в вашем тестовом коде, например сложная проверка ошибок на +основе условных различий в настройке теста (часто основанных на входных +параметрах табличного теста), может быть [трудной для +понимания](https://neonxp.ru/pages/gostyleguide/google/guide/#maintainability), когда каждая запись в таблице имеет +специализированную логику на основе входных данных. Если тестовые случаи имеют +разную логику, но идентичную настройку, последовательность +[подтестов](#subtests) внутри одной тестовой функции может быть более читаемой. +Тестовый помощник также может быть полезен для упрощения настройки теста с целью +сохранения читаемости тела теста. + +Вы можете комбинировать табличные тесты с несколькими тестовыми функциями. +Например, при тестировании того, что вывод функции точно соответствует +ожидаемому выводу и что функция возвращает ненулевую ошибку для неверного ввода, +лучшим подходом является написание двух отдельных табличных тестовых функций: +одну для обычных не-ошибочных выводов, и одну для ошибочных выводов. + +[GoTip #50: Disjoint Table Tests]: + https://google.github.io/styleguide/go/index.html#gotip + +<a id="table-tests-data-driven"></a> + +#### Тестовые случаи на основе данных + +Строки табличных тестов иногда могут становиться сложными, причем значения строк +диктуют условное поведение внутри тестового случая. Дополнительная ясность от +дублирования между тестовыми случаями необходима для читаемости. + +```go +// Хорошо: +type decodeCase struct { + name string + input string + output string + err error +} + +func TestDecode(t *testing.T) { + // setupCodex медленный, так как создает реальный Codex для теста. + codex := setupCodex(t) + + var tests []decodeCase // строки опущены для краткости + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + output, err := Decode(test.input, codex) + if got, want := output, test.output; got != want { + t.Errorf("Decode(%q) = %v, want %v", test.input, got, want) + } + if got, want := err, test.err; !cmp.Equal(got, want) { + t.Errorf("Decode(%q) err %q, want %q", test.input, got, want) + } + }) + } +} + +func TestDecodeWithFake(t *testing.T) { + // fakeCodex — это быстрое приближение реального Codex. + codex := newFakeCodex() + + var tests []decodeCase // строки опущены для краткости + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + output, err := Decode(test.input, codex) + if got, want := output, test.output; got != want { + t.Errorf("Decode(%q) = %v, want %v", test.input, got, want) + } + if got, want := err, test.err; !cmp.Equal(got, want) { + t.Errorf("Decode(%q) err %q, want %q", test.input, got, want) + } + }) + } +} +``` + +В контринте ниже обратите внимание, как сложно различить, какой тип `Codex` +используется на тестовый случай в настройке случая. (Выделенные части нарушают +совет из [TotT: Data Driven Traps!][tott-97] .) + +```go +// Плохо: +type decodeCase struct { + name string + input string + codex testCodex + output string + err error +} + +type testCodex int + +const ( + fake testCodex = iota + prod +) + +func TestDecode(t *testing.T) { + var tests []decodeCase // строки опущены для краткости + + for _, test := tests { + t.Run(test.name, func(t *testing.T) { + var codex Codex + switch test.codex { + case fake: + codex = newFakeCodex() + case prod: + codex = setupCodex(t) + default: + t.Fatalf("Unknown codex type: %v", codex) + } + output, err := Decode(test.input, codex) + if got, want := output, test.output; got != want { + t.Errorf("Decode(%q) = %q, want %q", test.input, got, want) + } + if got, want := err, test.err; !cmp.Equal(got, want) { + t.Errorf("Decode(%q) err %q, want %q", test.input, got, want) + } + }) + } +} +``` + +[tott-97]: https://testing.googleblog.com/2008/09/tott-data-driven-traps.html + +<a id="table-tests-identifying-the-row"></a> + +#### Идентификация строки + +Не используйте индекс теста в тестовой таблице в качестве замены именования +ваших тестов или печати входных данных. Никто не хочет проходить через вашу +тестовую таблицу и считать записи, чтобы выяснить, какой тестовый случай +провалился. + +```go +// Плохо: +tests := []struct { + input, want string +}{ + {"hello", "HELLO"}, + {"wORld", "WORLD"}, +} +for i, d := range tests { + if strings.ToUpper(d.input) != d.want { + t.Errorf("Failed on case #%d", i) + } +} +``` + +Добавьте описание теста в вашу тестовую структуру и печатайте его вместе с +сообщениями об ошибках. При использовании подтестов имя вашего подтеста должно +эффективно идентифицировать строку. + +**Важно:** Хотя `t.Run` ограничивает область вывода и выполнения, вы должны +всегда [идентифицировать входные данные]. Имена строк табличных тестов должны +следовать [руководству по именованию подтестов]. + +[идентифицировать входные данные]: #identify-the-input +[руководству по именованию подтестов]: #subtest-names + +<a id="mark-test-helpers"></a> + +### Тестовые помощники (Test helpers) + +Тестовый помощник — это функция, выполняющая задачу настройки или очистки. Все +ошибки, возникающие в тестовых помощниках, должны быть ошибками окружения (а не +кода под тестом) — например, когда тестовая база данных не может быть запущена +потому что на этой машине не осталось свободных портов. + +Если вы передаете `*testing.T`, вызовите [`t.Helper`], чтобы приписать ошибки в +тестовом помощнике строке, где помощник вызывается. Этот параметр должен идти +после параметра [контекста](#contexts), если он присутствует, и перед любыми +оставшимися параметрами. + +```go +// Хорошо: +func TestSomeFunction(t *testing.T) { + golden := readFile(t, "testdata/golden-result.txt") + // ... тесты против golden ... +} + +// readFile возвращает содержимое файла данных. +// Может вызываться только из той же горутины, которая начала тест. +func readFile(t *testing.T, filename string) string { + t.Helper() + contents, err := runfiles.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + return string(contents) +} +``` + +Не используйте этот шаблон, когда он скрывает связь между неудачей теста и +условиями, которые к ней привели. Конкретно, рекомендации о [библиотеках +утверждений](#assert) все еще применяются, и [`t.Helper`] не должен +использоваться для реализации таких библиотек. + +**Совет:** Подробнее о различии между тестовыми помощниками и помощниками +утверждений см. [лучшие практики](https://neonxp.ru/pages/gostyleguide/google/best-practices/#test-functions). + +Хотя вышесказанное относится к `*testing.T`, большая часть рекомендаций остается +той же для вспомогательных функций бенчмарков и фаззинга. + +[`t.Helper`]: https://pkg.go.dev/testing#T.Helper + +<a id="test-package"></a> + +### Тестовый пакет + +<a id="TOC-TestPackage"></a> + +<a id="test-same-package"></a> + +#### Тесты в том же пакете + +Тесты могут быть определены в том же пакете, что и тестируемый код. + +Чтобы написать тест в том же пакете: + +* Поместите тесты в файл `foo_test.go` +* Используйте `package foo` для тестового файла +* Не импортируйте явно тестируемый пакет + +```build +# Хорошо: +go_library( + name = "foo", + srcs = ["foo.go"], + deps = [ + ... + ], +) + +go_test( + name = "foo_test", + size = "small", + srcs = ["foo_test.go"], + library = ":foo", + deps = [ + ... + ], +) +``` + +Тест в том же пакете может обращаться к неэкспортированным идентификаторам в +пакете. Это может обеспечить лучшее покрытие тестами и более лаконичные тесты. +Имейте в виду, что любые [примеры], объявленные в тесте, не будут иметь имен +пакетов, которые понадобятся пользователю в их коде. + +[`library`]: + https://github.com/bazelbuild/rules_go/blob/master/docs/go/core/rules.md#go_library +[примеры]: #examples + +<a id="test-different-package"></a> + +#### Тесты в другом пакете + +Не всегда уместно или даже возможно определить тест в том же пакете, что и +тестируемый код. В этих случаях используйте имя пакета с суффиксом `_test`. Это +исключение из правила "без подчеркиваний" для [имен пакетов](#package-names). +Например: + +* Если интеграционный тест не имеет очевидной библиотеки, к которой он + принадлежит + + ```go + // Хорошо: + package gmailintegration_test + + import "testing" + ``` + +* Если определение тестов в том же пакете приводит к циклическим зависимостям + + ```go + // Хорошо: + package fireworks_test + + import ( + "fireworks" + "fireworkstestutil" // fireworkstestutil также импортирует fireworks + ) + ``` + +<a id="use-package-testing"></a> + +### Использование пакета `testing` + +Стандартная библиотека Go предоставляет пакет [`testing`]. Это единственный +фреймворк тестирования, разрешенный для кода Go в кодовой базе Google. В +частности, [библиотеки утверждений](#assert) и сторонние фреймворки тестирования +не разрешены. + +Пакет `testing` предоставляет минимальный, но полный набор функциональности для +написания хороших тестов: + +* Тесты верхнего уровня +* Бенчмарки +* [Исполняемые примеры](https://blog.golang.org/examples) +* Подтесты +* Логирование +* Ошибки и фатальные ошибки + +Они разработаны для слаженной работы с основными языковыми возможностями, такими +как [составные литералы] и [if с инициализатором], чтобы позволить авторам +тестов писать [понятные, читаемые и поддерживаемые тесты]. + +[`testing` package]: https://pkg.go.dev/testing +[составные литералы]: https://go.dev/ref/spec#Composite_literals +[if с инициализатором]: https://go.dev/ref/spec#If_statements + +<a id="non-decisions"></a> + +## Не-решения + +Руководство по стилю не может перечислять позитивные предписания по всем +вопросам, как не может оно перечислить все вопросы, по которым оно не дает +мнения. Тем не менее, вот несколько вещей, по которым сообщество читаемости +ранее спорило и не достигло консенсуса. + +* **Локальная инициализация переменных нулевым значением**. `var i int` и `i + := 0` эквивалентны. См. также [лучшие практики инициализации]. +* **Пустой составной литерал vs. `new` или `make`**. `&File{}` и `new(File)` + эквивалентны. Так же `map[string]bool{}` и `make(map[string]bool)`. См. + также [лучшие практики составных объявлений]. +* **Порядок аргументов got, want в вызовах cmp.Diff**. Будьте локально + последовательны и [включите легенду](#print-diffs) в ваше сообщение об + ошибке. +* **`errors.New` vs `fmt.Errorf` на неформатированных строках**. + `errors.New("foo")` и `fmt.Errorf("foo")` могут использоваться + взаимозаменяемо. + +Если возникнут особые обстоятельства, наставник по читаемости может сделать +необязательный комментарий, но в целом автор волен выбирать предпочитаемый им +стиль в данной ситуации. + +Естественно, если что-то, не охваченное руководством по стилю, требует +дополнительного обсуждения, авторы могут спросить — либо в конкретном ревью, +либо на внутренних досках сообщений. + +[лучшие практики составных объявлений]: + https://google.github.io/styleguide/go/best-practices#vardeclcomposite +[лучшие практики инициализации]: + https://google.github.io/styleguide/go/best-practices#vardeclinitialization diff --git a/content/pages/gostyleguide/google/guide.md b/content/pages/gostyleguide/google/guide.md new file mode 100644 index 0000000..31b3b33 --- /dev/null +++ b/content/pages/gostyleguide/google/guide.md @@ -0,0 +1,495 @@ +--- +order: 3 +title: Google Go Style Guide — Руководство +--- + +# Руководство по стилю Go (Go Style Guide) + +Оригинал: https://google.github.io/styleguide/go/guide + +[Обзор](https://neonxp.ru/pages/gostyleguide/google/) | [Руководство](https://neonxp.ru/pages/gostyleguide/google/guide) | [Решения](https://neonxp.ru/pages/gostyleguide/google/decisions) | +[Лучшие практики](https://neonxp.ru/pages/gostyleguide/google/best-practices) + + +**Примечание:** Это часть серии документов, описывающих [Стиль Go (Go +Style)](https://neonxp.ru/pages/gostyleguide/google/) в Google. Данный документ является **[нормативным +(normative)](https://neonxp.ru/pages/gostyleguide/google/#normative) и [каноническим (canonical)](https://neonxp.ru/pages/gostyleguide/google/#canonical)**. +Подробнее см. [в обзоре](https://neonxp.ru/pages/gostyleguide/google/#about). + +<a id="principles"></a> + +## Принципы стиля + +Существует несколько основополагающих принципов, которые суммируют подход к +написанию читаемого кода на Go. Ниже перечислены атрибуты читаемого кода в +порядке важности: + +1. **[Понятность (Clarity)]**: Цель и обоснование кода ясны читателю. +1. **[Простота (Simplicity)]**: Код достигает своей цели наиболее простым + способом. +1. **[Лаконичность (Concision)]**: Код имеет высокое отношение сигнала к шуму. +1. **[Поддерживаемость (Maintainability)]**: Код написан так, чтобы его было + легко поддерживать. +1. **[Согласованность (Consistency)]**: Код согласуется с более широкой + кодобазой Google. + +[Понятность (Clarity)]: #clarity +[Простота (Simplicity)]: #simplicity +[Лаконичность (Concision)]: #concision +[Поддерживаемость (Maintainability)]: #maintainability +[Согласованность (Consistency)]: #consistency + +<a id="clarity"></a> + +### Понятность (Clarity) + +Основная цель удобочитаемости — создание кода, который понятен читателю. + +Понятность достигается в первую очередь за счет эффективного именования, +полезных комментариев и продуманной организации кода. + +Понятность следует рассматривать с точки зрения читателя, а не автора кода. +Важнее, чтобы код был легок для чтения, а не для написания. Понятность кода +имеет два аспекта: + +* [Что именно делает код?](#clarity-purpose) +* [Почему код делает то, что он делает?](#clarity-rationale) + +<a id="clarity-purpose"></a> + +#### Что именно делает код? + +Go разработан таким образом, чтобы относительно легко было понять, что делает +код. В случаях неопределенности или когда читателю могут потребоваться +предварительные знания для понимания кода, стоит потратить время, чтобы сделать +цель кода более ясной для будущих читателей. Например, может помочь: + +* Использование более описательных имен переменных +* Добавление дополнительных комментариев +* Разделение кода пробелами и комментариями +* Рефакторинг кода в отдельные функции/методы для повышения модульности + +Здесь нет универсального решения, но при разработке кода на Go важно отдавать +приоритет понятности. + +<a id="clarity-rationale"></a> + +#### Почему код делает то, что он делает? + +Обоснование кода часто достаточно ясно передается именами переменных, функций, +методов или пакетов. Если это не так, важно добавить комментарии. "Почему?" +особенно важно, когда код содержит нюансы, с которыми читатель может быть не +знаком, например: + +* Нюанс языка, например, замыкание захватит переменную цикла, но само + замыкание находится далеко от него +* Нюанс бизнес-логики, например, проверка прав доступа, которая должна + различать реального пользователя и того, кто выдает себя за пользователя + +API может требовать осторожного использования. Например, фрагмент кода может +быть сложным и трудным для понимания из-за соображений производительности, или +сложная последовательность математических операций может использовать +преобразования типов неожиданным образом. В этих и многих других случаях важно, +чтобы сопровождающие комментарии и документация объясняли эти аспекты, чтобы +будущие сопровождающие не допустили ошибку и чтобы читатели могли понять код без +необходимости его обратной разработки. + +Также важно осознавать, что некоторые попытки повысить понятность (например, +добавление лишних комментариев) могут фактически затуманить цель кода, добавляя +беспорядок, пересказывая то, что код уже говорит, противореча коду или добавляя +нагрузку по поддержке актуальности комментариев. Позвольте коду говорить самому +за себя (например, делая имена символов самодокументированными), а не добавляйте +избыточные комментарии. Зачастую лучше, чтобы комментарии объясняли, *почему* +что-то сделано, а не *что* делает код. + +Кодовая база Google в значительной степени единообразна и согласована. Часто +бывает, что код, который выделяется (например, использованием незнакомого +шаблона), делает это по уважительной причине, обычно для производительности. +Поддержание этого свойства важно, чтобы дать читателям понять, куда им следует +направить внимание при чтении нового фрагмента кода. + +Стандартная библиотека содержит множество примеров реализации этого принципа. +Среди них: + +* Комментарии сопровождающих в [`package + sort`](https://cs.opensource.google/go/go/+/refs/tags/go1.19.2:src/sort/sort.go). +* Хорошие [запускаемые примеры в том же + пакете](https://cs.opensource.google/go/go/+/refs/tags/go1.19.2:src/sort/example_search_test.go), + которые полезны как пользователям (они [отображаются в + godoc](https://pkg.go.dev/sort#pkg-examples)), так и сопровождающим (они + [запускаются как часть тестов](https://neonxp.ru/pages/gostyleguide/google/decisions/#examples)). +* [`strings.Cut`](https://pkg.go.dev/strings#Cut) — это всего четыре строки + кода, но они повышают [понятность и корректность мест вызова + (callsites)](https://github.com/golang/go/issues/46336). + +<a id="simplicity"></a> + +### Простота (Simplicity) + +Ваш код на Go должен быть простым для тех, кто его использует, читает и +поддерживает. + +Код на Go должен быть написан наиболее простым способом, достигающим его целей, +как с точки зрения поведения, так и производительности. Внутри кодовой базы Go в +Google простой код: + +* Легко читается сверху вниз +* Не предполагает, что вы уже знаете, что он делает +* Не предполагает, что вы можете запомнить весь предшествующий код +* Не имеет ненужных уровней абстракции +* Не имеет имен, которые привлекают внимание к чему-то обыденному +* Делает передачу значений и принятие решений понятными для читателя +* Имеет комментарии, которые объясняют *почему*, а не *что* делает код, чтобы + избежать будущих отклонений +* Имеет самодостаточную документацию +* Имеет полезные ошибки и полезные сообщения об ошибках в тестах +* Часто может быть взаимоисключающим с "умным" кодом + +Могут возникать компромиссы между простотой кода и простотой использования API. +Например, может иметь смысл сделать код более сложным, чтобы конечному +пользователю API было легче правильно его вызывать. И наоборот, также может быть +целесообразно оставить немного дополнительной работы конечному пользователю API, +чтобы код оставался простым и легким для понимания. + +Когда коду требуется сложность, она должна добавляться обдуманно. Обычно это +необходимо, если требуется дополнительная производительность или если у +конкретной библиотеки или сервиса есть несколько различных клиентов. Сложность +может быть оправдана, но она должна сопровождаться документацией, чтобы клиенты +и будущие сопровождающие могли понять и ориентироваться в ней. Это должно +дополняться тестами и примерами, демонстрирующими ее правильное использование, +особенно если существует как "простой", так и "сложный" способ использования +кода. + +Этот принцип не означает, что сложный код нельзя или не следует писать на Go, +или что коду на Go не разрешено быть сложным. Мы стремимся к кодовой базе, +которая избегает ненужной сложности, чтобы, когда сложность появляется, это +указывало на то, что рассматриваемый код требует внимания для понимания и +поддержки. В идеале должны быть сопроводительные комментарии, объясняющие +обоснование и указывающие на меры предосторожности. Это часто возникает при +оптимизации кода для производительности; это часто требует более сложного +подхода, например, предварительного выделения буфера и его повторного +использования в течение времени жизни горутины. Когда сопровождающий видит это, +это должно быть сигналом, что рассматриваемый код критичен для +производительности, и это должно влиять на осторожность при внесении будущих +изменений. С другой стороны, если эта сложность применена без необходимости, она +становится обузой для тех, кому нужно читать или изменять код в будущем. + +Если код оказывается очень сложным, в то время как его цель должна быть простой, +это часто сигнал пересмотреть реализацию, чтобы увидеть, есть ли более простой +способ достичь того же. + +<a id="least-mechanism"></a> + +#### Наименьшая механизация (Least mechanism) + +Если есть несколько способов выразить одну и ту же идею, предпочтите тот, +который использует наиболее стандартные инструменты. Сложные механизмы часто +существуют, но не должны применяться без причины. Легко добавить сложность в код +по мере необходимости, тогда как гораздо сложнее удалить существующую сложность +после того, как выяснилось, что она не нужна. + +1. Стремитесь использовать базовую конструкцию языка (например, канал, срез + (slice), мапу (map), цикл или структуру (struct)), если этого достаточно для + вашего случая использования. +2. Если такой нет, ищите инструмент в стандартной библиотеке (например, + HTTP-клиент или механизм шаблонов (template engine)). +3. Наконец, рассмотрите, есть ли в кодовой базе Google основная библиотека, + которой достаточно, прежде чем вводить новую зависимость или создавать свою + собственную. + +В качестве примера рассмотрим production-код, содержащий флаг, привязанный к +переменной со значением по умолчанию, которое должно быть переопределено в +тестах. Если не предполагается тестирование самого интерфейса командной строки +программы (скажем, с помощью `os/exec`), проще и поэтому предпочтительнее +переопределить привязанное значение напрямую, а не с помощью `flag.Set`. + +Аналогично, если фрагменту кода требуется проверка принадлежности к множеству +(set membership check), часто достаточно мапы с булевыми значениями (например, +`map[string]bool`). Библиотеки, предоставляющие типы и функциональность, похожие +на множества (set), следует использовать только в том случае, если требуются +более сложные операции, которые невозможны или чрезмерно сложны с мапой. + +<a id="concision"></a> + +### Лаконичность (Concision) + +Лаконичный код на Go имеет высокое отношение сигнала к шуму. Легко различить +соответствующие детали, а именование и структура направляют читателя через эти +детали. + +Многое может помешать выделению наиболее важных деталей в любой момент: + +* Повторяющийся код +* Лишний синтаксис +* [Непонятные имена](#naming) +* Ненужная абстракция +* Пробелы + +Повторяющийся код особенно затмевает различия между каждым почти идентичным +разделом и требует от читателя визуального сравнения похожих строк кода, чтобы +найти изменения. [Табличное тестирование (Table-driven testing)] — хороший +пример механизма, который может лаконично вынести общий код за рамки важных +деталей каждого повторения, но выбор того, какие части включить в таблицу, +повлияет на то, насколько легко таблицу понять. + +При рассмотрении нескольких способов структурирования кода стоит подумать, какой +способ делает важные детали наиболее очевидными. + +Понимание и использование общих конструкций кода и идиом также важны для +поддержания высокого отношения сигнала к шуму. Например, следующий блок кода +очень распространен при [обработке ошибок (error handling)], и читатель может +быстро понять его назначение. + +```go +// Хорошо: +if err := doSomething(); err != nil { + // ... +} +``` + +Если код выглядит очень похоже, но имеет тонкое отличие, читатель может не +заметить изменение. В таких случаях стоит намеренно ["усилить" сигнал] проверки +ошибки, добавив комментарий, чтобы привлечь к нему внимание. + +```go +// Хорошо: +if err := doSomething(); err == nil { // если ошибки НЕТ + // ... +} +``` + +[Табличное тестирование (Table-driven testing)]: + https://github.com/golang/go/wiki/TableDrivenTests +[обработке ошибок (error handling)]: https://go.dev/blog/errors-are-values +["усилить" сигнал]: best-practices#signal-boost + +<a id="maintainability"></a> + +### Поддерживаемость (Maintainability) + +Код редактируется гораздо чаще, чем пишется. Читаемый код не только понятен +читателю, который пытается понять, как он работает, но и программисту, которому +нужно его изменить. Ключевое значение имеет понятность. + +Поддерживаемый код: + +* Легко модифицируется будущим программистом правильно +* Имеет API, структурированные таким образом, что они могут элегантно + развиваться +* Четко указывает предположения, которые он делает, и выбирает абстракции, + которые соответствуют структуре проблемы, а не структуре кода +* Избегает ненужной связности и не включает неиспользуемые функции +* Имеет комплексный набор тестов, чтобы гарантировать сохранение заявленного + поведения и корректность важной логики, а тесты предоставляют четкие, + действенные диагностические сообщения в случае неудачи + +При использовании абстракций, таких как интерфейсы и типы, которые по +определению удаляют информацию из контекста, в котором они используются, важно +обеспечить, чтобы они приносили достаточную пользу. Редакторы и IDE могут +напрямую подключаться к определению метода и показывать соответствующую +документацию при использовании конкретного типа, но в противном случае могут +ссылаться только на определение интерфейса. Интерфейсы — мощный инструмент, но +они имеют свою цену, поскольку сопровождающему, возможно, потребуется понять +особенности базовой реализации, чтобы правильно использовать интерфейс, что +должно быть объяснено в документации интерфейса или в месте вызова (call-site). + +Поддерживаемый код также избегает скрытия важных деталей в местах, которые легко +упустить из виду. Например, в каждой из следующих строк кода наличие или +отсутствие одного символа имеет критическое значение для понимания строки: + +```go +// Плохо: +// Использование = вместо := может полностью изменить эту строку. +if user, err = db.UserByID(userID); err != nil { + // ... +} +``` + +```go +// Плохо: +// Символ ! в середине этой строки очень легко пропустить. +leap := (year%4 == 0) && (!(year%100 == 0) || (year%400 == 0)) +``` + +Ни один из этих примеров не является неверным, но оба могут быть написаны в +более явной форме или могут иметь сопровождающий комментарий, привлекающий +внимание к важному поведению: + +```go +// Хорошо: +u, err := db.UserByID(userID) +if err != nil { + return fmt.Errorf("invalid origin user: %s", err) +} +user = u +``` + +```go +// Хорошо: +// Григорианские високосные годы — это не просто year%4 == 0. +// См. https://en.wikipedia.org/wiki/Leap_year#Algorithm. +var ( + leap4 = year%4 == 0 + leap100 = year%100 == 0 + leap400 = year%400 == 0 +) +leap := leap4 && (!leap100 || leap400) +``` + +Таким же образом вспомогательная функция, скрывающая критическую логику или +важный крайний случай (edge-case), может облегчить ситуации, когда будущее +изменение не учитывает их должным образом. + +Предсказуемые имена — еще одна особенность поддерживаемого кода. Пользователь +пакета или сопровождающий фрагмента кода должны иметь возможность предсказать +имя переменной, метода или функции в данном контексте. Параметры функций и имена +получателей (receiver) для идентичных концепций обычно должны иметь одно и то же +имя, как для понятности документации, так и для облегчения рефакторинга кода с +минимальными затратами. + +Поддерживаемый код минимизирует свои зависимости (как явные, так и неявные). +Зависимость от меньшего количества пакетов означает меньше строк кода, которые +могут повлиять на поведение. Избегание зависимостей от внутреннего или +недокументированного поведения делает код менее обременительным для поддержки +при изменении этого поведения в будущем. + +При обдумывании того, как структурировать или писать код, стоит потратить время +на размышления о том, как код может развиваться с течением времени. Если данный +подход способствует более легким и безопасным будущим изменениям, это часто +является хорошим компромиссом, даже если это означает несколько более сложный +дизайн. + +<a id="consistency"></a> + +### Согласованность (Consistency) + +Согласованный код — это код, который выглядит, ощущается и ведет себя так же, +как и аналогичный код во всей кодовой базе, в контексте команды или пакета и +даже в пределах одного файла. + +Соображения согласованности не отменяют ни один из вышеперечисленных принципов, +но если необходимо принять решение, часто полезно решить его в пользу +согласованности. + +Согласованность внутри пакета часто является наиболее важным уровнем +согласованности. Может быть очень неприятно, если одна и та же проблема решается +несколькими способами в пределах пакета или если одна концепция имеет много имен +в пределах файла. Однако даже это не должно перевешивать задокументированные +принципы стиля или глобальную согласованность. + +<a id="core"></a> + +## Основные рекомендации (Core guidelines) + +Эти рекомендации собирают наиболее важные аспекты стиля Go, которым, как +ожидается, следует весь код на Go. Мы ожидаем, что эти принципы будут изучены и +соблюдены к моменту получения права на ревью кода (readability). Они не должны +часто меняться, и новые дополнения должны преодолеть высокий барьер. + +Приведенные ниже рекомендации расширяют рекомендации из [Effective Go], которые +обеспечивают общую основу для кода на Go во всем сообществе. + +[Effective Go]: https://go.dev/doc/effective_go + +<a id="formatting"></a> + +### Форматирование (Formatting) + +Все исходные файлы Go должны соответствовать формату, выводимому инструментом +`gofmt`. Этот формат обеспечивается проверкой перед отправкой (presubmit check) +в кодовой базе Google. [Сгенерированный код] также обычно должен +форматироваться (например, с использованием [`format.Source`]), так как он также +просматривается в Code Search. + +[Сгенерированный код]: + https://docs.bazel.build/versions/main/be/general.html#genrule +[`format.Source`]: https://pkg.go.dev/go/format#Source + +<a id="mixed-caps"></a> + +### MixedCaps + +Исходный код Go использует `MixedCaps` или `mixedCaps` (верблюжий регистр, camel +case) вместо подчеркиваний (змеиный регистр, snake case) при написании составных +имен. + +Это применимо даже тогда, когда это нарушает соглашения в других языках. +Например, константа называется `MaxLength` (не `MAX_LENGTH`), если она +экспортируемая (exported), и `maxLength` (не `max_length`), если +неэкспортируемая (unexported). + +Локальные переменные считаются [неэкспортируемыми (unexported)] для целей выбора +начального регистра. + +<!--#include file="/go/g3doc/style/includes/special-name-exception.md"--> + +[неэкспортируемыми (unexported)]: https://go.dev/ref/spec#Exported_identifiers + +<a id="line-length"></a> + +### Длина строки (Line length) + +Не существует фиксированной длины строки для исходного кода Go. Если строка +кажется слишком длинной, предпочтительнее рефакторинг, чем ее разделение. Если +строка уже настолько короткая, насколько это практично, ей следует позволить +оставаться длинной. + +Не разделяйте строку: + +* Перед [изменением отступа (indentation + change)](https://neonxp.ru/pages/gostyleguide/google/decisions/#indentation-confusion) (например, объявление функции, + условие) +* Чтобы длинная строка (например, URL) поместилась в несколько более коротких + строк + +<a id="naming"></a> + +### Именование (Naming) + +Именование — это скорее искусство, чем наука. В Go имена, как правило, несколько +короче, чем во многих других языках, но применяются те же [общие принципы]. +Имена должны: + +* Не казаться [избыточными (repetitive)](https://neonxp.ru/pages/gostyleguide/google/decisions/#repetition) при + использовании +* Учитывать контекст +* Не повторять концепции, которые уже ясны + +Более конкретные рекомендации по именованию можно найти в [решениях +(https://neonxp.ru/pages/gostyleguide/google/decisions/)](https://neonxp.ru/pages/gostyleguide/google/decisions/#naming). + +[общие принципы]: + https://testing.googleblog.com/2017/10/code-health-identifiernamingpostforworl.html + +<a id="local-consistency"></a> + +### Локальная согласованность (Local consistency) + +Если в руководстве по стилю ничего не сказано по конкретному вопросу стиля, +авторы могут свободно выбирать предпочтительный стиль, если только код в +непосредственной близости (обычно в том же файле или пакете, но иногда в +пределах каталога команды или проекта) не занял последовательную позицию по +этому вопросу. + +Примеры **допустимых** соображений локального стиля: + +* Использование `%s` или `%v` для форматированного вывода ошибок +* Использование буферизованных каналов вместо мьютексов + +Примеры **недопустимых** соображений локального стиля: + +* Ограничения на длину строк для кода +* Использование библиотек тестирования на основе утверждений (assertion-based) + +Если локальный стиль противоречит руководству по стилю, но влияние на читаемость +ограничено одним файлом, это обычно будет отмечено в ревью кода, для которого +согласованное исправление выходит за рамки данного CL (change list). В этом +случае уместно завести задачу (bug) для отслеживания исправления. + +Если изменение ухудшит существующее отклонение от стиля, выставит его в большем +количестве API, увеличит количество файлов, в которых присутствует отклонение, +или внесет фактическую ошибку, то локальная согласованность больше не является +допустимым обоснованием для нарушения руководства по стилю в новом коде. В этих +случаях автору уместно либо очистить существующую кодовую базу в том же CL, либо +выполнить рефакторинг перед текущим CL, либо найти альтернативу, которая, по +крайней мере, не усугубляет локальную проблему.
\ No newline at end of file diff --git a/content/pages/gostyleguide/google/main.md b/content/pages/gostyleguide/google/main.md new file mode 100644 index 0000000..b3714ee --- /dev/null +++ b/content/pages/gostyleguide/google/main.md @@ -0,0 +1,184 @@ +--- +order: 1 +title: Google Go Style Guide — Руководство +--- + +## О руководстве + +<!--more --> + +Руководство по стилю Go и сопутствующие документы кодифицируют современные +наилучшие подходы к написанию читаемого и идиоматичного кода на Go. Следование +Руководству по стилю не является абсолютным требованием, и эти документы никогда +не будут исчерпывающими. Наша цель — минимизировать неопределённость при +написании читаемого кода на Go, чтобы новички в языке могли избежать +распространённых ошибок. Руководство по стилю также служит для унификации +рекомендаций по стилю, даваемых любым рецензентом кода Go в Google. + +| Документ | Ссылка | Основная аудитория | [Нормативный] | [Канонический] | +| ------------------------ | ----------------------------------------------------- | ------------------------ | ------------- | -------------- | +| **Руководство по стилю** | https://google.github.io/styleguide/go/guide | Все | Да | Да | +| **Решения по стилю** | https://google.github.io/styleguide/go/decisions | Наставники по читаемости | Да | Нет | +| **Лучшие практики** | https://google.github.io/styleguide/go/best-practices | Все заинтересованные | Нет | Нет | + +[Нормативный]: #нормативный +[Канонический]: #канонический + +<a id="docs"></a> + +### Документы + +1. **[Руководство по стилю](https://google.github.io/styleguide/go/guide)** + описывает основы стиля Go в Google. Этот документ является окончательным и + служит основой для рекомендаций в «Решениях по стилю» и «Лучших практиках». + +1. **[Решения по стилю](https://google.github.io/styleguide/go/decisions)** — + это более подробный документ, который суммирует решения по конкретным + вопросам стиля и, где уместно, обсуждает обоснование этих решений. + + Эти решения могут иногда меняться на основе новых данных, новых возможностей + языка, новых библиотек или возникающих паттернов, но не ожидается, что + отдельные программисты Go в Google должны следить за актуальностью этого + документа. + +1. **[Лучшие практики](https://google.github.io/styleguide/go/best-practices)** + документируют некоторые паттерны, которые развивались со временем для + решения общих задач, хорошо читаются и устойчивы к потребностям поддержки + кода. + + Эти лучшие практики не являются каноническими, но программистам Go в Google + рекомендуется использовать их там, где это возможно, для сохранения + единообразия и согласованности кодовой базы. + +Эти документы призваны: + +- Согласовать набор принципов для оценки альтернативных стилей +- Кодифицировать устоявшиеся вопросы стиля Go +- Документировать и предоставить канонические примеры идиом Go +- Документировать плюсы и минусы различных решений по стилю +- Помочь минимизировать неожиданности при рецензировании читаемости кода Go +- Помочь наставникам по читаемости использовать согласованную терминологию и + рекомендации + +Эти документы **не** призваны: + +- Быть исчерпывающим списком замечаний, которые можно дать при рецензировании + читаемости +- Перечислять все правила, которые каждый должен помнить и всегда соблюдать +- Заменять здравый смысл при использовании возможностей языка и стиля +- Оправдывать масштабные изменения для устранения различий в стиле + +Всегда будут существовать различия между разными программистами Go и между +кодовыми базами разных команд. Однако в интересах Google и Alphabet, чтобы наша +кодовая база была как можно более согласованной. (Подробнее о согласованности +см. [руководство](https://neonxp.ru/pages/gostyleguide/google/guide/#consistency)). В связи с этим не стесняйтесь вносить +улучшения стиля по мере необходимости, но вам не нужно придираться к каждому +нарушению Руководства по стилю, которое вы обнаружите. В частности, эти +документы могут меняться со временем, и это не повод вызывать лишнюю суету в +существующих кодовых базах; достаточно писать новый код, используя новейшие +лучшие практики, и со временем устранять проблемы поблизости. + +Важно понимать, что вопросы стиля по своей природе субъективны и всегда +сопряжены с компромиссами. Большая часть рекомендаций в этих документах +субъективна, но, как и в случае с `gofmt`, в обеспечиваемом ими единообразии +есть значительная ценность. Поэтому рекомендации по стилю не будут меняться без +должного обсуждения, и программистам Go в Google рекомендуется следовать +руководству по стилю, даже если они с чем-то не согласны. + +<a id="definitions"></a> + +## Определения + +Ниже приведены определения следующих слов, которые используются во всех +документах по стилю: + +- **Канонический**: Устанавливает предписывающие и долговечные правила <a + id="canonical"></a> + + В этих документах «канонический» используется для описания чего-либо, что + считается стандартом, которому должен следовать весь код (старый и новый) и + который не должен существенно меняться с течением времени. Принципы в + канонических документах должны быть понятны как авторам, так и рецензентам, + поэтому всё, что включается в канонический документ, должно соответствовать + высоким стандартам. Как таковые, канонические документы обычно короче и + предписывают меньше элементов стиля, чем неканонические документы. + + https://google.github.io/styleguide/go#canonical + +- **Нормативный**: Призван установить согласованность <a id="normative"></a> + + В этих документах «нормативный» используется для описания чего-либо, что + является согласованным элементом стиля для использования рецензентами кода + Go, чтобы предложения, терминология и обоснования были последовательными. + Эти элементы могут меняться со временем, и эти документы будут отражать + такие изменения, чтобы рецензенты могли оставаться последовательными и в + курсе событий. От авторов кода на Go не ожидается знакомства с нормативными + документами, но рецензенты будут часто использовать их в качестве + справочного материала при проверке читаемости. + + https://google.github.io/styleguide/go#normative + +- **Идиоматичный**: Распространённый и знакомый <a id="idiomatic"></a> + + В этих документах «идиоматичный» используется для обозначения чего-либо, что + широко распространено в коде на Go и стало знакомым паттерном, который легко + узнать. В целом, идиоматичный паттерн следует предпочитать неидиоматичному, + если оба служат одной цели в контексте, поскольку именно это будет наиболее + знакомо читателям. + + https://google.github.io/styleguide/go#idiomatic + +<a id="references"></a> + +## Дополнительные ссылки + +Данное руководство предполагает, что читатель знаком с [Effective Go], поскольку +оно обеспечивает общую основу для кода на Go во всём сообществе Go. + +Ниже приведены некоторые дополнительные ресурсы для тех, кто хочет +самостоятельно изучить стиль Go, и для рецензентов, желающих предоставить в +своих отзывах дополнительный контекст с ссылками. От участников процесса +проверки читаемости Go не ожидается знакомства с этими ресурсами, но они могут +упоминаться в качестве контекста при таких проверках. + +[Effective Go]: https://go.dev/doc/effective_go + +**Внешние ссылки** + +- [Спецификация языка Go](https://go.dev/ref/spec) +- [Часто задаваемые вопросы по Go](https://go.dev/doc/faq) +- [Модель памяти Go](https://go.dev/ref/mem) +- [Структуры данных в Go](https://research.swtch.com/godata) +- [Интерфейсы в Go](https://research.swtch.com/interfaces) +- [Поговорки Go](https://go-proverbs.github.io/) + +- <a id="gotip"></a> Выпуски Go Tip — следите за обновлениями. + +- <a id="unit-testing-practices"></a> Практики модульного тестирования — + следите за обновлениями. + +**Соответствующие статьи Testing-on-the-Toilet** + +- [TotT: Именование идентификаторов][tott-431] +- [TotT: Тестирование состояния vs. Тестирование взаимодействий][tott-281] +- [TotT: Эффективное тестирование][tott-324] +- [TotT: Тестирование, основанное на рисках][tott-329] +- [TotT: Детекторные тесты считаются вредными][tott-350] + +[tott-431]: https://testing.googleblog.com/2017/10/code-health-identifiernamingpostforworl.html +[tott-281]: https://testing.googleblog.com/2013/03/testing-on-toilet-testing-state-vs.html +[tott-324]: https://testing.googleblog.com/2014/05/testing-on-toilet-effective-testing.html +[tott-329]: https://testing.googleblog.com/2014/05/testing-on-toilet-risk-driven-testing.html +[tott-350]: https://testing.googleblog.com/2015/01/testing-on-toilet-change-detector-tests.html + +**Дополнительные внешние материалы** + +- [Go и догма](https://research.swtch.com/dogma) +- [Меньше — значит экспоненциально + больше](https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html) +- [Воображение + Эсмеральды](https://commandcenter.blogspot.com/2011/12/esmereldas-imagination.html) +- [Регулярные выражения для синтаксического + анализа](https://commandcenter.blogspot.com/2011/08/regular-expressions-in-lexing-and.html) +- [Стиль Gofmt никому не нравится, но Gofmt нравится + всем](https://www.youtube.com/watch?v=PAAkCSZUG1c&t=8m43s) (YouTube) diff --git a/content/pages/gostyleguide/uber/index.md b/content/pages/gostyleguide/uber/index.md new file mode 100644 index 0000000..a8e74fd --- /dev/null +++ b/content/pages/gostyleguide/uber/index.md @@ -0,0 +1,4140 @@ +--- +order: 10 +title: Uber Go Style Guide +--- + +# Руководство по стилю Uber для Go + +Оригинал: https://github.com/uber-go/guide/blob/master/style.md + +<!--more--> + +- [Введение](#введение) +- [Рекомендации](#рекомендации) + - [Указатели на интерфейсы](#указатели-на-интерфейсы) + - [Проверка соответствия интерфейсу](#проверка-соответствия-интерфейсу) + - [Получатели и интерфейсы](#получатели-и-интерфейсы) + - [Нулевые значения мьютексов + допустимы](#нулевые-значения-мьютексов-допустимы) + - [Копируйте срезы и карты на границах](#копируйте-срезы-и-карты-на-границах) + - [Используйте `defer` для очистки](#используйте-defer-для-очистки) + - [Размер канала — один или ноль](#размер-канала-один-или-ноль) + - [Начинайте перечисления с единицы](#начинайте-перечисления-с-единицы) + - [Используйте `"time"` для работы со + временем](#используйте-time-для-работы-со-временем) + - [Ошибки](#ошибки) + - [Типы ошибок](#типы-ошибок) + - [Обёртывание ошибок](#обёртывание-ошибок) + - [Именование ошибок](#именование-ошибок) + - [Обрабатывайте ошибки один раз](#обрабатывайте-ошибки-один-раз) + - [Обрабатывайте сбои утверждения типа](#обрабатывайте-сбои-утверждения-типа) + - [Не паникуйте](#не-паникуйте) + - [Используйте go.uber.org/atomic](#используйте-gouberorgatomic) + - [Избегайте изменяемых глобальных + переменных](#избегайте-изменяемых-глобальных-переменных) + - [Избегайте встраивания типов в публичные + структуры](#избегайте-встраивания-типов-в-публичные-структуры) + - [Избегайте использования встроенных + имён](#избегайте-использования-встроенных-имён) + - [Избегайте `init()`](#избегайте-init) + - [Завершение программы в main](#завершение-программы-в-main) + - [Завершайте программу один раз](#завершайте-программу-один-раз) + - [Используйте теги полей в структурах для + сериализации](#используйте-теги-полей-в-структурах-для-сериализации) + - [Не запускайте горутины по принципу «запустил и + забыл»](#не-запускайте-горутины-по-принципу-запустил-и-забыл) + - [Дожидайтесь завершения горутин](#дожидайтесь-завершения-горутин) + - [Не используйте горутины в `init()`](#не-используйте-горутины-в-init) +- [Производительность](#производительность) + - [Предпочитайте strconv вместо fmt](#предпочитайте-strconv-вместо-fmt) + - [Избегайте повторного преобразования строк в + байты](#избегайте-повторного-преобразования-строк-в-байты) + - [Предпочитайте указание ёмкости + контейнеров](#предпочитайте-указание-ёмкости-контейнеров) +- [Стиль](#стиль) + - [Избегайте слишком длинных строк](#избегайте-слишком-длинных-строк) + - [Будьте последовательны](#будьте-последовательны) + - [Группируйте схожие объявления](#группируйте-схожие-объявления) + - [Порядок групп импорта](#порядок-групп-импорта) + - [Имена пакетов](#имена-пакетов) + - [Имена функций](#имена-функций) + - [Псевдонимы импорта](#псевдонимы-импорта) + - [Группировка и порядок функций](#группировка-и-порядок-функций) + - [Уменьшайте вложенность](#уменьшайте-вложенность) + - [Избыточный Else](#избыточный-else) + - [Объявления переменных верхнего + уровня](#объявления-переменных-верхнего-уровня) + - [Префикс \_ для неэкспортируемых глобальных + переменных](#префикс-_-для-неэкспортируемых-глобальных-переменных) + - [Встраивание в структурах](#встраивание-в-структурах) + - [Объявление локальных переменных](#объявление-локальных-переменных) + - [nil — это валидный срез](#nil-это-валидный-срез) + - [Уменьшайте область видимости + переменных](#уменьшайте-область-видимости-переменных) + - [Избегайте «голых» параметров](#избегайте-голых-параметров) + - [Используйте сырые строковые литералы, чтобы избежать + экранирования](#используйте-сырые-строковые-литералы-чтобы-избежать-экранирования) + - [Инициализация структур](#инициализация-структур) + - [Используйте имена полей для инициализации + структур](#используйте-имена-полей-для-инициализации-структур) + - [Опускайте поля с нулевыми значениями в + структурах](#опускайте-поля-с-нулевыми-значениями-в-структурах) + - [Используйте `var` для структур с нулевыми + значениями](#используйте-var-для-структур-с-нулевыми-значениями) + - [Инициализация ссылок на структуры](#инициализация-ссылок-на-структуры) + - [Инициализация карт](#инициализация-карт) + - [Строки формата вне Printf](#строки-формата-вне-printf) + - [Именование функций в стиле Printf](#именование-функций-в-стиле-printf) +- [Паттерны](#паттерны) + - [Табличные тесты](#табличные-тесты) + - [Функциональные опции](#функциональные-опции) +- [Линтинг](#линтинг) + +## Введение + +Стили — это соглашения, которые регулируют наш код. Термин «стиль» немного +неудачен, поскольку эти соглашения охватывают гораздо больше, чем просто +форматирование исходных файлов — этим занимается `gofmt`. + +Цель этого руководства — управлять этой сложностью, подробно описывая, что +следует и чего не следует делать при написании кода на Go в Uber. Эти правила +существуют, чтобы кодовая база оставалась управляемой, позволяя при этом +инженерам эффективно использовать возможности языка Go. + +Первоначально это руководство было создано [Прашантом +Варанаси](https://github.com/prashantv) и [Саймоном +Ньютоном](https://github.com/nomis52) как способ познакомить некоторых коллег с +использованием Go. С годами оно было дополнено на основе отзывов других. + +В этом документе описаны идиоматические соглашения в коде на Go, которым мы +следуем в Uber. Многие из них являются общими рекомендациями для Go, в то время +как другие расширяют внешние ресурсы: + +1. [Effective Go](https://go.dev/doc/effective_go) +2. [Go Common Mistakes](https://go.dev/wiki/CommonMistakes) +3. [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments) + +Мы стремимся к тому, чтобы примеры кода были корректны для двух последних +минорных версий [релизов Go](https://go.dev/doc/devel/release). + +Весь код должен быть свободен от ошибок при проверке с помощью `golint` и `go +vet`. Мы рекомендуем настроить ваш редактор так, чтобы он: + +- Запускал `goimports` при сохранении +- Запускал `golint` и `go vet` для проверки на ошибки + +Информацию о поддержке инструментов Go в редакторах можно найти здесь: +https://go.dev/wiki/IDEsAndTextEditorPlugins + +## Рекомендации + +### Указатели на интерфейсы + +Почти никогда не нужен указатель на интерфейс. Следует передавать интерфейсы как +значения — базовые данные при этом всё равно могут быть указателем. + +Интерфейс состоит из двух полей: + +1. Указатель на информацию, специфичную для типа. Можно думать об этом как о + «типе». +2. Указатель на данные. Если хранимые данные являются указателем, они + сохраняются напрямую. Если хранимые данные являются значением, то сохраняется + указатель на это значение. + +Если нужно, чтобы методы интерфейса изменяли базовые данные, необходимо +использовать указатель. + +### Проверка соответствия интерфейсу + +Проверяйте соответствие интерфейсу на этапе компиляции там, где это уместно. Это +включает: + +- Экспортируемые типы, которые должны реализовывать определённые интерфейсы как + часть своего API-контракта +- Экспортируемые или неэкспортируемые типы, которые являются частью коллекции + типов, реализующих один и тот же интерфейс +- Другие случаи, когда нарушение интерфейса приведёт к поломке пользователей + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +type Handler struct { + // ... +} + + + +func (h *Handler) ServeHTTP( + w http.ResponseWriter, + r *http.Request, +) { + ... +} +``` + +</td><td> + +```go +type Handler struct { + // ... +} + +var _ http.Handler = (*Handler)(nil) + +func (h *Handler) ServeHTTP( + w http.ResponseWriter, + r *http.Request, +) { + // ... +} +``` + +</td></tr> +</tbody></table> + +Выражение `var _ http.Handler = (*Handler)(nil)` не скомпилируется, если +`*Handler` перестанет соответствовать интерфейсу `http.Handler`. + +Правая часть присваивания должна быть нулевым значением утверждаемого типа. Это +`nil` для типов-указателей (например, `*Handler`), срезов и карт, и пустая +структура для типов-структур. + +```go +type LogHandler struct { + h http.Handler + log *zap.Logger +} + +var _ http.Handler = LogHandler{} + +func (h LogHandler) ServeHTTP( + w http.ResponseWriter, + r *http.Request, +) { + // ... +} +``` + +### Получатели и интерфейсы + +Методы со значением-получателем могут вызываться как на указателях, так и на +значениях. Методы с указателем-получателем могут вызываться только на указателях +или [адресуемых значениях](https://go.dev/ref/spec#Method_values). + +Например, + +```go +type S struct { + data string +} + +func (s S) Read() string { + return s.data +} + +func (s *S) Write(str string) { + s.data = str +} + +// Мы не можем получить указатели на значения, хранящиеся в картах, потому что они не являются адресуемыми значениями. +sVals := map[int]S{1: {"A"}} + +// Мы можем вызвать Read для значений, хранящихся в карте, потому что Read имеет получатель-значение, который не требует, чтобы значение было адресуемым. +sVals[1].Read() + +// Мы не можем вызвать Write для значений, хранящихся в карте, потому что Write имеет получатель-указатель, и невозможно получить указатель на значение, хранящееся в карте. +// +// sVals[1].Write("test") + +sPtrs := map[int]*S{1: {"A"}} + +// Вы можете вызвать как Read, так и Write, если карта хранит указатели, потому что указатели по своей природе адресуемы. +sPtrs[1].Read() +sPtrs[1].Write("test") +``` + +Аналогично, интерфейс может быть удовлетворён указателем, даже если метод имеет +получатель-значение. + +```go +type F interface { + f() +} + +type S1 struct{} + +func (s S1) f() {} + +type S2 struct{} + +func (s *S2) f() {} + +s1Val := S1{} +s1Ptr := &S1{} +s2Val := S2{} +s2Ptr := &S2{} + +var i F +i = s1Val +i = s1Ptr +i = s2Ptr + +// Следующее не скомпилируется, так как s2Val является значением, и для f нет получателя-значения. +// i = s2Val +``` + +В Effective Go есть хорошее описание по теме [Указатели против +значений](https://go.dev/doc/effective_go#pointers_vs_values). + +### Нулевые значения мьютексов допустимы + +Нулевое значение `sync.Mutex` и `sync.RWMutex` является допустимым, поэтому +почти никогда не нужен указатель на мьютекс. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +mu := new(sync.Mutex) +mu.Lock() +``` + +</td><td> + +```go +var mu sync.Mutex +mu.Lock() +``` + +</td></tr> +</tbody></table> + +Если вы используете структуру по указателю, то мьютекс должен быть не +указателем, а полем в ней. Не встраивайте мьютекс в структуру, даже если +структура не экспортируется. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +type SMap struct { + sync.Mutex + + data map[string]string +} + +func NewSMap() *SMap { + return &SMap{ + data: make(map[string]string), + } +} + +func (m *SMap) Get(k string) string { + m.Lock() + defer m.Unlock() + + return m.data[k] +} +``` + +</td><td> + +```go +type SMap struct { + mu sync.Mutex + + data map[string]string +} + +func NewSMap() *SMap { + return &SMap{ + data: make(map[string]string), + } +} + +func (m *SMap) Get(k string) string { + m.mu.Lock() + defer m.mu.Unlock() + + return m.data[k] +} +``` + +</td></tr> + +<tr><td> + +Поле `Mutex`, а также методы `Lock` и `Unlock` непреднамеренно становятся частью +публичного API `SMap`. + +</td><td> + +Мьютекс и его методы являются деталями реализации `SMap`, скрытыми от его +вызывающих сторон. + +</td></tr> +</tbody></table> + +### Копируйте срезы и карты на границах + +Срезы и карты содержат указатели на базовые данные, поэтому будьте осторожны в +ситуациях, когда их нужно скопировать. + +#### Получение срезов и карт + +Помните, что пользователи могут изменить карту или срез, которые вы получили в +качестве аргумента, если сохраните ссылку на них. + +<table> +<thead><tr><th>Плохо</th> <th>Хорошо</th></tr></thead> +<tbody> +<tr> +<td> + +```go +func (d *Driver) SetTrips(trips []Trip) { + d.trips = trips +} + +trips := ... +d1.SetTrips(trips) + +// Вы хотели изменить d1.trips? +trips[0] = ... +``` + +</td> +<td> + +```go +func (d *Driver) SetTrips(trips []Trip) { + d.trips = make([]Trip, len(trips)) + copy(d.trips, trips) +} + +trips := ... +d1.SetTrips(trips) + +// Теперь мы можем изменить trips[0], не затрагивая d1.trips. +trips[0] = ... +``` + +</td> +</tr> + +</tbody> +</table> + +#### Возврат срезов и карт + +Аналогично, будьте осторожны с модификациями карт или срезов, раскрывающих +внутреннее состояние. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +type Stats struct { + mu sync.Mutex + counters map[string]int +} + +// Snapshot возвращает текущую статистику. +func (s *Stats) Snapshot() map[string]int { + s.mu.Lock() + defer s.mu.Unlock() + + return s.counters +} + +// snapshot больше не защищён мьютексом, поэтому любой доступ к snapshot подвержен состоянию гонки. +snapshot := stats.Snapshot() +``` + +</td><td> + +```go +type Stats struct { + mu sync.Mutex + counters map[string]int +} + +func (s *Stats) Snapshot() map[string]int { + s.mu.Lock() + defer s.mu.Unlock() + + result := make(map[string]int, len(s.counters)) + for k, v := range s.counters { + result[k] = v + } + return result +} + +// Snapshot теперь является копией. +snapshot := stats.Snapshot() +``` + +</td></tr> +</tbody></table> + +### Используйте `defer` для очистки + +Используйте `defer` для очистки ресурсов, таких как файлы и блокировки. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +p.Lock() +if p.count < 10 { + p.Unlock() + return p.count +} + +p.count++ +newCount := p.count +p.Unlock() + +return newCount + +// легко пропустить разблокировки из-за множественных возвратов +``` + +</td><td> + +```go +p.Lock() +defer p.Unlock() + +if p.count < 10 { + return p.count +} + +p.count++ +return p.count + +// более читаемо +``` + +</td></tr> +</tbody></table> + +`defer` имеет крайне малые накладные расходы, и его следует избегать только если +вы можете доказать, что время выполнения вашей функции измеряется в +наносекундах. Выигрыш в читаемости от использования `defer` стоит той мизерной +стоимости, которую он вносит. Это особенно верно для больших методов, где +присутствуют не только простые операции доступа к памяти, а другие вычисления +более значимы, чем `defer`. + +### Размер канала — один или ноль + +Каналы обычно должны иметь размер один или быть небуферизированными. По +умолчанию каналы небуферизированы и имеют размер ноль. Любой другой размер +должен подвергаться тщательному анализу. Подумайте, как определяется размер, что +предотвращает заполнение канала под нагрузкой и блокировку писателей, и что +происходит, когда это случается. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +// Должно хватить на всех! +c := make(chan int, 64) +``` + +</td><td> + +```go +// Размер один +c := make(chan int, 1) // или +// Небуферизированный канал, размер ноль +c := make(chan int) +``` + +</td></tr> +</tbody></table> + +### Начинайте перечисления с единицы + +Стандартный способ введения перечислений в Go — объявление пользовательского +типа и группы `const` с `iota`. Поскольку переменные имеют значение по умолчанию +0, обычно следует начинать перечисления с ненулевого значения. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +type Operation int + +const ( + Add Operation = iota + Subtract + Multiply +) + +// Add=0, Subtract=1, Multiply=2 +``` + +</td><td> + +```go +type Operation int + +const ( + Add Operation = iota + 1 + Subtract + Multiply +) + +// Add=1, Subtract=2, Multiply=3 +``` + +</td></tr> +</tbody></table> + +Бывают случаи, когда использование нулевого значения имеет смысл, например, +когда случай с нулевым значением является желаемым поведением по умолчанию. + +```go +type LogOutput int + +const ( + LogToStdout LogOutput = iota + LogToFile + LogToRemote +) + +// LogToStdout=0, LogToFile=1, LogToRemote=2 +``` + +<!-- TODO: раздел о String методах для перечислений --> + +### Используйте `"time"` для работы со временем + +Время — это сложно. Часто делаются неверные предположения о времени, включая +следующие. + +1. В сутках 24 часа +2. В часе 60 минут +3. В неделе 7 дней +4. В году 365 дней +5. [И многое + другое](https://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time) + +Например, _1_ означает, что добавление 24 часов к моменту времени не всегда даст +новый календарный день. + +Поэтому всегда используйте пакет [`"time"`](https://pkg.go.dev/time) при работе +со временем, так как он помогает безопаснее и точнее справляться с этими +неверными предположениями. + +#### Используйте `time.Time` для моментов времени + +Используйте [`time.Time`](https://pkg.go.dev/time#Time) при работе с моментами +времени и методы `time.Time` для сравнения, добавления или вычитания времени. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +func isActive(now, start, stop int) bool { + return start <= now && now < stop +} +``` + +</td><td> + +```go +func isActive(now, start, stop time.Time) bool { + return (start.Before(now) || start.Equal(now)) && now.Before(stop) +} +``` + +</td></tr> +</tbody></table> + +#### Используйте `time.Duration` для промежутков времени + +Используйте [`time.Duration`](https://pkg.go.dev/time#Duration) при работе с +промежутками времени. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +func poll(delay int) { + for { + // ... + time.Sleep(time.Duration(delay) * time.Millisecond) + } +} + +poll(10) // это секунды или миллисекунды? +``` + +</td><td> + +```go +func poll(delay time.Duration) { + for { + // ... + time.Sleep(delay) + } +} + +poll(10*time.Second) +``` + +</td></tr> +</tbody></table> + +Возвращаясь к примеру добавления 24 часов к моменту времени, метод, который мы +используем для добавления времени, зависит от намерения. Если мы хотим получить +то же время суток, но на следующий календарный день, следует использовать +[`Time.AddDate`](https://pkg.go.dev/time#Time.AddDate). Однако, если мы хотим +момент времени, гарантированно наступающий через 24 часа после предыдущего, +следует использовать [`Time.Add`](https://pkg.go.dev/time#Time.Add). + +```go +newDay := t.AddDate(0 /* years */, 0 /* months */, 1 /* days */) +maybeNewDay := t.Add(24 * time.Hour) +``` + +#### Используйте `time.Time` и `time.Duration` с внешними системами + +По возможности используйте `time.Duration` и `time.Time` при взаимодействии с +внешними системами. Например: + +- Флаги командной строки: [`flag`](https://pkg.go.dev/flag) поддерживает + `time.Duration` через + [`time.ParseDuration`](https://pkg.go.dev/time#ParseDuration) +- JSON: [`encoding/json`](https://pkg.go.dev/encoding/json) поддерживает + кодирование `time.Time` как строки [RFC + 3339](https://tools.ietf.org/html/rfc3339) через свой [`UnmarshalJSON` + метод](https://pkg.go.dev/time#Time.UnmarshalJSON) +- SQL: [`database/sql`](https://pkg.go.dev/database/sql) поддерживает + преобразование столбцов `DATETIME` или `TIMESTAMP` в `time.Time` и обратно, + если базовый драйвер поддерживает это. +- YAML: [`gopkg.in/yaml.v2`](https://pkg.go.dev/gopkg.in/yaml.v2) поддерживает + `time.Time` как строку [RFC 3339](https://tools.ietf.org/html/rfc3339) и + `time.Duration` через + [`time.ParseDuration`](https://pkg.go.dev/time#ParseDuration). + +Если невозможно использовать `time.Duration` в этих взаимодействиях, используйте +`int` или `float64` и включайте единицу измерения в имя поля. + +Например, так как `encoding/json` не поддерживает `time.Duration`, единица +измерения включается в имя поля. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +// {"interval": 2} +type Config struct { + Interval int `json:"interval"` +} +``` + +</td><td> + +```go +// {"intervalMillis": 2000} +type Config struct { + IntervalMillis int `json:"intervalMillis"` +} +``` + +</td></tr> +</tbody></table> + +Если невозможно использовать `time.Time` в этих взаимодействиях, если не +согласована альтернатива, используйте `string` и форматируйте временные метки в +соответствии с [RFC 3339](https://tools.ietf.org/html/rfc3339). Этот формат +используется по умолчанию в +[`Time.UnmarshalText`](https://pkg.go.dev/time#Time.UnmarshalText) и доступен +для использования в `Time.Format` и `time.Parse` через +[`time.RFC3339`](https://pkg.go.dev/time#RFC3339). + +Хотя на практике это обычно не проблема, имейте в виду, что пакет `"time"` не +поддерживает разбор временных меток с високосными секундами +([8728](https://github.com/golang/go/issues/8728)), а также не учитывает +високосные секунды в вычислениях +([15190](https://github.com/golang/go/issues/15190)). Если вы сравниваете два +момента времени, разница не будет включать високосные секунды, которые могли +произойти между этими моментами. + +### Ошибки + +#### Типы ошибок + +Есть несколько вариантов объявления ошибок. Рассмотрите следующее, прежде чем +выбрать вариант, наиболее подходящий для вашего случая. + +- Нужно ли вызывающей стороне сопоставлять ошибку, чтобы обработать её? Если + да, мы должны поддерживать функции [`errors.Is`](https://pkg.go.dev/errors#Is) + или [`errors.As`](https://pkg.go.dev/errors#As) путём объявления переменной + ошибки верхнего уровня или пользовательского типа. +- Сообщение об ошибке — статическая строка или динамическая строка, требующая + контекстной информации? Для первого случая можно использовать + [`errors.New`](https://pkg.go.dev/errors#New), но для второго необходимо + использовать [`fmt.Errorf`](https://pkg.go.dev/fmt#Errorf) или + пользовательский тип ошибки. +- Мы распространяем новую ошибку, возвращённую нижележащей функцией? Если да, + см. [раздел об обёртывании ошибок](#обёртывание-ошибок). + +| Сопоставление ошибок? | Сообщение об ошибке | Рекомендация | +| --------------------- | ------------------- | -------------------------------------------------------------------------- | +| Нет | статическое | [`errors.New`](https://pkg.go.dev/errors#New) | +| Нет | динамическое | [`fmt.Errorf`](https://pkg.go.dev/fmt#Errorf) | +| Да | статическое | переменная верхнего уровня с [`errors.New`](https://pkg.go.dev/errors#New) | +| Да | динамическое | пользовательский тип `error` | + +Например, используйте [`errors.New`](https://pkg.go.dev/errors#New) для ошибки +со статической строкой. Экспортируйте эту ошибку как переменную, чтобы +поддерживать её сопоставление с `errors.Is`, если вызывающей стороне нужно +сопоставить и обработать эту ошибку. + +<table> +<thead><tr><th>Без сопоставления ошибок</th><th>С сопоставлением ошибок</th></tr></thead> +<tbody> +<tr><td> + +```go +// package foo + +func Open() error { + return errors.New("could not open") +} + +// package bar + +if err := foo.Open(); err != nil { + // Не можем обработать ошибку. + panic("unknown error") +} +``` + +</td><td> + +```go +// package foo + +var ErrCouldNotOpen = errors.New("could not open") + +func Open() error { + return ErrCouldNotOpen +} + +// package bar + +if err := foo.Open(); err != nil { + if errors.Is(err, foo.ErrCouldNotOpen) { + // обработать ошибку + } else { + panic("unknown error") + } +} +``` + +</td></tr> +</tbody></table> + +Для ошибки с динамической строкой используйте +[`fmt.Errorf`](https://pkg.go.dev/fmt#Errorf), если вызывающей стороне не нужно +её сопоставлять, и пользовательский `error`, если нужно. + +<table> +<thead><tr><th>Без сопоставления ошибок</th><th>С сопоставлением ошибок</th></tr></thead> +<tbody> +<tr><td> + +```go +// package foo + +func Open(file string) error { + return fmt.Errorf("file %q not found", file) +} + +// package bar + +if err := foo.Open("testfile.txt"); err != nil { + // Не можем обработать ошибку. + panic("unknown error") +} +``` + +</td><td> + +```go +// package foo + +type NotFoundError struct { + File string +} + +func (e *NotFoundError) Error() string { + return fmt.Sprintf("file %q not found", e.File) +} + +func Open(file string) error { + return &NotFoundError{File: file} +} + + +// package bar + +if err := foo.Open("testfile.txt"); err != nil { + var notFound *NotFoundError + if errors.As(err, ¬Found) { + // обработать ошибку + } else { + panic("unknown error") + } +} +``` + +</td></tr> +</tbody></table> + +Обратите внимание, что если вы экспортируете переменные или типы ошибок из +пакета, они станут частью публичного API пакета. + +#### Обёртывание ошибок + +Есть три основных варианта распространения ошибок при неудачном вызове: + +- вернуть исходную ошибку как есть +- добавить контекст с помощью `fmt.Errorf` и глагола `%w` +- добавить контекст с помощью `fmt.Errorf` и глагола `%v` + +Возвращайте исходную ошибку как есть, если нечего добавить к контексту. Это +сохраняет исходный тип и сообщение ошибки. Это хорошо подходит для случаев, +когда базовое сообщение об ошибке содержит достаточно информации для +отслеживания её происхождения. + +В противном случае добавляйте контекст к сообщению об ошибке, где это возможно, +чтобы вместо расплывчатой ошибки вроде "connection refused" вы получали более +полезные ошибки, такие как "call service foo: connection refused". + +Используйте `fmt.Errorf` для добавления контекста к вашим ошибкам, выбирая между +глаголами `%w` или `%v` в зависимости от того, должна ли вызывающая сторона +иметь возможность сопоставить и извлечь базовую причину. + +- Используйте `%w`, если вызывающая сторона должна иметь доступ к базовой + ошибке. Это хороший вариант по умолчанию для большинства обёрнутых ошибок, но + имейте в виду, что вызывающие стороны могут начать полагаться на это + поведение. Поэтому для случаев, когда обёрнутая ошибка является известной + `var` или типом, документируйте и тестируйте это как часть контракта вашей + функции. +- Используйте `%v`, чтобы скрыть базовую ошибку. Вызывающие стороны не смогут её + сопоставить, но вы сможете переключиться на `%w` в будущем, если потребуется. + +При добавлении контекста к возвращаемым ошибкам сохраняйте контекст кратким, +избегая фраз типа "failed to", которые констатируют очевидное и накапливаются по +мере всплытия ошибки по стеку: + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +s, err := store.New() +if err != nil { + return fmt.Errorf( + "failed to create new store: %w", err) +} +``` + +</td><td> + +```go +s, err := store.New() +if err != nil { + return fmt.Errorf( + "new store: %w", err) +} +``` + +</td></tr><tr><td> + +```plain +failed to x: failed to y: failed to create new store: the error +``` + +</td><td> + +```plain +x: y: new store: the error +``` + +</td></tr> +</tbody></table> + +Однако, как только ошибка отправляется в другую систему, должно быть понятно, +что сообщение является ошибкой (например, тег `err` или префикс "Failed" в +логах). + +См. также [Don't just check errors, handle them +gracefully](https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully). + +#### Именование ошибок + +Для значений ошибок, хранящихся как глобальные переменные, используйте префикс +`Err` или `err` в зависимости от того, экспортируются они или нет. Это указание +заменяет [Префикс \_ для неэкспортируемых глобальных +переменных](#префикс-_-для-неэкспортируемых-глобальных-переменных). + +```go +var ( + // Следующие две ошибки экспортируются, чтобы пользователи этого пакета могли сопоставлять их с errors.Is. + + ErrBrokenLink = errors.New("link is broken") + ErrCouldNotOpen = errors.New("could not open") + + // Эта ошибка не экспортируется, потому что мы не хотим делать её частью нашего публичного API. Мы всё ещё можем использовать её внутри пакета с errors.Is. + + errNotFound = errors.New("not found") +) +``` + +Для пользовательских типов ошибок используйте суффикс `Error` вместо этого. + +```go +// Аналогично, эта ошибка экспортируется, чтобы пользователи этого пакета могли сопоставлять её с errors.As. + +type NotFoundError struct { + File string +} +func (e *NotFoundError) Error() string { + return fmt.Sprintf("file %q not found", e.File) +} + +// А эта ошибка не экспортируется, потому что мы не хотим делать её частью публичного API. Мы всё ещё можем использовать её внутри пакета с errors.As. + +type resolveError struct { + Path string +} + +func (e *resolveError) Error() string { + return fmt.Sprintf("resolve %q", e.Path) +} +``` + +#### Обрабатывайте ошибки один раз + +Когда вызывающая сторона получает ошибку от вызываемой функции, она может +обработать её различными способами в зависимости от того, что она знает об +ошибке. + +К ним относятся, но не ограничиваются: + +- если контракт вызываемой функции определяет конкретные ошибки, сопоставить + ошибку с `errors.Is` или `errors.As` и обработать ветви по-разному +- если ошибка является восстанавливаемой, залогировать ошибку и выполнить + graceful degradation +- если ошибка представляет собой отказ в предметной области, вернуть чётко + определённую ошибку +- вернуть ошибку, либо [обёрнутую](#обёртывание-ошибок), либо как есть + +Независимо от того, как вызывающая сторона обрабатывает ошибку, обычно она +должна обрабатывать каждую ошибку только один раз. Вызывающая сторона не должна, +например, логировать ошибку, а затем возвращать её, потому что _её_ вызывающие +стороны тоже могут обработать ошибку. + +Например, рассмотрим следующие случаи: + +<table> +<thead><tr><th>Описание</th><th>Код</th></tr></thead> +<tbody> +<tr><td> + +**Плохо**: Логировать ошибку и возвращать её + +Вызывающие стороны выше по стеку, вероятно, предпримут аналогичные действия с +ошибкой. Это приведёт к большому количеству шума в логах приложения при малой +пользе. + +</td><td> + +```go +u, err := getUser(id) +if err != nil { + // ПЛОХО: см. описание + log.Printf("Could not get user %q: %v", id, err) + return err +} +``` + +</td></tr> +<tr><td> + +**Хорошо**: Обернуть ошибку и вернуть её + +Вызывающие стороны выше по стеку обработают ошибку. Использование `%w` +гарантирует, что они смогут сопоставить ошибку с `errors.Is` или `errors.As`, +если это уместно. + +</td><td> + +```go +u, err := getUser(id) +if err != nil { + return fmt.Errorf("get user %q: %w", id, err) +} +``` + +</td></tr> +<tr><td> + +**Хорошо**: Логировать ошибку и выполнить graceful degradation + +Если операция не является строго необходимой, мы можем обеспечить +деградировавший, но работающий опыт, восстановившись после ошибки. + +</td><td> + +```go +if err := emitMetrics(); err != nil { + // Неудача при записи метрик не должна ломать приложение. + log.Printf("Could not emit metrics: %v", err) +} + +``` + +</td></tr> +<tr><td> + +**Хорошо**: Сопоставить ошибку и выполнить graceful degradation + +Если вызываемая функция определяет конкретную ошибку в своём контракте и сбой +является восстанавливаемым, сопоставьте этот случай ошибки и выполните graceful +degradation. Для всех остальных случаев оберните ошибку и верните её. + +Вызывающие стороны выше по стеку обработают другие ошибки. + +</td><td> + +```go +tz, err := getUserTimeZone(id) +if err != nil { + if errors.Is(err, ErrUserNotFound) { + // Пользователь не существует. Используем UTC. + tz = time.UTC + } else { + return fmt.Errorf("get user %q: %w", id, err) + } +} +``` + +</td></tr> +</tbody></table> + +### Обрабатывайте сбои утверждения типа + +Форма утверждения типа с одним возвращаемым значением вызовет панику при +неверном типе. Поэтому всегда используйте идиому "comma ok". + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +t := i.(string) +``` + +</td><td> + +```go +t, ok := i.(string) +if !ok { + // обработать ошибку корректно +} +``` + +</td></tr> +</tbody></table> + +<!-- TODO: Есть несколько ситуаций, где форма с одним присваиванием допустима. --> + +### Не паникуйте + +Код, работающий в production, должен избегать паник. Паники являются основной +причиной [каскадных сбоев](https://en.wikipedia.org/wiki/Cascading_failure). +Если возникает ошибка, функция должна вернуть ошибку и позволить вызывающей +стороне решить, как её обработать. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +func run(args []string) { + if len(args) == 0 { + panic("an argument is required") + } + // ... +} + +func main() { + run(os.Args[1:]) +} +``` + +</td><td> + +```go +func run(args []string) error { + if len(args) == 0 { + return errors.New("an argument is required") + } + // ... + return nil +} + +func main() { + if err := run(os.Args[1:]); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} +``` + +</td></tr> +</tbody></table> + +Panic/recover — это не стратегия обработки ошибок. Программа должна паниковать +только при возникновении чего-то невосстановимого, например, разыменовании nil. +Исключением является инициализация программы: проблемы при запуске программы, +которые должны её завершить, могут вызывать панику. + +```go +var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML")) +``` + +Даже в тестах предпочитайте `t.Fatal` или `t.FailNow` вместо паник, чтобы +гарантировать, что тест будет помечен как неудачный. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +// func TestFoo(t *testing.T) + +f, err := os.CreateTemp("", "test") +if err != nil { + panic("failed to set up test") +} +``` + +</td><td> + +```go +// func TestFoo(t *testing.T) + +f, err := os.CreateTemp("", "test") +if err != nil { + t.Fatal("failed to set up test") +} +``` + +</td></tr> +</tbody></table> + +### Используйте go.uber.org/atomic + +Атомарные операции с пакетом [sync/atomic](https://pkg.go.dev/sync/atomic) +работают с базовыми типами (`int32`, `int64` и т.д.), поэтому легко забыть +использовать атомарную операцию для чтения или изменения переменных. + +[go.uber.org/atomic](https://pkg.go.dev/go.uber.org/atomic) добавляет +безопасность типов к этим операциям, скрывая базовый тип. Кроме того, он +включает удобный тип `atomic.Bool`. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +type foo struct { + running int32 // atomic +} + +func (f* foo) start() { + if atomic.SwapInt32(&f.running, 1) == 1 { + // уже работает… + return + } + // запустить Foo +} + +func (f *foo) isRunning() bool { + return f.running == 1 // состояние гонки! +} +``` + +</td><td> + +```go +type foo struct { + running atomic.Bool +} + +func (f *foo) start() { + if f.running.Swap(true) { + // уже работает… + return + } + // запустить Foo +} + +func (f *foo) isRunning() bool { + return f.running.Load() +} +``` + +</td></tr> +</tbody></table> + +### Избегайте изменяемых глобальных переменных + +Избегайте изменения глобальных переменных, отдавая предпочтение внедрению +зависимостей. Это относится как к указателям на функции, так и к другим видам +значений. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +// sign.go + +var _timeNow = time.Now + +func sign(msg string) string { + now := _timeNow() + return signWithTime(msg, now) +} +``` + +</td><td> + +```go +// sign.go + +type signer struct { + now func() time.Time +} + +func newSigner() *signer { + return &signer{ + now: time.Now, + } +} + +func (s *signer) Sign(msg string) string { + now := s.now() + return signWithTime(msg, now) +} +``` + +</td></tr> +<tr><td> + +```go +// sign_test.go + +func TestSign(t *testing.T) { + oldTimeNow := _timeNow + _timeNow = func() time.Time { + return someFixedTime + } + defer func() { _timeNow = oldTimeNow }() + + assert.Equal(t, want, sign(give)) +} +``` + +</td><td> + +```go +// sign_test.go + +func TestSigner(t *testing.T) { + s := newSigner() + s.now = func() time.Time { + return someFixedTime + } + + assert.Equal(t, want, s.Sign(give)) +} +``` + +</td></tr> +</tbody></table> + +### Избегайте встраивания типов в публичные структуры + +Такие встроенные типы раскрывают детали реализации, препятствуют эволюции типов +и затрудняют чтение документации. + +Предположим, вы реализовали различные типы списков, используя общий +`AbstractList`. Избегайте встраивания `AbstractList` в ваши конкретные +реализации списков. Вместо этого напишите вручную только методы вашего +конкретного списка, которые будут делегировать вызовы абстрактному списку. + +```go +type AbstractList struct {} + +// Add добавляет сущность в список. +func (l *AbstractList) Add(e Entity) { + // ... +} + +// Remove удаляет сущность из списка. +func (l *AbstractList) Remove(e Entity) { + // ... +} +``` + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +// ConcreteList — это список сущностей. +type ConcreteList struct { + *AbstractList +} +``` + +</td><td> + +```go +// ConcreteList — это список сущностей. +type ConcreteList struct { + list *AbstractList +} + +// Add добавляет сущность в список. +func (l *ConcreteList) Add(e Entity) { + l.list.Add(e) +} + +// Remove удаляет сущность из списка. +func (l *ConcreteList) Remove(e Entity) { + l.list.Remove(e) +} +``` + +</td></tr> +</tbody></table> + +Go позволяет [встраивание типов](https://go.dev/doc/effective_go#embedding) как +компромисс между наследованием и композицией. Внешний тип получает неявные копии +методов встроенного типа. Эти методы по умолчанию делегируют вызовы тому же +методу встроенного экземпляра. + +Структура также получает поле с тем же именем, что и тип. Таким образом, если +встроенный тип является публичным, поле также является публичным. Для сохранения +обратной совместимости каждая будущая версия внешнего типа должна сохранять +встроенный тип. + +Встраивание типа редко необходимо. Это удобство, которое помогает избежать +написания утомительных делегирующих методов. + +Даже встраивание совместимого интерфейса `AbstractList` вместо структуры дало бы +разработчику больше гибкости для изменений в будущем, но всё равно раскрыло бы +деталь, что конкретные списки используют абстрактную реализацию. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +// AbstractList — это обобщённая реализация для различных видов списков сущностей. +type AbstractList interface { + Add(Entity) + Remove(Entity) +} + +// ConcreteList — это список сущностей. +type ConcreteList struct { + AbstractList +} +``` + +</td><td> + +```go +// ConcreteList — это список сущностей. +type ConcreteList struct { + list AbstractList +} + +// Add добавляет сущность в список. +func (l *ConcreteList) Add(e Entity) { + l.list.Add(e) +} + +// Remove удаляет сущность из списка. +func (l *ConcreteList) Remove(e Entity) { + l.list.Remove(e) +} +``` + +</td></tr> +</tbody></table> + +И в случае со встроенной структурой, и со встроенным интерфейсом встроенный тип +накладывает ограничения на эволюцию типа. + +- Добавление методов во встроенный интерфейс — это breaking change. +- Удаление методов из встроенной структуры — это breaking change. +- Удаление встроенного типа — это breaking change. +- Замена встроенного типа, даже на альтернативу, удовлетворяющую тому же + интерфейсу, — это breaking change. + +Хотя написание этих делегирующих методов утомительно, дополнительные усилия +скрывают деталь реализации, оставляют больше возможностей для изменений, а также +устраняют косвенность при обнаружении полного интерфейса `List` в документации. + +### Избегайте использования встроенных имён + +[Спецификация языка Go](https://go.dev/ref/spec) описывает несколько встроенных, +[предобъявленных +идентификаторов](https://go.dev/ref/spec#Predeclared_identifiers), которые не +должны использоваться как имена в программах на Go. + +В зависимости от контекста повторное использование этих идентификаторов в +качестве имён либо затеняет оригинал в текущей лексической области видимости (и +любых вложенных областях), либо делает затронутый код запутанным. В лучшем +случае компилятор пожалуется; в худшем — такой код может привести к скрытым, +трудноуловимым ошибкам. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +var error string +// `error` затеняет встроенный идентификатор + +// или + +func handleErrorMessage(error string) { + // `error` затеняет встроенный идентификатор +} +``` + +</td><td> + +```go +var errorMessage string +// `error` ссылается на встроенный идентификатор + +// или + +func handleErrorMessage(msg string) { + // `error` ссылается на встроенный идентификатор +} +``` + +</td></tr> +<tr><td> + +```go +type Foo struct { + // Хотя технически эти поля не создают затенение, поиск по строкам `error` или `string` теперь неоднозначен. + error error + string string +} + +func (f Foo) Error() error { + // `error` и `f.error` визуально похожи + return f.error +} + +func (f Foo) String() string { + // `string` и `f.string` визуально похожи + return f.string +} +``` + +</td><td> + +```go +type Foo struct { + // `error` и `string` теперь однозначны. + err error + str string +} + +func (f Foo) Error() error { + return f.err +} + +func (f Foo) String() string { + return f.str +} +``` + +</td></tr> +</tbody></table> + +Обратите внимание, что компилятор не будет генерировать ошибки при использовании +предобъявленных идентификаторов, но такие инструменты, как `go vet`, должны +корректно указывать на эти и другие случаи затенения. + +### Избегайте `init()` + +Избегайте `init()` там, где это возможно. Когда `init()` неизбежен или +желателен, код должен пытаться: + +1. Быть полностью детерминированным, независимо от среды программы или вызова. +2. Избегать зависимости от порядка или побочных эффектов других функций + `init()`. Хотя порядок `init()` хорошо известен, код может меняться, и + поэтому зависимости между функциями `init()` могут сделать код хрупким и + подверженным ошибкам. +3. Избегать доступа или манипуляции глобальным состоянием или состоянием + окружения, таким как информация о машине, переменные окружения, рабочая + директория, аргументы/вводы программы и т.д. +4. Избегать ввода-вывода, включая файловую систему, сеть и системные вызовы. + +Код, который не может удовлетворить этим требованиям, вероятно, должен быть +вспомогательной функцией, вызываемой как часть `main()` (или в другом месте +жизненного цикла программы), или быть написан как часть самого `main()`. В +частности, библиотеки, предназначенные для использования другими программами, +должны особенно тщательно следить за полной детерминированностью и не выполнять +"init magic". + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +type Foo struct { + // ... +} + +var _defaultFoo Foo + +func init() { + _defaultFoo = Foo{ + // ... + } +} +``` + +</td><td> + +```go +var _defaultFoo = Foo{ + // ... +} + +// или, лучше, для тестируемости: + +var _defaultFoo = defaultFoo() + +func defaultFoo() Foo { + return Foo{ + // ... + } +} +``` + +</td></tr> +<tr><td> + +```go +type Config struct { + // ... +} + +var _config Config + +func init() { + // Плохо: зависит от текущей директории + cwd, _ := os.Getwd() + + // Плохо: ввод-вывод + raw, _ := os.ReadFile( + path.Join(cwd, "config", "config.yaml"), + ) + + yaml.Unmarshal(raw, &_config) +} +``` + +</td><td> + +```go +type Config struct { + // ... +} + +func loadConfig() Config { + cwd, err := os.Getwd() + // обработать err + + raw, err := os.ReadFile( + path.Join(cwd, "config", "config.yaml"), + ) + // обработать err + + var config Config + yaml.Unmarshal(raw, &config) + + return config +} +``` + +</td></tr> +</tbody></table> + +Учитывая вышесказанное, некоторые ситуации, в которых `init()` может быть +предпочтительнее или необходим, включают: + +- Сложные выражения, которые нельзя представить в виде одиночных присваиваний. +- Подключаемые хуки, такие как диалекты `database/sql`, регистры типов + кодирования и т.д. +- Оптимизации для [Google Cloud + Functions](https://cloud.google.com/functions/docs/bestpractices/tips#use_global_variables_to_reuse_objects_in_future_invocations) + и другие формы детерминированного предварительного вычисления. + +### Завершение программы в main + +Программы на Go используют [`os.Exit`](https://pkg.go.dev/os#Exit) или +[`log.Fatal*`](https://pkg.go.dev/log#Fatal) для немедленного завершения. +(Паника — нехороший способ завершения программ, пожалуйста, [не +паникуйте](#не-паникуйте).) + +Вызывайте `os.Exit` или `log.Fatal*` **только в `main()`**. Все остальные +функции должны возвращать ошибки для сигнализации о сбое. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +func main() { + body := readFile(path) + fmt.Println(body) +} + +func readFile(path string) string { + f, err := os.Open(path) + if err != nil { + log.Fatal(err) + } + + b, err := io.ReadAll(f) + if err != nil { + log.Fatal(err) + } + + return string(b) +} +``` + +</td><td> + +```go +func main() { + body, err := readFile(path) + if err != nil { + log.Fatal(err) + } + fmt.Println(body) +} + +func readFile(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + + b, err := io.ReadAll(f) + if err != nil { + return "", err + } + + return string(b), nil +} +``` + +</td></tr> +</tbody></table> + +Обоснование: Программы с несколькими функциями, которые завершают выполнение, +создают несколько проблем: + +- Неочевидный поток управления: любая функция может завершить программу, поэтому + становится трудно рассуждать о потоке управления. +- Сложность тестирования: функция, завершающая программу, также завершит тест, + который её вызывает. Это делает функцию трудной для тестирования и создаёт + риск пропуска других тестов, которые ещё не были запущены `go test`. +- Пропущенная очистка: когда функция завершает программу, она пропускает вызовы + функций, поставленные в очередь с операторами `defer`. Это добавляет риск + пропуска важных задач очистки. + +#### Завершайте программу один раз + +По возможности старайтесь вызывать `os.Exit` или `log.Fatal` **не более одного +раза** в вашем `main()`. Если есть несколько сценариев ошибок, которые +останавливают выполнение программы, поместите эту логику в отдельную функцию и +возвращайте из неё ошибки. + +Это приводит к сокращению функции `main()` и помещению всей ключевой +бизнес-логики в отдельную, тестируемую функцию. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +package main + +func main() { + args := os.Args[1:] + if len(args) != 1 { + log.Fatal("missing file") + } + name := args[0] + + f, err := os.Open(name) + if err != nil { + log.Fatal(err) + } + defer f.Close() + + // Если мы вызовем log.Fatal после этой строки, f.Close не будет вызван. + + b, err := io.ReadAll(f) + if err != nil { + log.Fatal(err) + } + + // ... +} +``` + +</td><td> + +```go +package main + +func main() { + if err := run(); err != nil { + log.Fatal(err) + } +} + +func run() error { + args := os.Args[1:] + if len(args) != 1 { + return errors.New("missing file") + } + name := args[0] + + f, err := os.Open(name) + if err != nil { + return err + } + defer f.Close() + + b, err := io.ReadAll(f) + if err != nil { + return err + } + + // ... +} +``` + +</td></tr> +</tbody></table> + +В примере выше используется `log.Fatal`, но рекомендация также применима к +`os.Exit` или любому библиотечному коду, который вызывает `os.Exit`. + +```go +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} +``` + +Вы можете изменить сигнатуру `run()`, чтобы она соответствовала вашим +потребностям. Например, если ваша программа должна завершаться с определёнными +кодами выхода при сбоях, `run()` может возвращать код выхода вместо ошибки. Это +позволяет модульным тестам также напрямую проверять это поведение. + +```go +func main() { + os.Exit(run(args)) +} + +func run() (exitCode int) { + // ... +} +``` + +Более общо, обратите внимание, что функция `run()`, используемая в этих +примерах, не является предписывающей. Существует гибкость в имени, сигнатуре и +настройке функции `run()`. Среди прочего, вы можете: + +- принимать неразобранные аргументы командной строки (например, + `run(os.Args[1:])`) +- разбирать аргументы командной строки в `main()` и передавать их в `run` +- использовать пользовательский тип ошибки для передачи кода выхода обратно в + `main()` +- помещать бизнес-логику на другой уровень абстракции, отличный от `package +main` + +Эта рекомендация требует только, чтобы в вашем `main()` было единственное место, +ответственное за фактическое завершение процесса. + +### Используйте теги полей в структурах для сериализации + +Любое поле структуры, которое сериализуется в JSON, YAML или другие форматы, +поддерживающие именование полей на основе тегов, должно быть аннотировано +соответствующим тегом. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +type Stock struct { + Price int + Name string +} + +bytes, err := json.Marshal(Stock{ + Price: 137, + Name: "UBER", +}) +``` + +</td><td> + +```go +type Stock struct { + Price int `json:"price"` + Name string `json:"name"` + // Безопасно переименовать Name в Symbol. +} + +bytes, err := json.Marshal(Stock{ + Price: 137, + Name: "UBER", +}) +``` + +</td></tr> +</tbody></table> + +Обоснование: Сериализованная форма структуры — это контракт между различными +системами. Изменения в структуре сериализованной формы — включая имена полей — +нарушают этот контракт. Указание имён полей внутри тегов делает контракт явным и +защищает от случайного его нарушения при рефакторинге или переименовании полей. + +### Не запускайте горутины по принципу «запустил и забыл» + +Горутины легковесны, но не бесплатны: как минимум, они требуют памяти для своего +стека и процессорного времени для планирования. Хотя эти затраты малы для +типичного использования горутин, они могут вызвать значительные проблемы с +производительностью, если горутины создаются в больших количествах без +контролируемого времени жизни. Горутины с неуправляемым временем жизни также +могут вызывать другие проблемы, например, мешать сборке мусора для +неиспользуемых объектов и удерживать ресурсы, которые в противном случае больше +не используются. + +Поэтому не допускайте утечек горутин в production коде. Используйте +[go.uber.org/goleak](https://pkg.go.dev/go.uber.org/goleak) для тестирования +утечек горутин внутри пакетов, которые могут их создавать. + +В общем случае каждая горутина: + +- должна иметь предсказуемый момент, когда она перестанет выполняться; или +- должен быть способ сигнализировать горутине, что ей следует остановиться + +В обоих случаях должен быть способ заблокировать выполнение и дождаться +завершения горутины. + +Например: + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +go func() { + for { + flush() + time.Sleep(delay) + } +}() +``` + +</td><td> + +```go +var ( + stop = make(chan struct{}) // сигнализирует горутине остановиться + done = make(chan struct{}) // сигнализирует нам, что горутина завершилась +) +go func() { + defer close(done) + + ticker := time.NewTicker(delay) + defer ticker.Stop() + for { + select { + case <-ticker.C: + flush() + case <-stop: + return + } + } +}() + +// В другом месте... +close(stop) // сигнализировать горутине остановиться +<-done // и ждать её завершения +``` + +</td></tr> +<tr><td> + +Нет способа остановить эту горутину. Она будет работать, пока приложение не +завершится. + +</td><td> + +Эту горутину можно остановить с помощью `close(stop)`, и мы можем дождаться её +завершения с помощью `<-done`. + +</td></tr> +</tbody></table> + +#### Дожидайтесь завершения горутин + +Для горутины, созданной системой, должен быть способ дождаться её завершения. +Есть два популярных способа сделать это: + +- Используйте `sync.WaitGroup`, чтобы дождаться завершения нескольких горутин. + Делайте так, если нужно ждать несколько горутин. + + ```go + var wg sync.WaitGroup + for i := 0; i < N; i++ { + wg.Go(...) + } + + // Чтобы дождаться завершения всех: + wg.Wait() + ``` + +- Добавьте ещё один `chan struct{}`, который горутина закроет по завершении. + Делайте так, если есть только одна горутина. + + ```go + done := make(chan struct{}) + go func() { + defer close(done) + // ... + }() + + // Чтобы дождаться завершения горутины: + <-done + ``` + +#### Не используйте горутины в `init()` + +Функции `init()` не должны запускать горутины. См. также [Избегайте +init()](#избегайте-init). + +Если пакету нужна фоновая горутина, он должен предоставлять объект, +ответственный за управление временем жизни горутины. Этот объект должен +предоставлять метод (`Close`, `Stop`, `Shutdown` и т.д.), который сигнализирует +фоновой горутине об остановке и ждёт её завершения. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +func init() { + go doWork() +} + +func doWork() { + for { + // ... + } +} +``` + +</td><td> + +```go +type Worker struct{ /* ... */ } + +func NewWorker(...) *Worker { + w := &Worker{ + stop: make(chan struct{}), + done: make(chan struct{}), + // ... + } + go w.doWork() +} + +func (w *Worker) doWork() { + defer close(w.done) + for { + // ... + case <-w.stop: + return + } +} + +// Shutdown говорит воркеру остановиться и ждёт, пока он завершится. +func (w *Worker) Shutdown() { + close(w.stop) + <-w.done +} +``` + +</td></tr> +<tr><td> + +Создаёт фоновую горутину безусловно при экспорте этого пакета пользователем. +Пользователь не имеет контроля над горутиной или способа её остановки. + +</td><td> + +Создаёт воркера только если пользователь его запрашивает. Предоставляет способ +остановки воркера, чтобы пользователь мог освободить используемые им ресурсы. + +Обратите внимание, что следует использовать `WaitGroup`, если воркер управляет +несколькими горутинами. См. [Дожидайтесь завершения +горутин](#дожидайтесь-завершения-горутин). + +</td></tr> +</tbody></table> + +## Производительность + +Рекомендации, специфичные для производительности, применяются только к «горячему +пути» (hot path). + +### Предпочитайте strconv вместо fmt + +При преобразовании примитивов в строки и обратно `strconv` быстрее, чем `fmt`. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +for i := 0; i < b.N; i++ { + s := fmt.Sprint(rand.Int()) +} +``` + +</td><td> + +```go +for i := 0; i < b.N; i++ { + s := strconv.Itoa(rand.Int()) +} +``` + +</td></tr> +<tr><td> + +```plain +BenchmarkFmtSprint-4 143 ns/op 2 allocs/op +``` + +</td><td> + +```plain +BenchmarkStrconv-4 64.2 ns/op 1 allocs/op +``` + +</td></tr> +</tbody></table> + +### Избегайте повторного преобразования строк в байты + +Не создавайте срезы байт из фиксированной строки повторно. Вместо этого +выполните преобразование один раз и сохраните результат. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +for i := 0; i < b.N; i++ { + w.Write([]byte("Hello world")) +} +``` + +</td><td> + +```go +data := []byte("Hello world") +for i := 0; i < b.N; i++ { + w.Write(data) +} +``` + +</td></tr> +<tr><td> + +```plain +BenchmarkBad-4 50000000 22.2 ns/op +``` + +</td><td> + +```plain +BenchmarkGood-4 500000000 3.25 ns/op +``` + +</td></tr> +</tbody></table> + +### Предпочитайте указание ёмкости контейнеров + +По возможности указывайте ёмкость контейнеров, чтобы выделить память для +контейнера заранее. Это минимизирует последующие выделения памяти (из-за +копирования и изменения размера контейнера) при добавлении элементов. + +#### Указание подсказки ёмкости для карт + +По возможности предоставляйте подсказку ёмкости при инициализации карт с помощью +`make()`. + +```go +make(map[T1]T2, hint) +``` + +Предоставление подсказки ёмкости для `make()` пытается правильно определить +размер карты при инициализации, что уменьшает необходимость её роста и выделений +памяти при добавлении элементов. + +Обратите внимание, что в отличие от срезов, подсказки ёмкости для карт не +гарантируют полного, упреждающего выделения, а используются для приблизительного +определения количества необходимых корзин хэш-карты. Следовательно, выделения +памяти всё ещё могут происходить при добавлении элементов в карту, даже до +указанной ёмкости. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +m := make(map[string]os.FileInfo) + +files, _ := os.ReadDir("./files") +for _, f := range files { + m[f.Name()] = f +} +``` + +</td><td> + +```go + +files, _ := os.ReadDir("./files") + +m := make(map[string]os.DirEntry, len(files)) +for _, f := range files { + m[f.Name()] = f +} +``` + +</td></tr> +<tr><td> + +`m` создаётся без подсказки размера; при присваивании может быть больше +выделений памяти. + +</td><td> + +`m` создаётся с подсказкой размера; при присваивании может быть меньше выделений +памяти. + +</td></tr> +</tbody></table> + +#### Указание ёмкости срезов + +По возможности предоставляйте подсказку ёмкости при инициализации срезов с +помощью `make()`, особенно при использовании `append`. + +```go +make([]T, length, capacity) +``` + +В отличие от карт, ёмкость среза — это не подсказка: компилятор выделит +достаточно памяти для ёмкости среза, предоставленной в `make()`, что означает, +что последующие операции `append()` не будут приводить к выделениям памяти (пока +длина среза не совпадёт с ёмкостью, после чего любые добавления потребуют +изменения размера для хранения дополнительных элементов). + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +for n := 0; n < b.N; n++ { + data := make([]int, 0) + for k := 0; k < size; k++{ + data = append(data, k) + } +} +``` + +</td><td> + +```go +for n := 0; n < b.N; n++ { + data := make([]int, 0, size) + for k := 0; k < size; k++{ + data = append(data, k) + } +} +``` + +</td></tr> +<tr><td> + +```plain +BenchmarkBad-4 100000000 2.48s +``` + +</td><td> + +```plain +BenchmarkGood-4 100000000 0.21s +``` + +</td></tr> +</tbody></table> + +## Стиль + +### Избегайте слишком длинных строк + +Избегайте строк кода, которые заставляют читателей прокручивать по горизонтали +или слишком сильно поворачивать голову. + +Мы рекомендуем мягкое ограничение длины строки в **99 символов**. Авторы должны +стараться переносить строки до достижения этого предела, но это не строгое +ограничение. Коду разрешено превышать этот лимит. + +### Будьте последовательны + +Некоторые рекомендации, изложенные в этом документе, можно оценить объективно; +другие ситуативны, контекстны или субъективны. + +Прежде всего, **будьте последовательны**. + +Последовательный код легче поддерживать, легче осмыслить, требует меньше +когнитивных усилий и легче переносить или обновлять по мере появления новых +соглашений или исправления классов ошибок. + +И наоборот, наличие множества различных или конфликтующих стилей в одной кодовой +базе создаёт накладные расходы на поддержку, неопределённость и когнитивный +диссонанс, что напрямую может способствовать снижению скорости разработки, +болезненным код-ревью и ошибкам. + +При применении этих рекомендаций к кодовой базе рекомендуется вносить изменения +на уровне пакета (или выше): применение на уровне подпакета нарушает указанную +выше проблему, внося несколько стилей в один код. + +### Группируйте схожие объявления + +Go поддерживает группировку схожих объявлений. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +import "a" +import "b" +``` + +</td><td> + +```go +import ( + "a" + "b" +) +``` + +</td></tr> +</tbody></table> + +Это также относится к константам, переменным и объявлениям типов. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go + +const a = 1 +const b = 2 + + + +var a = 1 +var b = 2 + + + +type Area float64 +type Volume float64 +``` + +</td><td> + +```go +const ( + a = 1 + b = 2 +) + +var ( + a = 1 + b = 2 +) + +type ( + Area float64 + Volume float64 +) +``` + +</td></tr> +</tbody></table> + +Группируйте только связанные объявления. Не группируйте несвязанные объявления. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +type Operation int + +const ( + Add Operation = iota + 1 + Subtract + Multiply + EnvVar = "MY_ENV" +) +``` + +</td><td> + +```go +type Operation int + +const ( + Add Operation = iota + 1 + Subtract + Multiply +) + +const EnvVar = "MY_ENV" +``` + +</td></tr> +</tbody></table> + +Группы не ограничены в том, где могут использоваться. Например, их можно +использовать внутри функций. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +func f() string { + red := color.New(0xff0000) + green := color.New(0x00ff00) + blue := color.New(0x0000ff) + + // ... +} +``` + +</td><td> + +```go +func f() string { + var ( + red = color.New(0xff0000) + green = color.New(0x00ff00) + blue = color.New(0x0000ff) + ) + + // ... +} +``` + +</td></tr> +</tbody></table> + +Исключение: Объявления переменных, особенно внутри функций, должны +группироваться вместе, если они объявлены рядом с другими переменными. Делайте +так для переменных, объявленных вместе, даже если они не связаны. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +func (c *client) request() { + caller := c.name + format := "json" + timeout := 5*time.Second + var err error + + // ... +} +``` + +</td><td> + +```go +func (c *client) request() { + var ( + caller = c.name + format = "json" + timeout = 5*time.Second + err error + ) + + // ... +} +``` + +</td></tr> +</tbody></table> + +### Порядок групп импорта + +Должно быть две группы импорта: + +- Стандартная библиотека +- Все остальные + +Это группировка, применяемая по умолчанию в `goimports`. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +import ( + "fmt" + "os" + "go.uber.org/atomic" + "golang.org/x/sync/errgroup" +) +``` + +</td><td> + +```go +import ( + "fmt" + "os" + + "go.uber.org/atomic" + "golang.org/x/sync/errgroup" +) +``` + +</td></tr> +</tbody></table> + +### Имена пакетов + +При именовании пакетов выбирайте имя, которое: + +- Состоит только из строчных букв. Без заглавных букв и подчёркиваний. +- Не требует переименования с использованием именованных импортов в большинстве + мест вызова. +- Короткое и ёмкое. Помните, что имя полностью указывается в каждом месте + вызова. +- Не во множественном числе. Например, `net/url`, а не `net/urls`. +- Не "common", "util", "shared" или "lib". Это плохие, неинформативные имена. + +См. также [Package Names](https://go.dev/blog/package-names) и [Style guideline +for Go packages](https://rakyll.org/style-packages/). + +### Имена функций + +Мы следуем соглашению сообщества Go об использовании [MixedCaps для имён +функций](https://go.dev/doc/effective_go#mixed-caps). Исключение делается для +тестовых функций, которые могут содержать подчёркивания для группировки +связанных тестовых случаев, например, `TestMyFunction_WhatIsBeingTested`. + +### Псевдонимы импорта + +Псевдонимы импорта должны использоваться, если имя пакета не совпадает с +последним элементом пути импорта. + +```go +import ( + "net/http" + + client "example.com/client-go" + trace "example.com/trace/v2" +) +``` + +Во всех остальных сценариях псевдонимы импорта следует избегать, если нет +прямого конфликта между импортами. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +import ( + "fmt" + "os" + runtimetrace "runtime/trace" + + nettrace "golang.net/x/trace" +) +``` + +</td><td> + +```go +import ( + "fmt" + "os" + "runtime/trace" + + nettrace "golang.net/x/trace" +) +``` + +</td></tr> +</tbody></table> + +### Группировка и порядок функций + +- Функции должны быть отсортированы в приблизительном порядке вызовов. +- Функции в файле должны быть сгруппированы по получателю. + +Следовательно, экспортируемые функции должны появляться первыми в файле после +определений `struct`, `const`, `var`. + +`newXYZ()`/`NewXYZ()` может появиться после определения типа, но до остальных +методов получателя. + +Поскольку функции группируются по получателю, простые вспомогательные функции +должны появляться ближе к концу файла. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +func (s *something) Cost() { + return calcCost(s.weights) +} + +type something struct{ ... } + +func calcCost(n []int) int {...} + +func (s *something) Stop() {...} + +func newSomething() *something { + return &something{} +} +``` + +</td><td> + +```go +type something struct{ ... } + +func newSomething() *something { + return &something{} +} + +func (s *something) Cost() { + return calcCost(s.weights) +} + +func (s *something) Stop() {...} + +func calcCost(n []int) int {...} +``` + +</td></tr> +</tbody></table> + +### Уменьшайте вложенность + +Код должен уменьшать вложенность, где это возможно, обрабатывая случаи +ошибок/особые условия первыми и возвращаясь рано или продолжая цикл. Уменьшайте +количество кода, вложенного на несколько уровней. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +for _, v := range data { + if v.F1 == 1 { + v = process(v) + if err := v.Call(); err == nil { + v.Send() + } else { + return err + } + } else { + log.Printf("Invalid v: %v", v) + } +} +``` + +</td><td> + +```go +for _, v := range data { + if v.F1 != 1 { + log.Printf("Invalid v: %v", v) + continue + } + + v = process(v) + if err := v.Call(); err != nil { + return err + } + v.Send() +} +``` + +</td></tr> +</tbody></table> + +### Избыточный Else + +Если переменная устанавливается в обеих ветках if, это можно заменить одним if. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +var a int +if b { + a = 100 +} else { + a = 10 +} +``` + +</td><td> + +```go +a := 10 +if b { + a = 100 +} +``` + +</td></tr> +</tbody></table> + +### Объявления переменных верхнего уровня + +На верхнем уровне используйте стандартное ключевое слово `var`. Не указывайте +тип, если он не отличается от типа выражения. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +var _s string = F() + +func F() string { return "A" } +``` + +</td><td> + +```go +var _s = F() +// Поскольку F уже указывает, что возвращает строку, нам не нужно снова указывать тип. + +func F() string { return "A" } +``` + +</td></tr> +</tbody></table> + +Указывайте тип, если тип выражения не совпадает точно с желаемым типом. + +```go +type myError struct{} + +func (myError) Error() string { return "error" } + +func F() myError { return myError{} } + +var _e error = F() +// F возвращает объект типа myError, но мы хотим error. +``` + +### Префикс \_ для неэкспортируемых глобальных переменных + +Добавляйте префикс `_` к неэкспортируемым переменным и константам верхнего +уровня, чтобы было ясно, что они являются глобальными символами, когда они +используются. + +Обоснование: Переменные и константы верхнего уровня имеют область видимости +пакета. Использование общего имени делает лёгким случайное использование +неправильного значения в другом файле. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +// foo.go + +const ( + defaultPort = 8080 + defaultUser = "user" +) + +// bar.go + +func Bar() { + defaultPort := 9090 + ... + fmt.Println("Default port", defaultPort) + + // Мы не увидим ошибку компиляции, если первая строка Bar() будет удалена. +} +``` + +</td><td> + +```go +// foo.go + +const ( + _defaultPort = 8080 + _defaultUser = "user" +) +``` + +</td></tr> +</tbody></table> + +**Исключение**: Неэкспортируемые значения ошибок могут использовать префикс +`err` без подчёркивания. См. [Именование ошибок](#именование-ошибок). + +### Встраивание в структурах + +Встроенные типы должны находиться в начале списка полей структуры, и должна быть +пустая строка, отделяющая встроенные поля от обычных полей. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +type Client struct { + version int + http.Client +} +``` + +</td><td> + +```go +type Client struct { + http.Client + + version int +} +``` + +</td></tr> +</tbody></table> + +Встраивание должно обеспечивать ощутимую пользу, например, добавлять или +расширять функциональность семантически-уместным способом. Оно должно делать это +без каких-либо негативных последствий для пользователя (см. также: [Избегайте +встраивания типов в публичные +структуры](#избегайте-встраивания-типов-в-публичные-структуры)). + +Исключение: Мьютексы не должны встраиваться, даже в неэкспортируемые типы. См. +также: [Нулевые значения мьютексов +допустимы](#нулевые-значения-мьютексов-допустимы). + +Встраивание **НЕ должно**: + +- Быть чисто косметическим или ориентированным на удобство. +- Усложнять создание или использование внешних типов. +- Влиять на нулевые значения внешних типов. Если внешний тип имеет полезное + нулевое значение, он должен сохранять его после встраивания внутреннего типа. +- Раскрывать несвязанные функции или поля внешнего типа как побочный эффект + встраивания внутреннего типа. +- Раскрывать неэкспортируемые типы. +- Влиять на семантику копирования внешних типов. +- Менять API или семантику типов внешних типов. +- Встраивать неканоническую форму внутреннего типа. +- Раскрывать детали реализации внешнего типа. +- Позволять пользователям наблюдать или контролировать внутренности типа. +- Менять общее поведение внутренних функций через обёртывание таким образом, + который может удивить пользователей. + +Проще говоря, встраивайте осознанно и преднамеренно. Хороший тест: "все ли эти +экспортируемые внутренние методы/поля были бы добавлены напрямую к внешнему +типу"; если ответ "некоторые" или "нет", не встраивайте внутренний тип — +используйте поле. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +type A struct { + // Плохо: A.Lock() и A.Unlock() теперь доступны, не предоставляют функциональной пользы и позволяют пользователям контролировать детали внутренностей A. + sync.Mutex +} +``` + +</td><td> + +```go +type countingWriteCloser struct { + // Хорошо: Write() предоставлен на этом внешнем уровне для конкретной цели и делегирует работу Write() внутреннего типа. + io.WriteCloser + + count int +} + +func (w *countingWriteCloser) Write(bs []byte) (int, error) { + w.count += len(bs) + return w.WriteCloser.Write(bs) +} +``` + +</td></tr> +<tr><td> + +```go +type Book struct { + // Плохо: указатель меняет полезность нулевого значения + io.ReadWriter + + // другие поля +} + +// позже +var b Book +b.Read(...) // panic: nil pointer +b.String() // panic: nil pointer +b.Write(...) // panic: nil pointer +``` + +</td><td> + +```go +type Book struct { + // Хорошо: имеет полезное нулевое значение + bytes.Buffer + + // другие поля +} + +// позже + +var b Book +b.Read(...) // ok +b.String() // ok +b.Write(...) // ok +``` + +</td></tr> +<tr><td> + +```go +type Client struct { + sync.Mutex + sync.WaitGroup + bytes.Buffer + url.URL +} +``` + +</td><td> + +```go +type Client struct { + mtx sync.Mutex + wg sync.WaitGroup + buf bytes.Buffer + url url.URL +} +``` + +</td></tr> +</tbody></table> + +### Объявление локальных переменных + +Короткое объявление переменных (`:=`) должно использоваться, если переменная +явно устанавливается в некоторое значение. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +var s = "foo" +``` + +</td><td> + +```go +s := "foo" +``` + +</td></tr> +</tbody></table> + +Однако бывают случаи, когда значение по умолчанию понятнее при использовании +ключевого слова `var`. [Объявление пустых +срезов](https://go.dev/wiki/CodeReviewComments#declaring-empty-slices), +например. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +func f(list []int) { + filtered := []int{} + for _, v := range list { + if v > 10 { + filtered = append(filtered, v) + } + } +} +``` + +</td><td> + +```go +func f(list []int) { + var filtered []int + for _, v := range list { + if v > 10 { + filtered = append(filtered, v) + } + } +} +``` + +</td></tr> +</tbody></table> + +### nil — это валидный срез + +`nil` — это валидный срез длины 0. Это означает, что: + +- Не следует явно возвращать срез длины ноль. Возвращайте `nil` вместо этого. + + <table> + <thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> + <tbody> + <tr><td> + + ```go + if x == "" { + return []int{} + } + ``` + + </td><td> + + ```go + if x == "" { + return nil + } + ``` + + </td></tr> + </tbody></table> + +- Чтобы проверить, пуст ли срез, всегда используйте `len(s) == 0`. Не проверяйте + на `nil`. + + <table> + <thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> + <tbody> + <tr><td> + + ```go + func isEmpty(s []string) bool { + return s == nil + } + ``` + + </td><td> + + ```go + func isEmpty(s []string) bool { + return len(s) == 0 + } + ``` + + </td></tr> + </tbody></table> + +- Нулевое значение (срез, объявленный с `var`) можно использовать сразу без +`make()`. + + <table> + <thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> + <tbody> + <tr><td> + +```go +nums := []int{} +// или, nums := make([]int) + +if add1 { + nums = append(nums, 1) +} + +if add2 { + nums = append(nums, 2) +} +``` + +</td><td> + +```go +var nums []int + +if add1 { + nums = append(nums, 1) +} + +if add2 { + nums = append(nums, 2) +} +``` + +</td></tr> +</tbody></table> + +Помните, что хотя nil-срез является валидным срезом, он не эквивалентен +выделенному срезу длины 0 — один является nil, а другой нет — и они могут +обрабатываться по-разному в разных ситуациях (например, при сериализации). + +### Уменьшайте область видимости переменных + +По возможности уменьшайте область видимости переменных и констант. Не уменьшайте +область видимости, если это противоречит [Уменьшению +вложенности](#уменьшайте-вложенность). + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +err := os.WriteFile(name, data, 0644) +if err != nil { + return err +} +``` + +</td><td> + +```go +if err := os.WriteFile(name, data, 0644); err != nil { + return err +} +``` + +</td></tr> +</tbody></table> + +Если вам нужен результат вызова функции вне if, то не следует пытаться уменьшить +область видимости. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +if data, err := os.ReadFile(name); err == nil { + err = cfg.Decode(data) + if err != nil { + return err + } + + fmt.Println(cfg) + return nil +} else { + return err +} +``` + +</td><td> + +```go +data, err := os.ReadFile(name) +if err != nil { + return err +} + +if err := cfg.Decode(data); err != nil { + return err +} + +fmt.Println(cfg) +return nil +``` + +</td></tr> +</tbody></table> + +Константам не нужно быть глобальными, если они не используются в нескольких +функциях или файлах или являются частью внешнего контракта пакета. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +const ( + _defaultPort = 8080 + _defaultUser = "user" +) + +func Bar() { + fmt.Println("Default port", _defaultPort) +} +``` + +</td><td> + +```go +func Bar() { + const ( + defaultPort = 8080 + defaultUser = "user" + ) + fmt.Println("Default port", defaultPort) +} +``` + +</td></tr> +</tbody></table> + +### Избегайте «голых» параметров + +«Голые» параметры в вызовах функций могут ухудшать читаемость. Добавляйте +комментарии в стиле C (`/* ... */`) для имён параметров, когда их значение +неочевидно. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +// func printInfo(name string, isLocal, done bool) + +printInfo("foo", true, true) +``` + +</td><td> + +```go +// func printInfo(name string, isLocal, done bool) + +printInfo("foo", true /* isLocal */, true /* done */) +``` + +</td></tr> +</tbody></table> + +Ещё лучше заменить «голые» типы `bool` пользовательскими типами для более +читаемого и типобезопасного кода. Это позволяет в будущем иметь более двух +состояний (true/false) для этого параметра. + +```go +type Region int + +const ( + UnknownRegion Region = iota + Local +) + +type Status int + +const ( + StatusReady Status = iota + 1 + StatusDone + // Возможно, в будущем у нас будет StatusInProgress. +) + +func printInfo(name string, region Region, status Status) +``` + +### Используйте сырые строковые литералы, чтобы избежать экранирования + +Go поддерживает [сырые строковые +литералы](https://go.dev/ref/spec#raw_string_lit), которые могут занимать +несколько строк и включать кавычки. Используйте их, чтобы избежать ручного +экранирования строк, которое гораздо труднее читать. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +wantError := "unknown name:\"test\"" +``` + +</td><td> + +```go +wantError := `unknown error:"test"` +``` + +</td></tr> +</tbody></table> + +### Инициализация структур + +#### Используйте имена полей для инициализации структур + +Вы почти всегда должны указывать имена полей при инициализации структур. Теперь +это обеспечивается [`go vet`](https://pkg.go.dev/cmd/vet). + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +k := User{"John", "Doe", true} +``` + +</td><td> + +```go +k := User{ + FirstName: "John", + LastName: "Doe", + Admin: true, +} +``` + +</td></tr> +</tbody></table> + +Исключение: Имена полей _могут_ быть опущены в таблицах тестов, когда полей 3 +или меньше. + +```go +tests := []struct{ + op Operation + want string +}{ + {Add, "add"}, + {Subtract, "subtract"}, +} +``` + +#### Опускайте поля с нулевыми значениями в структурах + +При инициализации структур с именами полей опускайте поля, имеющие нулевые +значения, если они не предоставляют значимый контекст. В противном случае +позвольте Go автоматически установить их в нулевые значения. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +user := User{ + FirstName: "John", + LastName: "Doe", + MiddleName: "", + Admin: false, +} +``` + +</td><td> + +```go +user := User{ + FirstName: "John", + LastName: "Doe", +} +``` + +</td></tr> +</tbody></table> + +Это помогает уменьшить шум для читателей, опуская значения, которые являются +стандартными в данном контексте. Указываются только значимые значения. + +Включайте нулевые значения, когда имена полей предоставляют значимый контекст. +Например, тестовые случаи в [Табличных тестах](#табличные-тесты) могут выиграть +от указания имён полей, даже когда они имеют нулевые значения. + +```go +tests := []struct{ + give string + want int +}{ + {give: "0", want: 0}, + // ... +} +``` + +#### Используйте `var` для структур с нулевыми значениями + +Когда все поля структуры опущены в объявлении, используйте форму `var` для +объявления структуры. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +user := User{} +``` + +</td><td> + +```go +var user User +``` + +</td></tr> +</tbody></table> + +Это отличает структуры с нулевыми значениями от тех, у которых есть ненулевые +поля, аналогично различию, создаваемому для [инициализации +карт](#инициализация-карт), и соответствует тому, как мы предпочитаем [объявлять +пустые срезы](https://go.dev/wiki/CodeReviewComments#declaring-empty-slices). + +#### Инициализация ссылок на структуры + +Используйте `&T{}` вместо `new(T)` при инициализации ссылок на структуры, чтобы +это было согласовано с инициализацией структур. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +sval := T{Name: "foo"} + +// несогласованно +sptr := new(T) +sptr.Name = "bar" +``` + +</td><td> + +```go +sval := T{Name: "foo"} + +sptr := &T{Name: "bar"} +``` + +</td></tr> +</tbody></table> + +### Инициализация карт + +Предпочитайте `make(..)` для пустых карт и карт, заполняемых программно. Это +делает инициализацию карт визуально отличной от объявления и позволяет легко +добавить подсказку размера позже, если она доступна. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +var ( + // m1 безопасна для чтения и записи; + // m2 вызовет панику при записи. + m1 = map[T1]T2{} + m2 map[T1]T2 +) +``` + +</td><td> + +```go +var ( + // m1 безопасна для чтения и записи; + // m2 вызовет панику при записи. + m1 = make(map[T1]T2) + m2 map[T1]T2 +) +``` + +</td></tr> +<tr><td> + +Объявление и инициализация визуально похожи. + +</td><td> + +Объявление и инициализация визуально различны. + +</td></tr> +</tbody></table> + +По возможности предоставляйте подсказку ёмкости при инициализации карт с помощью +`make()`. См. [Указание подсказки ёмкости для +карт](#указание-подсказки-ёмкости-для-карт) для получения дополнительной +информации. + +С другой стороны, если карта содержит фиксированный список элементов, +используйте литералы карт для её инициализации. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +m := make(map[T1]T2, 3) +m[k1] = v1 +m[k2] = v2 +m[k3] = v3 +``` + +</td><td> + +```go +m := map[T1]T2{ + k1: v1, + k2: v2, + k3: v3, +} +``` + +</td></tr> +</tbody></table> + +Основное правило — использовать литералы карт при добавлении фиксированного +набора элементов во время инициализации, в противном случае используйте `make` +(и указывайте подсказку размера, если доступна). + +### Строки формата вне Printf + +Если вы объявляете строки формата для функций в стиле `Printf` вне строкового +литерала, сделайте их значениями `const`. + +Это помогает `go vet` выполнять статический анализ строки формата. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +msg := "unexpected values %v, %v\n" +fmt.Printf(msg, 1, 2) +``` + +</td><td> + +```go +const msg = "unexpected values %v, %v\n" +fmt.Printf(msg, 1, 2) +``` + +</td></tr> +</tbody></table> + +### Именование функций в стиле Printf + +Когда вы объявляете функцию в стиле `Printf`, убедитесь, что `go vet` может её +обнаружить и проверить строку формата. + +Это означает, что следует использовать предопределённые имена функций в стиле +`Printf`, если это возможно. `go vet` проверяет их по умолчанию. См. [Printf +family](https://pkg.go.dev/cmd/vet#hdr-Printf_family) для получения +дополнительной информации. + +Если использование предопределённых имён невозможно, заканчивайте выбранное вами +имя на f: `Wrapf`, а не `Wrap`. `go vet` можно попросить проверять определённые +имена в стиле `Printf`, но они должны заканчиваться на f. + +```shell +go vet -printfuncs=wrapf,statusf +``` + +См. также [go vet: Printf family +check](https://kuzminva.wordpress.com/2017/11/07/go-vet-printf-family-check/). + +## Паттерны + +### Табличные тесты + +Табличные тесты с [подтестами](https://go.dev/blog/subtests) могут быть полезным +паттерном для написания тестов, чтобы избежать дублирования кода, когда основная +тестовая логика повторяется. + +Если тестируемую систему нужно проверить на соответствие _нескольким условиям_, +где определённые части входных и выходных данных меняются, следует использовать +табличные тесты, чтобы уменьшить избыточность и улучшить читаемость. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +// func TestSplitHostPort(t *testing.T) + +host, port, err := net.SplitHostPort("192.0.2.0:8000") +require.NoError(t, err) +assert.Equal(t, "192.0.2.0", host) +assert.Equal(t, "8000", port) + +host, port, err = net.SplitHostPort("192.0.2.0:http") +require.NoError(t, err) +assert.Equal(t, "192.0.2.0", host) +assert.Equal(t, "http", port) + +host, port, err = net.SplitHostPort(":8000") +require.NoError(t, err) +assert.Equal(t, "", host) +assert.Equal(t, "8000", port) + +host, port, err = net.SplitHostPort("1:8") +require.NoError(t, err) +assert.Equal(t, "1", host) +assert.Equal(t, "8", port) +``` + +</td><td> + +```go +// func TestSplitHostPort(t *testing.T) + +tests := []struct{ + give string + wantHost string + wantPort string +}{ + { + give: "192.0.2.0:8000", + wantHost: "192.0.2.0", + wantPort: "8000", + }, + { + give: "192.0.2.0:http", + wantHost: "192.0.2.0", + wantPort: "http", + }, + { + give: ":8000", + wantHost: "", + wantPort: "8000", + }, + { + give: "1:8", + wantHost: "1", + wantPort: "8", + }, +} + +for _, tt := range tests { + t.Run(tt.give, func(t *testing.T) { + host, port, err := net.SplitHostPort(tt.give) + require.NoError(t, err) + assert.Equal(t, tt.wantHost, host) + assert.Equal(t, tt.wantPort, port) + }) +} +``` + +</td></tr> +</tbody></table> + +Табличные тесты облегчают добавление контекста к сообщениям об ошибках, +уменьшают дублирование логики и позволяют добавлять новые тестовые случаи. + +Мы следуем соглашению, что срез структур называется `tests`, а каждый тестовый +случай — `tt`. Кроме того, мы рекомендуем явно указывать входные и выходные +значения для каждого тестового случая с префиксами `give` и `want`. + +```go +tests := []struct{ + give string + wantHost string + wantPort string +}{ + // ... +} + +for _, tt := range tests { + // ... +} +``` + +#### Избегайте излишней сложности в табличных тестах + +Табличные тесты могут быть трудны для чтения и поддержки, если подтесты содержат +условные проверки или другую разветвлённую логику. Табличные тесты **НЕ ДОЛЖНЫ** +использоваться всякий раз, когда внутри подтестов требуется сложная или условная +логика (т.е. сложная логика внутри цикла `for`). + +Большие, сложные табличные тесты ухудшают читаемость и поддерживаемость, потому +что читателям тестов может быть трудно отлаживать возникающие сбои тестов. + +Такие табличные тесты следует разделить либо на несколько таблиц тестов, либо на +несколько отдельных функций `Test...`. + +К некоторым идеалам, к которым стоит стремиться, относятся: + +- Фокусировка на самой узкой единице поведения +- Минимизация «глубины теста» и избегание условных проверок (см. ниже) +- Обеспечение того, что все поля таблицы используются во всех тестах +- Обеспечение того, что вся тестовая логика выполняется для всех случаев таблицы + +В этом контексте «глубина теста» означает «внутри данного теста, количество +последовательных проверок, требующих выполнения предыдущих проверок» (аналогично +цикломатической сложности). Наличие «более мелких» тестов означает меньше связей +между проверками и, что более важно, что эти проверки по умолчанию менее +вероятно будут условными. + +Конкретно, табличные тесты могут стать запутанными и трудными для чтения, если +они используют несколько ветвящихся путей (например, `shouldError`, `expectCall` +и т.д.), используют много операторов `if` для специфичных ожиданий моков +(например, `shouldCallFoo`) или размещают функции внутри таблицы (например, +`setupMocks func(*FooMock)`). + +Однако при тестировании поведения, которое меняется только в зависимости от +изменённых входных данных, может быть предпочтительнее группировать схожие +случаи вместе в табличном тесте, чтобы лучше иллюстрировать, как поведение +меняется при всех входных данных, а не разделять иначе сопоставимые единицы на +отдельные тесты и делать их более трудными для сравнения и противопоставления. + +Если тело теста короткое и простое, допустимо иметь единственный ветвящийся путь +для случаев успеха и неудачи с полем таблицы типа `shouldErr` для указания +ожиданий ошибки. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +func TestComplicatedTable(t *testing.T) { + tests := []struct { + give string + want string + wantErr error + shouldCallX bool + shouldCallY bool + giveXResponse string + giveXErr error + giveYResponse string + giveYErr error + }{ + // ... + } + + for _, tt := range tests { + t.Run(tt.give, func(t *testing.T) { + // настройка моков + ctrl := gomock.NewController(t) + xMock := xmock.NewMockX(ctrl) + if tt.shouldCallX { + xMock.EXPECT().Call().Return( + tt.giveXResponse, tt.giveXErr, + ) + } + yMock := ymock.NewMockY(ctrl) + if tt.shouldCallY { + yMock.EXPECT().Call().Return( + tt.giveYResponse, tt.giveYErr, + ) + } + + got, err := DoComplexThing(tt.give, xMock, yMock) + + // проверка результатов + if tt.wantErr != nil { + require.EqualError(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, want, got) + }) + } +} +``` + +</td><td> + +```go +func TestShouldCallX(t *testing.T) { + // настройка моков + ctrl := gomock.NewController(t) + xMock := xmock.NewMockX(ctrl) + xMock.EXPECT().Call().Return("XResponse", nil) + + yMock := ymock.NewMockY(ctrl) + + got, err := DoComplexThing("inputX", xMock, yMock) + + require.NoError(t, err) + assert.Equal(t, "want", got) +} + +func TestShouldCallYAndFail(t *testing.T) { + // настройка моков + ctrl := gomock.NewController(t) + xMock := xmock.NewMockX(ctrl) + + yMock := ymock.NewMockY(ctrl) + yMock.EXPECT().Call().Return("YResponse", nil) + + _, err := DoComplexThing("inputY", xMock, yMock) + assert.EqualError(t, err, "Y failed") +} +``` + +</td></tr> +</tbody></table> + +Эта сложность делает тест более трудным для изменения, понимания и +доказательства его корректности. + +Хотя строгих правил нет, читаемость и поддерживаемость всегда должны быть +главными при выборе между табличными тестами и отдельными тестами для +множественных входов/выходов системы. + +#### Параллельные тесты + +Параллельные тесты, как и некоторые специализированные циклы (например, те, что +создают горутины или захватывают ссылки как часть тела цикла), должны заботиться +о явном присваивании переменных цикла внутри области видимости цикла, чтобы +гарантировать, что они содержат ожидаемые значения. + +```go +tests := []struct{ + give string + // ... +}{ + // ... +} + +for _, tt := range tests { + tt := tt // для t.Parallel + t.Run(tt.give, func(t *testing.T) { + t.Parallel() + // ... + }) +} +``` + +В примере выше мы должны объявить переменную `tt` с областью видимости итерации +цикла из-за использования `t.Parallel()` ниже. Если мы этого не сделаем, +большинство или все тесты получат неожиданное значение для `tt` или значение, +которое меняется во время их выполнения. + +<!-- TODO: Объяснить, как использовать _test пакеты. --> + +### Функциональные опции + +Функциональные опции — это паттерн, в котором вы объявляете непрозрачный тип +`Option`, который записывает информацию в некоторую внутреннюю структуру. Вы +принимаете переменное количество этих опций и действуете на основе полной +информации, записанной опциями во внутренней структуре. + +Используйте этот паттерн для необязательных аргументов в конструкторах и других +публичных API, которые, как вы предвидите, могут потребовать расширения, +особенно если у вас уже есть три или более аргументов в этих функциях. + +<table> +<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead> +<tbody> +<tr><td> + +```go +// package db + +func Open( + addr string, + cache bool, + logger *zap.Logger +) (*Connection, error) { + // ... +} +``` + +</td><td> + +```go +// package db + +type Option interface { + // ... +} + +func WithCache(c bool) Option { + // ... +} + +func WithLogger(log *zap.Logger) Option { + // ... +} + +// Open создаёт соединение. +func Open( + addr string, + opts ...Option, +) (*Connection, error) { + // ... +} +``` + +</td></tr> +<tr><td> + +Параметры cache и logger всегда должны предоставляться, даже если пользователь +хочет использовать значения по умолчанию. + +```go +db.Open(addr, db.DefaultCache, zap.NewNop()) +db.Open(addr, db.DefaultCache, log) +db.Open(addr, false /* cache */, zap.NewNop()) +db.Open(addr, false /* cache */, log) +``` + +</td><td> + +Опции предоставляются только при необходимости. + +```go +db.Open(addr) +db.Open(addr, db.WithLogger(log)) +db.Open(addr, db.WithCache(false)) +db.Open( + addr, + db.WithCache(false), + db.WithLogger(log), +) +``` + +</td></tr> +</tbody></table> + +Наш рекомендуемый способ реализации этого паттерна — использование интерфейса +`Option` с неэкспортируемым методом, записывающим опции в неэкспортируемую +структуру `options`. + +```go +type options struct { + cache bool + logger *zap.Logger +} + +type Option interface { + apply(*options) +} + +type cacheOption bool + +func (c cacheOption) apply(opts *options) { + opts.cache = bool(c) +} + +func WithCache(c bool) Option { + return cacheOption(c) +} + +type loggerOption struct { + Log *zap.Logger +} + +func (l loggerOption) apply(opts *options) { + opts.logger = l.Log +} + +func WithLogger(log *zap.Logger) Option { + return loggerOption{Log: log} +} + +// Open создаёт соединение. +func Open( + addr string, + opts ...Option, +) (*Connection, error) { + options := options{ + cache: defaultCache, + logger: zap.NewNop(), + } + + for _, o := range opts { + o.apply(&options) + } + + // ... +} +``` + +Обратите внимание, что существует метод реализации этого паттерна с +использованием замыканий, но мы считаем, что паттерн выше предоставляет больше +гибкости авторам и легче отлаживается и тестируется пользователями. В частности, +он позволяет сравнивать опции друг с другом в тестах и моках, в отличие от +замыканий, где это невозможно. Кроме того, он позволяет опциям реализовывать +другие интерфейсы, включая `fmt.Stringer`, что позволяет создавать удобочитаемые +строковые представления опций. + +См. также, + +- [Self-referential functions and the design of + options](https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html) +- [Functional options for friendly + APIs](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis) + +<!-- TODO: заменить это на структуры параметров и функциональные опции, когда использовать одно против другого --> + +## Линтинг + +Более важно, чем любой «благословленный» набор линтеров, — линтить +последовательно по всей кодовой базе. + +Мы рекомендуем использовать следующие линтеры как минимум, потому что считаем, +что они помогают выявить наиболее распространённые проблемы, а также +устанавливают высокую планку качества кода, не будучи излишне предписывающими: + +- [errcheck](https://github.com/kisielk/errcheck) для обеспечения обработки + ошибок +- [goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports) для + форматирования кода и управления импортами +- [golint](https://github.com/golang/lint) для указания на распространённые + стилевые ошибки +- [govet](https://pkg.go.dev/cmd/vet) для анализа кода на распространённые + ошибки +- [staticcheck](https://staticcheck.dev) для выполнения различных проверок + статического анализа + +### Запускатели линтеров + +Мы рекомендуем [golangci-lint](https://github.com/golangci/golangci-lint) в +качестве основного запускателя линтеров для кода на Go, во многом благодаря его +производительности в больших кодовых базах и возможности настраивать и +использовать многие канонические линтеры одновременно. Этот репозиторий содержит +пример +[.golangci.yml](https://github.com/uber-go/guide/blob/master/.golangci.yml) +файла конфигурации с рекомендуемыми линтерами и настройками. + +golangci-lint имеет [различные +линтеры](https://golangci-lint.run/usage/linters/), доступные для использования. +Вышеуказанные линтеры рекомендуются в качестве базового набора, и мы поощряем +команды добавлять любые дополнительные линтеры, которые имеют смысл для их +проектов. diff --git a/content/pages/setup/_index.md b/content/pages/setup/_index.md new file mode 100644 index 0000000..92c00a3 --- /dev/null +++ b/content/pages/setup/_index.md @@ -0,0 +1,6 @@ +--- +order: 10 +title: Мой сетап 2025 +--- + +Пополняемый раздел с моим сетапом diff --git a/content/pages/setup/laptop.md b/content/pages/setup/laptop.md new file mode 100644 index 0000000..4aa14c9 --- /dev/null +++ b/content/pages/setup/laptop.md @@ -0,0 +1,36 @@ +--- +order: 20 +title: Ноутбук +--- + +Уже достаточно давно я сторонник исключительно ноутбуков и никак не воспринимаю +стационарные компьютеры. При этом, я считаю, что ноутбук должен быть +одновременно и мощным, мобильным и, что важно, ремонтопригодным, даже в домашних +условиях. Понимаю, что на практике это практически не осуществимо. Но самым +близким к этому для меня стал Lenovo Thinkpad T14 Gen4, версия на intel (это +важно, т.к. только intel версия поддерживает расширение ОЗУ). ОЗУ я в нем добил +до 48Гб, пока мне хватает. + +Вот основное что на нём установлено: + +- ОС: AltLinux p11. +- DE: Gnome 48 +- Эмулятор терминала: Ghostty. +- Оболочка: zsh. +- Текстовый редактор: Neovim/VSCodium +- Браузер: Яндекс Браузер. +- Коммуникации: Thunderbird/Neomutt (e-mail), Dino (jabber), telegram desktop + (будь он неладен). +- Музыка: Rhythmbox. +- Видео: VLC. +- Книги: лежат на NAS + сразу достаточно много загрузил в читалку. Была calibre, + но я так и не оценил от неё плюсов и дропнул. +- Основной язык: golang (удивительно). +- Синхронизация: с помощью syncthing синхронизирую ноутбук <-> NAS <-> смартфон + только одну директорию: Документы. По сути, не считая директории с исходниками + проектов, это моя самая важная директория на компьютере. +- Хранилище знаний: <del>[Obsidian](/posts/2024-11-17-obsidian/)</del> пока + присматриваюсь к ZK или к обычным текстовым файлам + +Это база, остальное не столько важно. + diff --git a/content/pages/setup/nas.md b/content/pages/setup/nas.md new file mode 100644 index 0000000..a3f3d25 --- /dev/null +++ b/content/pages/setup/nas.md @@ -0,0 +1,32 @@ +--- +order: 40 +title: NAS +--- + +Synology DS420+ + +Это, наверное, одно из лучших моих вложений денег. Мой личный суверенитет от +облаков. + +Основные функции: + +* Собственно, синхронизация той самой важной для меня директории «Документы». + Потому что её потерю я точно не смогу восполнить. +* Инкрементальный бекап ноутбука через rsync, как я где-то уже писал, вроде в + телеге (может стоит и здесь оставить заметку на всякий случай?). +* Календарь / контакты по webdav. Просто приятно. Хорошая альтернатива облакам. +* Торрентокачалка. Пиратство морально оправдано. Точка. +* Медиасервер. В основном чтобы просматривать трофейный контент на смарт тв. +* TT-RSS. Читаю RSS ленты на смартфоне и ноутбуке, а TT-RSS позволяет + синхронизировать уже прочитанное. + +Утерянный или недоделанный функционал: + +* Синхронизация подкастов по протоколу gpodder (AntennaPod на смартфоне и в + автомобиле + Kasts на ноутбуке). Раньше синкал через Nextcloud, отказался от + него. Пилю синхронизацию в свободное время. Когда допилю — расскажу в этом + блоге. +* Медиастримминг музыки с NAS на смартфон / компьютер / автомобиль. Пока не + дошли руки наладить. Обхожусь дедовскими способами — на каждом устройстве + просто локальная медиатека. + diff --git a/content/pages/setup/pda.md b/content/pages/setup/pda.md new file mode 100644 index 0000000..59aa33c --- /dev/null +++ b/content/pages/setup/pda.md @@ -0,0 +1,38 @@ +--- +order: 30 +title: Смартфон +--- +OnePlus 10T (рутованный) + +Бо́льшая часть гуглоговна удалена или отключена. В основном, стараюсь +использовать софт из F-Droid. + +В основном, стараюсь держать минимально необходимый набор софта — банковские +приложения, навигацию, коммуникационные приложения, читалки, да пару простеньких +игрушек для скрашивания досуга. + +Самое главное — по максимуму отключаю всевозможные уведомления. Ничего не должно +меня тревожить. Всё так же остаюсь сторонником идеи что пуши не нужны, а что-то +_действительно_ важное — и так придёт на e-mail. Единственное исключение — +jabber и рабочий мессенджер. Первый — это этакий бонус узкому элитарному кругу +пользователей джаббера, а второй — поскольку рабочие обязанности важнее моих +заморочек. + +Из интересного: + +* ntodotxt - тудушник, который отлично работает с Todo.txt. +* Conversations - jabber. +* syncthing - синхронизация. +* DAVx5 - приложение для синхронизации календарей и контактов с NAS. +* DS File, DS Get - приложения для работы с NAS. +* OSMand и OrganicMaps - оффлайн навигация. Ни раз выручала, когда онлайновый + карты приказывали долго жить без интернетов. +* AntennaPod - ИМХО лучшее приложение для подкастов. +* AIMP - замечательный аудио плеер для локальной музыки. +* VLC - отличный видеоплеер. +* KDE Connect - стоит исключительно для быстрого перебрасывания файлов на + компьютер. + +Вроде, из самого интересного — всё. Буду дописывать, если что ещё вспомню. + + diff --git a/content/posts/2021-02-13-jsonnet.md b/content/posts/2021-02-13-jsonnet.md new file mode 100644 index 0000000..e4ea27e --- /dev/null +++ b/content/posts/2021-02-13-jsonnet.md @@ -0,0 +1,101 @@ +--- +categories: +- Без рубрики +date: '2021-02-13T22:08:19Z' +image: files/2021-02-13-jsonnet_logo.webp +tags: +- go +- it +- разное +title: Jsonnet +--- + +Редко такое бывает, что случайно натыкаешься на какую-то технологию и она +вызывает вау-эффект и буквально переворачивает всё верх дном. На днях для меня +такой технологией стал [Jsonnet](https://jsonnet.org/) от Google. + +В кратце, это надмножество JSON являющееся языком описания шаблонов. Пока звучит +не очень круто, да? На деле это офигенный Тьюринг полный функциональный язык, +результатом выполнения которого будет сформированый JSON (и не только) +документ(или несколько документов[^1]). +[^1]:https://jsonnet.org/learning/getting_started.html#multi + +Если интересно, рекомендую сразу переходить к туториалу — +https://jsonnet.org/learning/tutorial.html. + +## Почему же это круто? + +Ну, во-первых, он реально мощный и простой. С его помощью можно формировать +документы любой сложности. + +Во-вторых, его можно встроить в свою программу на Go (и не только, но на Go — +проще всего — https://jsonnet.org/ref/bindings.html), и это даст бесплатно +мощный DSL для написания очень гибких конфигов. + +В третьих, ну камон, приятно же когда компьютер берет на себя рутинную работу по +формированию больших и сложных JSON’ов! + +## Пример + +Накидал простенький пример который формирует конфигурацию пайплайна для +гипотетической CI системы: + +```json +local map(arr, predicate) = // определяем функцию map +if std.length(arr) == 0 then + [] + else + [ + predicate(arr[0]) + ] + map(arr[1:], predicate); // функциональненько! +local tasks = [['go1.14', '1.14-alpine'],['go1.15', '1.15-alpine'],['go1.16-RC', '1.16-rc-alpine']]; +local commands = ['go build', 'go test']; // Общая часть +{ // Результирующий JSON + pipeline: map(tasks, function (task) { // Вызов map от tasks + name: task[0], + image: "golang:"+task[1], + commands: commands, + }) +} +``` + +Результат: + +```json +{ + "pipeline": [ + { + "commands": [ + "go build", + "go test" + ], + "image": "golang:1.14-alpine", + "name": "go1.14" + }, + { + "commands": [ + "go build", + "go test" + ], + "image": "golang:1.15-alpine", + "name": "go1.15" + }, + { + "commands": [ + "go build", + "go test" + ], + "image": "golang:1.16-rc-alpine", + "name": "go1.16-RC" + } + ] +} +``` + +Круть же! + +Да, на небольшом примере не очень показательно, но даже тут, скажем, при добавлении новой цели сборки будет достаточно слегка подправить массив tasks и автоматически сформируется все остальное, а не копипаст целой секции и ручная правка в нужных местах. + +Я оставил за скобками то, что этот шаблонизатора позволяет формировать не только JSON но и фактически любой другой текстовый формат. И даже из одного скрипта формировать несколько документов разного формата. При этом локальные переменные будут использоваться общие. Теоретически, если упороться, можно одним скриптом сформировать весь /etc на новом сервере. Почему бы и нет?:) + +Не знаю смог ли передать ощущение своего восторга, но я охренеть как рад и жду выходных, чтобы с головой нырнуть в эту технологию, которая открывает столько новых интересных перспектив!
\ No newline at end of file diff --git a/content/posts/2021-05-13-цифровая-гигиена.md b/content/posts/2021-05-13-цифровая-гигиена.md new file mode 100644 index 0000000..d9fcede --- /dev/null +++ b/content/posts/2021-05-13-цифровая-гигиена.md @@ -0,0 +1,91 @@ +--- +categories: +- Без рубрики +date: '2021-05-13T15:37:01Z' +tags: +- it +- паранойя +- разное +title: Немного о цифровой гигиене +--- + +## Вступление + +Как раз вступление тут особо и не нужно. Ни для кого не открою америки, что в +современном цифровом обществе все мы являемся товаром для интернет-медиа +гигантов, того же фейсбука да гугла. Не скажу что это для меня, как личности +опасно или вредно, но мне это неприятно. Решил с этим что-то делать. + +## Вводные + +- В интернетах я уже очень давно и много где и как “наследил” своими данными. И + с этим уже ничего не поделать. +- У меня в телефоне и на всех компьютерах куча приложений работающих с + интернетом, и не только мессенджеры. +- У меня достаточно узкий круг людей с кем бы я хотел быть на связи, и не хочу + чтобы мои действия как-то ухудшили или усложнили их жизнь. +- Вопрос анонимности для меня не стоит, я не анонимен и это моё осознанное + решение. Я законопослушный человек и прятаться мне не от кого. И да, я знаю + что этот тезис стараниями либерах нынче пытаются выставить как глупость, но + нет. Глупость — это слушать либерах, а не иметь свою голову на плечах. И + контртезис “Гы гы гы, ну раз тебе нечего скрывать — поставь камеру у себя в + спальне и ванной” даже комментировать не буду в силу его ущербности. +- Я пользователь техники Apple и с этим уже ничего не поделать, менять целиком + экосистему для меня не вариант (это очень дорого, бессмысленно, а местами и + невозможно, например, рабочий мак мне поменять не на что). И да, есть наивная + надежда что у яблок в плане приватности всё получше чем у ведроидов. Во всяком + случае по сравнению со стоком. Гиковские прошивки с вырезанными зондами в + расчет не беру, верю что у них совсем всё хорошо. + +## Цели + +1. Уменьшить информационный шум вокруг себя и тем самым улучшить качество жизни. +2. Уменьшить свой “информационный след” +3. Иметь больше контроля над своими данными, чтобы мои волосы стали мягкими и + шелковистыми. + +## Наброски плана + +1. Перейти максимально на собственные ресурсы, которые я контролирую и которые + *точно* не сливают ничего налево.Примерно так: социалки => + <https://soc.neonxp.ru/> , GitHub => <https://gitrepo.ru/> , Облачные + диски => локальный NAS Synology и т.д. +2. Мне надо сократить мессенджеры в идеале до одного, не считая корпоративного + рабочего. Тут всё просто — оставляю Telegram, остальные сношу. +3. Мне надо отказаться от неэтичных социальных сетей, где я не могу полностью + контролировать свои данные. +4. При отказе от социальных сетей чтобы не доставить проблем моим контактам надо + оставить “новый адрес” по которому со мной можно связаться и, например, этот + пост. Я не хочу чтобы для всех мои действия были прозрачны и понятны, а не + “молча удалиться” оставив кого-то в недоумении. +5. Везде где возможно отключить или заблокировать телеметрию, чтобы как можно + меньше моих данных неконтролируемо утекало. Да, полностью не перекрыть, но + сократить возможно. +6. Для связи с “миром” оставить только e-mail как наиболее удобный асинхронный + метод коммуникации. + +## Дальнейшие шаги + +1. Превратить наброски плана в цельный план. Написать манифест? Возможно. +2. Подготовить “визитку” с актуальными контактами и объяснением что произошло. + Причем как в виде изображения, так и текста. +3. Вышеуказанную визитку поместить на уже неактуальных для меня местах обитания + (инстаграм, вк и проч). Удаляться не хочу. Жалко контент за столько лет, да и + пункт 3 предыдущего абзаца. +4. Удалить “лишние” приложения от вышеуказанных сервисов. +5. Разлогиниться в этих сервисах и очистить браузеры от них, чтобы исключить + треккинг на сторонних сайтах. +6. ????? +7. PROFIT! + +## Обратная связь + +Очень бы хотелось получить обратную связь по моему плану. Комментарии про то что +упустил и предложения улучшений приветствуются в комментариях к посту, в +комментариях к [телеграм каналу](https://t.me/neonxp), или на почту +<a.kiryukhin@mail.ru> (кстати, стоит наверное и почту перевести к себе? Но пока +уровень сервиса врядли смогу адекватный обеспечить) + +*UPD:* Да, я знаю что уже данные так и останутся в чужих руках, но со временем +они будут всё больше и больше протухать, а мой “цифровой профиль” терять +актуальность. Ведь я не скала, я тоже меняюсь и ухожу от этого профиля.
\ No newline at end of file diff --git a/content/posts/2022-05-30-возрождение.md b/content/posts/2022-05-30-возрождение.md new file mode 100644 index 0000000..c6721c8 --- /dev/null +++ b/content/posts/2022-05-30-возрождение.md @@ -0,0 +1,27 @@ +--- +categories: +- Без рубрики +date: '2022-05-30T23:37:00Z' +tags: +- блог +- моё +title: Возрождение? +--- + +Определенно, вести блог это не мое. Учитывая, что последний пост был год назад — +sad but true. + +Не буду говорить, что “вот сейчас то уж точно буду вести регулярно”. Нет не +буду. + +Но раз в полгода-год, наверное все же буду. + +Из новостей, что не писал в канал, наверное, только парочка: + +1. сейчас всё свободное время пилю свой петпроджект 😉 Пока что выходит ух какая + красота. Но об этом как-нибудь в другой раз, как говорится, пол работы не + показывают 🙂 +2. в ленивом режиме начали заниматься вопросами улучшения жилищных условий. + Давно пора. + +До встречи когда-нибудь потом 🙂
\ No newline at end of file diff --git a/content/posts/2022-05-31-golang-1.md b/content/posts/2022-05-31-golang-1.md new file mode 100644 index 0000000..75e1b64 --- /dev/null +++ b/content/posts/2022-05-31-golang-1.md @@ -0,0 +1,44 @@ +--- +categories: +- Без рубрики +date: '2022-05-31T01:00:00Z' +tags: +- go +- it +title: Golang подборка 1 +--- + +Просто собираю подборку интересных ссылок по гошке на почитать потом. + +- [Extra](https://github.com/neonxp/extra) — Моё. Пакет с разными полезными + функциями без дополнительных зависимостей. +- Серия видосов про создание игры в стиле Animal Crossing на golang с помощью + raylib — + https://www.youtube.com/watch?v=iWp-mCIQgMU&list=PLVotA8ycjnCsy30WQCwVU5RrZkt4lLgY5&index=1 +- Самописный распределенный типа Postgres + https://notes.eatonphil.com/distributed-postgres.html. Под капотом raft от + hashicorp, boltdb и самое интересное — парсинг SQL +- Рассчет расстояния между двумя Geo точками: + +```go +import "math" +... +// https://en.wikipedia.org/wiki/Haversine_formula +func GetDistance(lat1, lon1, lat2, lon2 float64) float64 { + lat1 *= math.Pi / 180 + lon1 *= math.Pi / 180 + lat2 *= math.Pi / 180 + lon2 *= math.Pi / 180 + return 12742 * math.Asin( + math.Sqrt( + math.Pow(math.Sin((lat2-lat1)/2), 2) + + math.Cos(lat1) * + math.Cos(lat2) * + math.Pow(math.Sin((lon2-lon1)/2), 2) + ) + ) +} +``` + +- [god](https://github.com/pioz/god) — Утилита подгатавливающая демоны из go + программы. Для меня ценное — что генерит systemd конфиги.
\ No newline at end of file diff --git a/content/posts/2023-01-12-gitrepo.md b/content/posts/2023-01-12-gitrepo.md new file mode 100644 index 0000000..7e521c3 --- /dev/null +++ b/content/posts/2023-01-12-gitrepo.md @@ -0,0 +1,167 @@ +--- +categories: +- Мои проекты +date: '2023-01-12T20:22:00Z' +tags: +- it +- моё +title: GitRepo.ru +--- + +Сегодня серьезно переделал свой хостинг [репозиториев +кода](https://gitrepo.ru/): + +- Переехал на большой арендованный сервак +- Привел в порядок оркестрацию вокруг сервака с использованием Docker Compose +- Gitea заменил на её форк [Forgejo](https://forgejo.org/) +- Впилил CI/CD на основе [Woodpecker CI](https://woodpecker-ci.org/) + +Приглашаю пользоваться заместо бездуховного западного github: +<https://gitrepo.ru/> + +Сервер физически находится в датацентре в Москве у весьма годного провайдера +Selectel. + +Тем более, время сейчас неспокойное и неизвестно когда github станет недоступен +для РФ, а GitRepo — он вот тут, в нашей стране. + +# Немного про устройство + +Расскажу немного как я организовал себе Ops сервиса. + +У меня на руках `docker-compose.yml` который полностью описывает всю +конфигурацию сервака, примерно так: + +```yml +version: "3" +services: + caddy: + image: caddy:2.6.2-alpine + container_name: gateway + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + - caddy_data:/data + - caddy_config:/config + networks: + - gateway + git: + image: codeberg.org/forgejo/forgejo:1.18.0-1 + container_name: git + environment: + - USER_UID=1000 + - USER_GID=1000 + - TZ=Europe/Moscow + - USER=git + - GITEA__database__DB_TYPE=postgres + - GITEA__database__HOST=db:5432 + - GITEA__database__NAME=${PG_NAME} + - GITEA__database__USER=${PG_USER} + - GITEA__database__PASSWD=${PG_PASS} + restart: always + networks: + - gitea + - gateway + volumes: + - /home/git/.ssh/:/data/git/.ssh + - forgejo:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "127.0.0.1:2222:22" + depends_on: + db: + condition: service_healthy + db: + image: postgres:13 + restart: always + environment: + - POSTGRES_USER=${PG_USER} + - POSTGRES_PASSWORD=${PG_PASS} + - POSTGRES_DB=${PG_NAME} + healthcheck: + test: /usr/bin/pg_isready + interval: 5s + timeout: 10s + retries: 120 + networks: + - gitea + volumes: + - postgres:/var/lib/postgresql/data + woodpecker-server: + image: woodpeckerci/woodpecker-server:latest + volumes: + - woodpecker-server-data:/var/lib/woodpecker/ + environment: + - WOODPECKER_OPEN=true + - WOODPECKER_GITEA=true + - WOODPECKER_GITEA_URL=https://gitrepo.ru + - WOODPECKER_GITEA_CLIENT=${GITEA_CLIENT} + - WOODPECKER_GITEA_SECRET=${GITEA_SECRET} + - WOODPECKER_HOST=https://ci.gitrepo.ru + - WOODPECKER_ADMIN=neonxp + - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET} + networks: + - gitea + - gateway + depends_on: + - git + woodpecker-agent: + image: woodpeckerci/woodpecker-agent:latest + command: agent + restart: always + depends_on: + - woodpecker-server + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + - WOODPECKER_SERVER=woodpecker-server:9000 + - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET} + networks: + - gitea +volumes: + woodpecker-server-data: + caddy_data: + caddy_config: + forgejo: + postgres: +networks: + gateway: + gitea: + external: false + +``` + +а рядом лежит `.env` файлик с значениями переменных `${...}`. + +Запускаю деплой я с локального компьютера, предварительно добавив удаленный +сервер в [контекст +докера](https://docs.docker.com/engine/context/working-with-contexts/): + +``` +# Создаю новый контекст для удаленного сервера +docker context create gitrepo --docker "host=ssh://gitrepo.ru" +# Все последующие docker команды выполняются на удаленном сервере +docker use gitrepo +# Возвращаюсь в локальный контекст +docker use default +``` + +# Оставшиеся проблемы + +Сейчас так получается, что Caddyfile должен лежать на удаленном сервере, т.к. +часть конфига + +```yml + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile +``` + +выполняется в контексте именно удаленного сервера, а значит при его апдейте на +локальном серваке приходится делать SCP этого файла на сервак. Такое себе. + +Как это решить — есть интересная идея, но это уже в другой раз.
\ No newline at end of file diff --git a/content/posts/2023-05-26-gist.md b/content/posts/2023-05-26-gist.md new file mode 100644 index 0000000..25b362f --- /dev/null +++ b/content/posts/2023-05-26-gist.md @@ -0,0 +1,19 @@ +--- +categories: +- Мои проекты +date: '2023-05-26T17:40:21Z' +tags: +- it +- моё +title: Импортозамещение Gist +--- + + +И в догонку к комментариям, запустил на своём серваке свой аналог Gist’ов от +GitHub. + +Вот и он: [gist.neonxp.ru](https://gist.neonxp.ru/) + +Пользуйтесь 🙂 + +[Другие мои проекты](/projects)
\ No newline at end of file diff --git a/content/posts/2023-07-24-tls.md b/content/posts/2023-07-24-tls.md new file mode 100644 index 0000000..43ee345 --- /dev/null +++ b/content/posts/2023-07-24-tls.md @@ -0,0 +1,85 @@ +--- +categories: +- Без рубрики +date: '2023-07-24T20:04:17Z' +tags: +- it +- Россия +- TLS +title: Немного мыслей о TLS (HTTPS) в России +--- + +Накопилось немного мыслей относительно того, что может грозить нам (и мне) в +связи с трендом на “балканизацию” рунета. + +И самое болезненное место — HTTPS который нынче стандарт де-факто в современных +интернетах. А болезненное оно потому, что целиком и полностью контролируется +другой стороной нынешного противостояния. Все доверенные удостоверяющие центры +принадлежат странам “коллективного запада”. Помню, были ещё какие-то китайские, +вроде, но с ними был какой-то скандал и не факт что они есть. + +Есть относительно [доверенный УЦ от Минцифры](https://www.gosuslugi.ru/tls). Это +здорово и я это всецело поддерживаю. Вот только есть момент. Он не для нас, +простых людей, и при попытке его получить видим то, что на скриншоте ниже. А +сранный Firefox вообще хочет его внести в черный список, чтобы даже специально +его нельзя было установить. В общем, пока его я поставить не могу даже при всём +желании. + +Какие ещё альтернативы есть, если нас вдруг прокинет Let’s encrypt? + +1. Не использовать HTTPS вообще. Я же не магазин и у меня нет форм логина, + которые требуют шифрования. Так-то оно так, да не так. Браузеры уже сейчас + очень косо смотрят на “обычные”, не HTTPS сайты, а в дальнейшем, не удивлюсь + если перестанут открывать вообще. Так же на HTTP сайтах не работают + прикольные браузерные API типа геолокации (наверное, это в каком-то роде даже + плюс 😉 ). Ну и ещё проблема, что, например, этот сайт без HTTPS вообще не + может работать, ибо для доменов зоны .dev насильно включено HSTS и они не + могут работать не по HTTPS. Последнее то я решу старым добрым доменом + neonxp.ru, но тем не менее. +2. Самоподписанные сертификаты. Вот это уже более менее похоже на правду! Да, + такие сайты надо добавлять в исключения и мороки с сертификатами чуть больше. + Но тут та же история с доменами .dev. Для них самоподписаные не катят. Выход + — опять таки старый добрый neonxp.ru. + +К чему я всё это? А то что в случае “балканизации” мы остаемся без нормального +валидного HTTPS. Для себя я выбрал второй путь, с самоподписанными +сертификатами. Чекнуть как работает можно на зеркале блога на +<https://neonxp.ru> . Там я выпустил сам себе сертификат на домен от своего +собственного удостоверяющего центра 🙂 А доверять ему или не доверять — дело +посетителей сайта. + +Если доверяете мне то [вот сертификат моего УЦ](/files/root_ca.crt), а установка +такая же как сертификата Минцифры 🙂 + +Ну и совсем краткая инструкция как выпустить сертификат для себя: + +1. `openssl genrsa -out root_ca.key 4096` — создание секретного ключа УЦ (должен + храниться в безопасности!) +2. `openssl req -x509 -new -key root_ca.key -days 3650 -out root_ca.crt` — + создаем сам сертификат УЦ (он НЕ секретный). Я указал срок действия 10 лет, + но это потому что я ленивый и не хочу его перегенеривать каждый год. Так + делать не советую. +3. `openssl genrsa -out server.key 4096` — создаем секретный ключ уже для + конкретного сайта (и поддоменов) +4. `openssl req -new -key server.key -subj "/CN=neonxp.ru/CN=*.neonxp.ru" -out + server.csr` — генерируем файл запроса для конкретного сайта +5. Создаем файл `openssl.cnf` с примерно таким содержимым: + ``` + [SAN] + subjectAltName = @alt_names + [alt_names] + DNS.1 = neonxp.ru + DNS.2 = *.neonxp.ru + ``` +6. И, наконец, создаем сертификат для сайта, который будет подписан ключами + server.key и root\_ca.key (то есть и своим удостоверяющим центром тоже): + ``` + openssl x509 -req -in server.csr -CA root_ca.crt -CAkey root_ca.key -CAcreateserial -out server.crt -days 365 -extensions SAN -extfile openssl.cnf + ``` + +В общем, всё. Полученные root_ca.crt (но не root_ca.key!), server.key и +server.crt можно вносить в конфигурацию используемого вебсервера. А так же +внести root_ca.crt в доверенные для себя. + +Так у меня выглядят [сертификат на сайт](/img/posts/20230724_204209.webp) и +[сертификат УЦ](/img/posts/20230724_204325.webp).
\ No newline at end of file diff --git a/content/posts/2023-12-29-переезд.md b/content/posts/2023-12-29-переезд.md new file mode 100644 index 0000000..c4a17bf --- /dev/null +++ b/content/posts/2023-12-29-переезд.md @@ -0,0 +1,28 @@ +--- +categories: +- Без рубрики +date: '2023-12-29T00:15:44Z' +tags: +- блог +- разное +title: Переезд и проблемы обновления +--- + +Немного новостей. + +Начну с грустного. Крайне неудачно обновил forgejo на gitrepo.ru. В общем, БД +побилась без возможности восстановления. Репозитории я спас, обращайтесь —пришлю +архив репозиториев. + +Очень грустно, я был крайне расстроен. Штош, теперь настроил зато постоянные +бекапы БД и данных на локальный NAS. Прошу прощения у пользователей, я очень +виноват. + +А теперь о негрустном. Всё же решил что мне больше нравится основным домен не +.dev, а именно .ru. Времена неспокойные — лучше перестраховаться и сделать +ставку именно на национальный домен, а не на международный. К тому же у .dev +домена есть неприятная особенность, что он требует обязательно валидного (то +есть одобренного западными “партнерами”) сертификата. А это не дело, как я уже +[писал в заметке](https://neonxp.ru/posts/2023-07-24-tls/). + +Пока что как-то так 🤷🏻♂️
\ No newline at end of file diff --git a/content/posts/2024-01-03-архив.md b/content/posts/2024-01-03-архив.md new file mode 100644 index 0000000..65a5be8 --- /dev/null +++ b/content/posts/2024-01-03-архив.md @@ -0,0 +1,14 @@ +--- +categories: +- Без рубрики +date: '2024-01-03T17:28:40Z' +tags: +- блог +title: Архив +--- + +Покопавшись по вебархиву смог вытащить древние посты с разных моих старых +блогов. В основном, кринжовые, конечно, но это моя жизнь, как она была в то +время. Так что пусть будут. + +[Архив блога](https://neonxp.ru/archive/)
\ No newline at end of file diff --git a/content/posts/2024-02-21-tls.md b/content/posts/2024-02-21-tls.md new file mode 100644 index 0000000..65f8dfa --- /dev/null +++ b/content/posts/2024-02-21-tls.md @@ -0,0 +1,54 @@ +--- +categories: +- Без рубрики +date: '2024-02-21T21:51:29Z' +tags: +- it +- Россия +- TLS +title: Конфигурация HTTPS с сертификатом от Минцифры +--- + +Третьего дня потратил достаточно много времени на установку на данном сайте +сертификата от Минцифры.А поскольку сертификат краткоживущий (90 дней) — заметка +мне самому пригодится на будущее. + +Началось всё с того, что я с удивлением обнаружил, что на госуслугах теперь +можно выпустить сертификат для домена физлицу.Это меня обрадовало, хотя ранее я +приунывал что нет никакой альтернативы простым смертным. Теперь есть. +<del>Закрывайте буржуйнет.</del> + +Поехали! + +1. Идём сюда: https://www.gosuslugi.ru/627603/1/form +2. По приведенной инструкции генерируем файл запроса сертификата. Вкратце так (только вместо neonxp.ru указываем свой домен): + ``` + openssl req -out neonxp.ru.csr -new -subj "/C=RU/CN=neonxp.ru" -addext "keyUsage = digitalSignature, keyEncipherment" -addext "subjectAltName=DNS: neonxp.ru" -addext "extendedKeyUsage = serverAuth" -newkey rsa:2048 -nodes -keyout neonxp.ru.key + ``` + Важно! Нужно сохранить файл ключа neonxp.ru.key в надежном месте. Если он попадет в чужие руки — нужно будет отзывать сертификат и начинать всё заново! SAN и Wildcard пока не поддерживается, но что имеем — то и имеем. Но по слухам таки будут, как минимум SAN. +3. Полученный файл csr загружаем там же на госуслуги +4. Ждём не долго (реально недолго, у меня прислали сертификат буквально через несколько минут!) +5. В ответ придёт файл с рандомным названием. Сохраняем его туда, где лежат другие файлы под названием “домен.crt” +6. Скачиваем корневой и промежуточные сертификаты: + ``` + wget https://gu-st.ru/content/Other/doc/russian_trusted_root_ca.cer + wget https://gu-st.ru/content/Other/doc/russian_trusted_sub_ca.cer + ``` +7. Преобразуем скачанный сертификат в формат PEM: + ``` + openssl x509 -in neonxp.ru.crt -out neonxp.cer -outform PEM + ``` +8. Соединяем свой сертификат и минцифровские в один бандл: + ``` + cat neonxp.cer russian_trusted_sub_ca_pem.cer russian_trusted_root_ca_pem.cer > chain.cer + ``` +9. Используем полученный бандл и сгенерированный в пункте 2 файл ключа в конфигурации вебсервера. У меня используется Caddy, поэтому мой конфиг выглядит так: + ``` + neonxp.ru:443 { + tls /data/ssl/chain.cer /data/ssl/neonxp.ru.key + ... + } + ``` + +В общем-то, всё. Как настанет время продлевать — я дополню заметку деталями +именно продления. Если будут вопросы — пишите, попробуем решить. diff --git a/content/posts/2024-06-01-вам-не-нужны-пуши.md b/content/posts/2024-06-01-вам-не-нужны-пуши.md new file mode 100644 index 0000000..4d5960f --- /dev/null +++ b/content/posts/2024-06-01-вам-не-нужны-пуши.md @@ -0,0 +1,110 @@ +--- +categories: +- Без рубрики +date: '2024-06-01T21:05:55Z' +tags: +- it +- разное +title: Вам не нужны пуши! +--- + +Я не шучу. Серьёзно. + +С неделю назад меня осенила крайне простая мысль, которая ранее, почему-то, мне +не приходила. + +<!--more--> + +Но сначала, две вводных, или, скажем, тезиса, которые послужили для вывода этой +мысли: + +## Тезис №1 + +Меня действительно огорчает количество пушей которые постоянно сыплются мне на +телефон. Это вызывает раздражение сразуна нескольких уровнях: + +1. Сам момент их прихода — я автоматически смотрю на телефон, что же пришло +2. Если я игнорирую пуш — он потом висит в шторке вызывая раздражение +3. Очень часто это сранная реклама от какого-нибудь озона или магнит маркета + (бывш. KazanExpress) + +Но бывают же и полезные пуши! Например, уведомления от Госуслуг или информация +что заказ доставлен ну илисообщения в мессенджерах. + +То есть, как будто, ради вышеуказанных полезных пушей, я должен терпеть и тонну +бесполезного говна! + +## Тезис №2 + +Ну и вторая вводная, которая, какмне кажется подтолкнула меня — я всегда любил +_простые_ и открытые технологии, какдревние, типа RSS, e-mail, irc, так и новые, +но такие же простые и открытые, как, например, gemini (да, сейчас он наэтом +сайте сломан, но я его починю на днях, честно!), федиверс и прочие подобные. +Кстати, сейчас подумалось, что именноэти качества меня и так сильно влюбили в +golang 🙂 + +## Та самая простая мысль + +На стыке двух вышеуказанных тезисов у меня внезапно для себя самого и +синтезировалась крайне простая мысль: + +> **<u>Действительно</u>** важные вещи всегда приходят на электропочту, а +> сообщения в мессенджерах — это не срочно! + +Таким образом, запретив на телефоне вообще все пуши кроме электропочты я +избавился от этого угнетающего информационногошума, оставив только полезный +сигнал. + +-Хей, да на почте же один спам! — скажешь ты мне + +На самом деле, уже давно нет. Я лично использую почту mail.ru (в данном случае, +это не очень важно и относится к любой)и на ней спама как такового уже давно нет +(если думаешь, что это не так, перепроверь, возможно, твои +представленияустарели). При этом, самое великое в этом то, что почта (на самом +деле, не важно, какая именно — mail.ru, yandex или,прости господи, гмейл) +предоставляет гибкие фильтры входящей почты. И потратив буквально пару десятков +минут можносформировать правила, чтобы, например, от того же озона пропускались +только письма со статусом заказа и больше ничего. + +Вот так, древняя технология обычных, старых-добрых, писем позволяет решить +проблему современных назойливых уведомлений! + +При этом, почта не пушит проверять её постоянно! Самое главное её преимущество +для меня — это её ассинхронность, вотличие от мессенджеров. Можно отключить от +нее уведомления тоже, но завести себе правило, что раз в Н времени выделятьвремя +на ее проверку. Самое главное — делать это в _комфортное для себя_ время. + +## Так же как и на мессенджеры, кстати! + +Выше я уже сказал, что мессенджеры — это не срочно. Ничего страшного не +случится, если я отвечу через час-два-три иливообще вечером. Если будет что-то +_действительно_ срочное — мне можно и позвонить. Но, к счастью, мне повезло, что +мояжизнь достаточно спокойная и _действительно_ срочное почти не случается. +Отрефлексируй, уважаемый читатель, насколько*действительно* срочные и важные +вопросы, которые ты таковыми считаешь и которыми ежедневно дёргают тебя? И +ответь себечестно, мир бы разрушился, если бы ты их отложил на комфортное для +_себя_, а не других время? + +Такой эксперимент я ставлю на себе уже неделю. Я практически не захожу в +мессенджеры, всё действительно важное мнеприходит на почту, лишней рекламы я не +вижу, нет никакого информационного шума, который буквально стал +бичомсовременности. + +## Вывод за неделю + +Моё внутреннее состояние ощущается как очень спокойное и, главное, комфортное. Я +чувствую полный контроль над тем, чтои когда я потребляю и нет никакого +информационного насилия, как его называет +[Столяров](http://stolyarov.info/)(хоть мне этот персонаж и кажется чрезвычайно +радикальным и оттого отталкивающим, но что-то в его словах таки есть). + +Считаю, что эксперимент оказался удачным, и я его продолжу! + +## Пишите письма! + +Напомню раз пришлось к слову, пожалуй, свою электропочту: <i@neonxp.ru> или +<a.kiryukhin@mail.ru> обе почты абсолютноравноценны, писать можно на любую. +Очевидно из поста, что молниеносный ответ я не гарантирую, но, сам факт ответа +вобозримое время гарантирован! + +73! diff --git a/content/posts/2024-06-02-книги-1.md b/content/posts/2024-06-02-книги-1.md new file mode 100644 index 0000000..0173245 --- /dev/null +++ b/content/posts/2024-06-02-книги-1.md @@ -0,0 +1,21 @@ +--- +categories: +- Без рубрики +date: '2024-06-02T01:48:16Z' +tags: +- книги +- фантастика +title: Книжные рекомендации 1 +--- + +Подумалось, почему бы не рекомендовать понравившиеся мне книги. + +В прошлый раз, ещё [в VK рекомендовал](https://vk.com/wall-174034751_45) +Азимовский цикл “Основание”. А в этот раз рекомендую цикл фантастики Андре +Нортона “Королева Солнца”. + +Если без спойлеров — цикл описывает приключения помощника супер-карго Дейла на +космическом корабле вольных торговцев «Королева Солнца». Читается легко и +увлекательно. Книги небольшие, проглатываются за пару часов. + +[Скачать](andre_norton-queen_of_sun.zip)
\ No newline at end of file diff --git a/content/posts/2024-07-13-joplin.md b/content/posts/2024-07-13-joplin.md new file mode 100644 index 0000000..f23bcc7 --- /dev/null +++ b/content/posts/2024-07-13-joplin.md @@ -0,0 +1,43 @@ +--- +categories: +- Без рубрики +date: '2024-07-13T20:49:12Z' +image: files/2024-07-13-joplin_joplin.webp +tags: +- it +- joplin +title: Заметочник Joplin +--- + +Просто хочу поделиться отличным приложением для заметок, вместо популярного +Notion и менее популярного Obsidian. + +Название на для русского уха звучит по дурацки — +[Joplin](https://joplinapp.org/). Но, не смотря на такое название, +самоприложение очень даже серьёзное. + +В общем и целом, это достаточно продвинутый опенсорсный заметочник. В качестве +формата текста он использует Markdown[^1]. +[^1]:https://skillbox.ru/media/code/yazyk-razmetki-markdown-shpargalka-po-sintaksisu-s-primerami/ + +Так же, из приятностей — большое количество плагинов +(<https://github.com/topics/joplin-plugin>) и возможность использовать свой +сервер для синхронизации +https://docs.vultr.com/how-to-host-a-joplin-server-with-docker-on-ubuntu . +Для себя я, конечно же, поставил на свой сервак. Ну, а более бюджетно, если нет +своего сервера — можно использовать любой WebDav сервер. В частности, [Облако +Mail.Ru](https://help.mail.ru/cloud_web/app/webdav/) или Яндекс Диск (адрес +<https://webdav.yandex.ru>, необходимо использовать [пароль +приложения](https://yandex.ru/support/id/authorization/app-passwords.html)). + +Но почему же стоит поднять свой сервер? Ну хотя бы для того, чтобы иметь +возможность спокойно публиковать заметки, например, вот так: +<https://notes.neonxp.ru/shares/UKB6Rkgt2yA2q1yrwpvb8F>. + +Или возможность совместной работы, например, со своей парой над общим списком +покупок. + +~~P.S. Если нужен аккаунт на моем сервере синхронизации Joplin — пишите на +почту, самостоятельной регистрации на сервере синхронизации не предусмотренно. +Вот только не забудьте при синхронизации включить в настройках шифрование +заметок. Я не хочу потом получать подозрения в нарушение приватности.~~
\ No newline at end of file diff --git a/content/posts/2024-07-21-bbs.md b/content/posts/2024-07-21-bbs.md new file mode 100644 index 0000000..c304c84 --- /dev/null +++ b/content/posts/2024-07-21-bbs.md @@ -0,0 +1,33 @@ +--- +categories: +- Без рубрики +date: '2024-07-21T20:28:34Z' +tags: +- разное +title: Преемственность от BBS до Телеграма +--- + +Чисто на правах воскресной шизы. + +Обнаружил для себя интересную тенденцию, в характерных своему времени +инструментах для общения за последние 40+ лет: + +Если взять эволюционный ряд BBS (в т.ч. Фидо) → Форумы → Соцсети → Мессенджеры, +то можно выделить в них несколько общихчерт: + +- Возможность общения 1—1 +- Возможность общения 1—М (оператор BBS, администратор форума может сделать + какую-то тему в read-only и сам туда писать,получая что-то типа каналов в + телеге или блога) +- Возможность общения М—М (обычный режим форума или многопользовательский чат в + мессенджерах) +- Возможность обмена файлами (в т.ч. картинками, не зависимо от того, сразу они + отображаются у собеседника или нет) +- Возможность проводить голосования (внезапно, да?) + +Что из этого следует? Да ничего, просто забавно. Интересно, что будет в +постмессенджеровую эпоху? По идее, какой быинструмент ни был — эти же черты +будут присущи и ему. + +P.S. Да, я тут не упомянул про мейллисты и условные IRC, но просто не знал куда +и после чего их приткнуть. Но по факту,черты все те же самые.
\ No newline at end of file diff --git a/content/posts/2024-09-26-hugo-wordpress.md b/content/posts/2024-09-26-hugo-wordpress.md new file mode 100644 index 0000000..aa862ef --- /dev/null +++ b/content/posts/2024-09-26-hugo-wordpress.md @@ -0,0 +1,16 @@ +--- +categories: +- Без рубрики +date: '2024-09-26T19:05:00Z' +tags: +- блог +title: Hugo → WordPress +--- + +Поменял в блоге движок с модного Hugo на немодный бумерский WordPress. Почему? +Да просто он удобнее. + +Серьёзно, неужели этот гиковский пердолинг с сборкой блога через Git CI удобнее +чем просто написать пост в браузере? Ну если не врать себе, то конечно же нет. + +Так что да, с возрастом начинаешь ценить просто удобные, а не новомодные вещи.
\ No newline at end of file diff --git a/content/posts/2024-10-06-цитатник-рунета.md b/content/posts/2024-10-06-цитатник-рунета.md new file mode 100644 index 0000000..cc9ba60 --- /dev/null +++ b/content/posts/2024-10-06-цитатник-рунета.md @@ -0,0 +1,64 @@ +--- +categories: +- Мои проекты +date: '2024-10-06T12:00:11Z' +image: files/2024-10-06-цитатник-рунета_bash_org.webp +location: Казань +tags: +- go +- it +- моё +title: Цитатник Рунета +--- + +В середине-конце нулевых был очень популярный сайт баш.орг.ру. Думаю, те, «кому +за» помнят ещё такой. + +Сайт просто был сборником цитат из разных чатов, irc каналов или личных +переписок. Изначально, был исключительно анимешно-айтишной направленности и тем +самым для нас, студентов и гиков был крайне популярным местом. В своё время, он +подарил мне много часов приятного времяпрепровождения и ламповых вечеров. + +Затем, когда БОР (как часто его сокращали) выиграл премию Рунета, на него хлынул +поток, как сейчас бы сказали, «нормисов». Которые, уже в свою очередь, заполнили +БОР всяким про отношения, офисно-планктонные темы, фейковыми цитатами, ответами +на цитаты, ответами на ответы на цитаты и прочим подобным, далёким от +изначального айтишного флёра, шлаком. + +В общем, как всегда, в андеграунд пришли нормисы и всё испортили. И да, баш +скатился уже, по сути, к десятым годам. + +Примерно тогда же он для меня и закончился, ибо стал уже совсем не «торт». Потом +он как-то жил больше декады за границами моего внимания. Успев при этом поменять +адрес с зоны .ru на зону .im зачем-то. Ну а с началом СВО его админы +окончательно сошли с ума и закрыли БОР который к тому времени и так едва ли был +жив. На этом, его история окончательно закончилась. + +Однако, не смотря на это БОР был интересным и знаковым феноменом, который +неотрывно вписан как в историю рунета так и в мою личную историю юности. + +Посему, я решил, так сказать, или возродить его, ну или, как минимум, сделать +ему мемориал. + +Сказано — сделано. Купил домен, который отсылается к самому старому домену +оригинала — [sh.org.ru](https://sh.org.ru) (sh является командной оболочкой, +предком командной оболочки bash). За несколько часов написал скраппер по +зеркалам и архивам бора, спарсил более 80К цитат. Затем, написал на golang +простенький движок и всего за день запустил свой цитатник в свободное плавание! + +Из функций пока только вывод цитат по страницам, а так же вывод случайных 20 +цитат + кнопка для выдачи других 20 случайных. Лично мне гораздо больше нравятся +как раз случайные подборки. Их можно обновлять почти бесконечно! + +Да, он пока не умеет принимать новые цитаты (да и кто их будет слать то, лол?), +да и нет других функций, типа голосований (классическими `[+]`, `[-]`, +`[:|||:]`). Буду ли я это доделывать и как-то развивать? Не знаю. Возможно, +время цитатника безвозвратно ушло. Но может быть и внезапный комбек. Кто знает +🤷♂️. В ближайшие дни я допилю и голосвалку и добавление цитат, но вряд ли буду +в это инвестировать много времени. Есть ещё и мысль публиковать цитаты через ТГ +бота простой пересылкой ему сообщений, а он уже их сам анонимизирует заменяя +данные пользователей на обезличенные XXX и YYY и оформляет цитату как надо. Как +вам такая идея? + +Вообще, я бы хотел это как-то, наверное, обсудить, относительно того как это +развивать и стоит ли?
\ No newline at end of file diff --git a/content/posts/2024-10-17-книги-2.md b/content/posts/2024-10-17-книги-2.md new file mode 100644 index 0000000..96cafc7 --- /dev/null +++ b/content/posts/2024-10-17-книги-2.md @@ -0,0 +1,39 @@ +--- +categories: +- Без рубрики +date: '2024-10-17T19:26:00Z' +image: files/2024-10-17-книги-2_Rama16wiki.webp +location: Казань +tags: +- книги +title: Книжные рекомендации №2 +--- + +Продолжу, пожалуй. + +Сегодня хочу порекомендовать всего две книги: + +## Свидание с Рамой + +Артур Кларк, 1973 + +Фантастическая повесть о встрече человечества с необитаемым(?) инопланетным +кораблём, который прилетел в нашу солнечную систему. На изображении выше — вид +этого корабля изнутри. + +- На сайте lib.ru: + [www.lib.ru/KLARK/rama1.txt](http://www.lib.ru/KLARK/rama1.txt) +- В виде аудиокниги: <https://akniga.org/klark-artur-svidanie-s-ramoy> +- Если надо — могу выложить по запросу в формате fb2 + +## Глубина в небе + +Вернор Виндж, 1999 + +Об экспедиции двух разных человеческих колоний к странной звезде, имеющей +свойство выключаться на 200 лет. Причиной отправки стали принятые с окрестной +планеты радио сигналы, свидетельствующие о наличии разумной жизни на ней. + +Книга является частью цикла, и я прикладываю цикл целиком: + +[Цикл «КенгХо» скачать](КенгХо.zip)
\ No newline at end of file diff --git a/content/posts/2024-11-15-hugo.md b/content/posts/2024-11-15-hugo.md new file mode 100644 index 0000000..7a677a4 --- /dev/null +++ b/content/posts/2024-11-15-hugo.md @@ -0,0 +1,19 @@ +--- +categories: +- Без рубрики +date: '2024-11-15T01:11:49+03:00' +location: Казань +tags: +- разное +title: Hugo +--- + +Так, ну я вернулся на hugo :D + +Основная причина — я нашел решение основной моей проблемы с Hugo, а именно, +удобной публикации. + +А как именно решил — тема отдельного поста на потом. + +Ну и тему наконец-то сделал сам с нуля. Как говорится, хочешь сделать хорошо — +сделай это сам.
\ No newline at end of file diff --git a/content/posts/2024-11-17-obsidian.md b/content/posts/2024-11-17-obsidian.md new file mode 100644 index 0000000..9ecf5f4 --- /dev/null +++ b/content/posts/2024-11-17-obsidian.md @@ -0,0 +1,240 @@ +--- +categories: +- Без рубрики +date: '2024-11-17T22:30:37+03:00' +description: '' +image: files/2024-11-17-obsidian_img/logo.webp +location: Казань +tags: +- it +- joplin +- obsidian +title: Obsidian +--- + +Некоторое время назад я [писал](/posts/2024-07-13-joplin/) про заметочник +Joplin. + +С тех пор мои вкусы несколько поменялись и я открыл для себя его величество +[Obsidian](https://obsidian.md/). + +В целом он такой же заметочник, с ± тем же функционалом, но имеет для меня одну +особенность, которая буквально переворачивает всё. Это мощнейшая система +плагинов. Серьёзно, я нашел плагины которые покрывают для меня всё, кроме одного +(но об этом позже). + +## Что такое Obisidian? + +Obsidian представляет собой приложение для ведения персональных баз данных, +основанное на принципах локальных файлов Markdown. Это значит, что ваши данные +хранятся в виде обычных текстовых файлов, что обеспечивает максимальную гибкость +и независимость от облачных сервисов. + +Приложение работает на операционных системах: Windows, macOS, Linux, iOS, +Android. + +<!--more--> + +## Основные функции и преимущества + +1. **Граф связей** — да, он есть уже много где, но нельзя его не упомянуть. +2. **Markdown** — очень приятно, что все заметки хранятся в Markdown, что + обеспечивает максимальную интероперабельность и переносимость +3. **Плагины** — плагины пишутся на JS/TS и их много. Даже не так, их **МНОГО**. + Что приятно, они скачиваются и лежат в той же директории что и основное + хранилище, а это важно для следующего пункта +4. **Синхронизация** — она есть. Но вроде как платная. Но мне это и не + интересно, я использую Syncthing. Просто шарю через него директорию + хранилища по схеме оба ноутбука <-> NAS <-> Android. При этом синкаются все + плагины и настройки. +5. **Скорость** — не смотря на то, что он написан на проклятом электроне, + работает достаточно шустро, претензий нет. +6. **Доска для рисования** — мелочь, конечно, но удобно, когда надо на скорую + руку накидать небольшую схемку. В конце этого поста как раз есть пример + такой схемки. + +## Минусы + +1. **Проприетарность** — Obsidian хоть и в целом бесплатный, но он не свободный + и даже не opensource. Да, это серьёзный минус, но он компенсируется тем, что + хотябы вся база данных не в проприетарном формате. И в случае чего можно + будет с наименьшими проблемами свалить куда-нибудь. +2. **Electron** — ну это скорей мой личный пунктик. Но при этом приходиться + смиряться с электроном что на Obsidian что на VSCode (VSCodium, конечно же), + потому что лучше-то и нет. + +Это только то, что сейчас пришло в голову. + +## А теперь самое вкусное + +Не помню, я упоминал что у него много плагинов? :) + +Так вот, поехали, мои самые любимые: + +### Dataview + +https://blacksmithgu.github.io/obsidian-dataview/ + +Ну это просто must-have плагин, который позволяет обращаться с вашими заметками +именно как с базой данных, не меньше. + +Например, можно создать новый документ, написать в него + +``` + ```dataview + TASK + WHERE status = " " + ``` +``` + +и волшебным образом вместо этого блока появятся все невыполненные задачи, а вот +так + +``` + ```dataview + TASK + WHERE status = "x" + ``` +``` + +мы получим все выполненные. + +Язык запросов очень мощный[^1], в нём сто́ит разобраться. +[^1]:https://blacksmithgu.github.io/obsidian-dataview/queries/structure/ + +Ещё есть возможность делать однострочные запросы, например, в домашней заметке +(которая у меня открывается по умолчанию) у меня есть ссылка на именно +сегодняшнюю заметку ежедневного журнала. Сделано вот так: + +``` +`=link(dateformat(date(today), "yyyy.MM.dd"))` +``` + +### Templater + +https://silentvoid13.github.io/Templater/ + +Этот плагин позволяет мне задать некоторым директориям умолчальный шаблон. +Например, вот такой у меня шаблон для ежедневных журналов: + +``` +<%* +try { + // Получаем имя текущей ежедневной заметки + const noteName = tp.file.title; + + // Разбиваем полученное имя на компоненты даты + const [year, month, day] = noteName.split('.').map(Number); + + // Создаём объект Date на основе поученных компонентов + const currentNoteDate = new Date(year, month - 1, day); + + // Вычисляем предыдущий и следующий день + let previousDayDate = new Date(currentNoteDate.setDate(currentNoteDate.getDate() - 1)); + let nextDayDate = new Date(currentNoteDate.setDate(currentNoteDate.getDate() + 2)); + + // Форматируем дату обратно в "DD-MM-YYYY" + const formatDate = (date) => { + const dd = String(date.getDate()).padStart(2, '0'); + const mm = String(date.getMonth() + 1).padStart(2, '0'); + const yyyy = date.getFullYear(); + return `${yyyy}.${mm}.${dd}`; + }; + + const previousDay = formatDate(previousDayDate); + const nextDay = formatDate(nextDayDate); + + // Формируем ссылки + const baseFolder = tp.file.folder(true); + const previousNotePath = `${baseFolder}/${previousDay}.md`; + const nextNotePath = `${baseFolder}/${nextDay}.md`; + + // Выводим даты в виде ссылок + tR += `← [[${previousNotePath}|${previousDay}]] | [[${nextNotePath}|${nextDay}]] →`; +} catch (error) { + console.error("Templater Error:", error); +} +%> + +## Задачи +___ +<% +`- [ ]` +%> + +## Заметки +___ + +``` + +и переходя к сегодняшней заметке я сразу получаю такую заготовку: + + + +### Остальные плагины + +Остальные тоже крутые, но я их приведу просто списком: + +- [tasks](https://publish.obsidian.md/tasks/Introduction) — помогает более + богато управлять задачами. В частности, у меня проставляет дату завершения + задачи, и проставляет даты дедлайна и прочее. +- [reminder](https://uphy.github.io/obsidian-reminder/) — трекает и напоминает + про задачи +- [calendar](https://github.com/liamcain/obsidian-calendar-plugin) — просто + миникалендарь в боковой панели +- [homepage](https://github.com/mirnovov/obsidian-homepage) — позволяет задать + произвольную заметку "домашней" +- [icon-folder](https://github.com/timolins/obsidian-icon-folder) — позволяет + задавать директориям и заметкам произвольные иконки. Пример есть как раз на + скриншоте выше. +- [pomodoro-timer](https://github.com/eatgrass/obsidian-pomodoro-timer) — думаю, + из названия и так понятно +- [kanban](https://publish.obsidian.md/kanban/) — шикарнейший канбан плагин + +## А что же мне не хватает? + +Я упомянул выше что мне кое чего не хватает. А именно, постить заметку в мой +блог по протоколу [Micropub](https://indieweb.org/Micropub). + +Только из-за Obsidian и того, что он использует Markdown я опять [вернулся на +Hugo](/posts/2024-11-15-hugo/), который так же рендерится из Markdown. + +«Но Hugo это же генератор статичных сайтов, куда ты ему будешь отправлять +заметку для публикации?» — можешь спросить меня ты. А я отвечу что у меня вот +такой план: + +```mermaid +graph TB +b1["Заметка в Obsidian"] +b2["Плагин obsidian-micropub"] +b3["micropub сервер на моем сервере"] +b4["вызов hugo"] +b5["Загрузка или копирование результата на веб сервер"] +b1 --> |Publish в контекстном меню| b2 +b2 --> |POST neonxp.ru/micropub| b3 +b3 --> |Запись в директорию content блога| b4 +b4 --> |hugo рендерит markdown -> html| b5 +style b1 fill:#28252e, stroke:#754fcc +style b2 fill:#2e2121, stroke:#c81319 +style b3 fill:#2e2121, stroke:#c81319 +style b4 fill:#222c2c, stroke:#20acaa +style b5 fill:#222c2c, stroke:#20acaa +``` + +То что выделено красным — ещё не существует в природе. + +micropub сервер для hugo я уже начал писать. Да, есть nanopub сервер, но у него +есть два серьёзных недостатка, это PHP и то что его сделал не я. + +micropub плагин для obsidian я вижу сделать на основе существующего плагина +rest-publish. Ну или как пойдёт. + +В общем, меня ждёт ещё очень много весёлого дрочева с этим всем. + +## Закругляюсь + +Пожалуй, пока на этом всё. Поделился как радостью использования Obsidian, так и +планами на пет-проекты, что ещё надо-то? + +Если что, пишите комментарии. Лучше всего здесь, но можно и во всяких +телеграмах-вкшках. diff --git a/content/posts/2024-11-27-hyperlocality.md b/content/posts/2024-11-27-hyperlocality.md new file mode 100644 index 0000000..eb8ff84 --- /dev/null +++ b/content/posts/2024-11-27-hyperlocality.md @@ -0,0 +1,182 @@ +--- +categories: +- гиперлокальность +date: '2024-11-27T17:50:18+03:00' +description: '' +location: Казань +tags: +- разное +- IT +- размышления +- гиперлокальность +title: Гиперлокальность +--- + +Это очередной пост моих пространных рассуждений про тенденции и будущее +интернета, которых в последнее время становится как-то многовато. Вероятно, в +последствии, это станет даже серией постов. + +Этот же я воспринимаю, как вводный в лор гиперлокальности. + +Сначала, пожалуй, расскажу про посылки, а потом уже о том, куда они ведут, и +какие из этого можно сделать выводы. +<!--more--> +## Посылка + +Думаю, все мы заметили как много вокруг стало ИИ инструментов. Сейчас ИИ на +хайпе и его засовывают буквально куда можно и куда нельзя. Само по себе меня это +не беспокоит. Я отношусь к ИИ как к просто очередному инструменту, который можно +и нужно использовать там, где он применим. С этим нет проблем. Пройдёт какое-то +время и ИИ инструменты займут ниши, где они наиболее уместны и где от них +наибольшая польза. Однако тут есть и негативный нюанс. Этот инструмент будет +способствовать в том числе и тому, что интернет станет (если ещё не стал!) по +сути своей «мёртвым». Не мёртвым буквально, а «мёртвым» в том же смысле, в +котором в «Руководстве путешествующего автостопом по галактики» Д. Адамса была +вселенная обозначена необитаемой. + +> Вселенная — кое-какая информация, облегчающая существование в ней. +> +> <...> +> +> 4. Население: Отсутствует. Известно, что существует бесконечное множество +> планет. Это объясняется той простой причиной, что пространство, в котором они +> могут существовать, также бесконечно. Однако не всякая из этих планет +> обитаема. Отсюда следует, что число обитаемых планет конечно. Частное от +> деления любого конечного числа на бесконечность стремится к нулю и не дает +> остатка, следовательно, можно заключить, что средняя численность населения +> планет Вселенной равна нулю. Отсюда следует, что численность населения во всей +> Вселенной также равна нулю, и потому все люди, которые порой попадаются на +> вашем пути, являются продуктом вашего воспаленного воображения. +> +> Д. Адамс — Ресторан «У края Вселенной», 19 глава + +## Следствие + +Количество сгенерированного ИИ контента, ИИ ботов пишущих комментарии и иным +способом имитирующих людей будет расти нелинейно. Таким образом будет +«размываться» весьма конечное количиство «живых» пользователей «неживыми» до +того, что все эти миллиарды «живых» пользователей будут лишь статистической +погрешностью относительно «неживых» ИИ ботов. + +## Как это повлияет на наше восприятие реальности? + +Представьте себе мир, где большинство сообщений, комментариев и публикаций +создаются ИИ. Мы будем жить в мире, где трудно отличить реальность от иллюзии. +Где каждый день нам придётся задаваться вопросом: кто написал этот комментарий – +реальный человек или искусственный интеллект? Это приведёт к тому, что доверие к +информации в интернете начнёт стремительно падать. Люди станут всё больше +сомневаться в подлинности того, что видят и читают. В итоге, интернет +превратится в огромное море данных, где настоящие голоса людей тонут в океане +фальшивок и симуляций. + + + +## Гиперлокальность + +Уже сейчас вполне себе просматиривается контур того, что я, за неимением лучшего +термина, называю «Гиперлокальностью». Термин мне нравится тем что он, с одной +стороны, хорошо описывает то, куда, по моему мнению, мы придём, а с другой +стороны, названием отсылает к «гипертексту». + +### Что я под этим подразумеваю? + +Помните старые времена, когда интернет только-только появлялся и из каждого +утюга звучало как одно из его преимуществ, то, что «вы сможете находить себе +собеседников и друзьей в любой точке мира, не выходя из дома». Звучало +многообещающе, и в каком-то смысле, оно так и было. + + + +Но что происходит сейчас? Интернет, вместо того чтобы соединять людей по всему +миру, начинает дробиться на маленькие замкнутые круги. Почему так происходит? +Ответ кроется в недоверии. Когда невозможно понять, кто перед тобой – настоящий +человек или ИИ-бот, люди начинают замыкаться в узких кругах тех, кому они +доверяют. + +> Интернет, он не сближает. Это скопление одиночества. Мы вроде вместе, но +> каждый один. Иллюзия общения, иллюзия дружбы, иллюзия жизни… +> +> Януш Леон Вишневский — Одиночество в Сети + +Эти круги становятся всё меньше и меньше, пока не превращаются в замкнутые +сообщества, где общение ограничено только теми, кого знаешь лично. Таким +образом, получается некая WebOfTrust, но только по валидации «человечности». Это +напоминает модель «доверительных сетей», которая существовала задолго до +появления интернета, но теперь она приобретает новый смысл в цифровую эпоху. + +А личные знакомства они, как правило, достаточно локальные. А следовательно, в +ближайшее время мы увидим расцвет изолированных «анклавов» из _лично_ знакомых +между собой людей, который и будут существовать своими маленькими, +**гиперлокальными** сообществами. Размер при этом может быть почти любой, как +группка из трёх друзей, так и небольшой клуб из пары десятков _лично знакомых_ +единомышленников. + +Причём, примеры гиперлокальных сообществ уже сейчас есть и в большом количестве. +Например, у меня с друзьями уже почти 10 лет есть свой маленький чатик на шесть +голов. И, в принципе, этого круга общения мне вполне хватает. И в своём кругу +мы, конечно же, уверенны в «человечности» каждого из нас, ибо знакомы и ИРЛ. + +Причём, «достаточность» этого кружка для меня такая, что если у меня, вдруг, +магическим образом, останется только этот чатик, мой NAS в который загруженно +примерно 50К книг и несколько любимых сериалов, и, конечно, VPN до работы, чтобы +я мог зарабатывать на жизнь — то, это и будет вся моя гиперлокальная сеть. И как +будто, не сильно то я и потеряю если останется только это, ну или как минимум, +уж точно выживу. Если что, это именно магически и гипотетически, но тем не +менее. + +Так же, подобные кружки, я видел и, например, у своих старших родственников. +Они, в основном, устраивают гиперлокальные «кружки» в том же вотсаппе. Там они +делятся рекомендациями фильмов, рецептами, шутками, новостями и прочим подобным. + +### А к чему я это всё? + +Да к тому что на текущем этапе развития интернета, мы всё больше уходим от +**глобальной** сети к **гиперлокальной**. И, наверное, мне это даже вполне +нравится. Это как-то... уютно чтоли. + + + +## Перспективы + +Дисклеймер. Дальше идут мои размышления, которые основываются в основном на +интуитивных, а не объективных предположениях + +С развитием этого тренда будут всё больше и больше отмирать крупные социальные +сети типа ВК или РКНбука. История сделает виток и восскресит т.н. локалки, +которые были популярны в 90е-00е. Конечно же, уже в другом облике. Никто не +будет лазать по чердакам чтобы протянуть витуху между соседями, но именно суть +останется. А суть в том, что будет бо́льшая концентрация на небольшом числе +условно локальных ресурсов, где человек будет только со своими друзьями, а +«большой» интернет отходит на второй план. + +Так же могут получить развитие indieweb технологии, а так же self-hosted решения +для общения, например, Matrix. Эти инструменты потребуются как ответ на +заполненные ботами и спамом соцсети и мессенджеры. Конечно же, всё что нужно не +затащить в свою уютненькую локалочку, но вылазка за недостающей инфой в интернет +будет ощущаться, как выход из своей зоны комфорта в дикую и опасную пустошь. + + + +## Окончание? + +Я отдаю себе отчёт что то, что я написал выше — весьма сумбурно. Но это +следствие того, что я ещё не до конца исследовал эту тему, и многие мысли на эту +тему в моей голове пока ещё не сформированы в слова, а остаются на интуитивном +уровне. + +А написал я это, скорее как повод начать дискуссию на эту тему. Мне интересно, +что вы думаете по этой теме. Возможно, мнение со стороны меня наведёт на еще +какие мысли. + +В дальнейшем у меня уже есть некоторые мысли на развитие темы, но уже в каких то +отдельных аспектах. + +Остаёмся на связи, 73! diff --git a/content/posts/2024-11-29-hobbies.md b/content/posts/2024-11-29-hobbies.md new file mode 100644 index 0000000..50fc928 --- /dev/null +++ b/content/posts/2024-11-29-hobbies.md @@ -0,0 +1,44 @@ +--- +categories: +- Мысли вслух +date: '2024-11-29T18:00:36+03:00' +description: '' +image: files/2024-11-29-hobbies_dozor.webp +location: Казань +tags: +- размышления +title: Откуда берутся увлечения? +--- + +На днях задался вопросом вынесенным в заголовок. Причём не столько над +эволюцией, сколько о том, откуда они взялись. + +Раньше я часто играл в ночные полевые игры, такие как «Дозоры» и «Энкаунтеры». +Они были мне очень интересны и играли важную роль в моей жизни. Даже, я бы +сказал, во взрослении, так как пришлись на возраст 19–25 лет. + +<!--more--> + +Сейчас давно уже не играю, но многие мои нынешние увлечения берут начало именно +оттуда. Например, самый очевидный пример — интерес к картографии и ГИСам +(геоинформационным системам), поскольку для «Дозоров» они были неотъемлемой +частью. Также сюда относится и то, что я хорошо ориентируюсь в своем городе и +немного интересуюсь его историей. + +Кроме того, можно проследить интерес к радиоэлектронике: пару раз, когда с +командой организовывали игры, я придумывал задания, основанные на простых +электронных устройствах, собранных на микроконтроллерах. + +Туда же и любовь к исключительно ноутбукам, да и вообще переносной, что важно, +технике. Теплое отношение к простым, надёжным, нетребовательным технологиям. Это +всё оттуда же! + +Любовь к программированию у меня возникла чуть раньше, поэтому её связать с +этими играми я не могу. Здесь, скорее случилось наоборот. И я не менее трех раз +даже порывался писать собственный «движок» для НПИ. Но, пока ни разу не успешно. +Вероятно, меня здесь привлекает процесс, а не результат. Так что, последний мой +заход хоть и является вполне себе функционально законченным, но для его развития +времени я не выкраиваю, к сожалению. + +Что-то еще было, но я не успел записать, и теперь забыл. Вывод: не стоит давать +остывать размышлениям дольше суток, а писать сразу 🙂
\ No newline at end of file diff --git a/content/posts/2024-12-12-guessr.md b/content/posts/2024-12-12-guessr.md new file mode 100644 index 0000000..235901c --- /dev/null +++ b/content/posts/2024-12-12-guessr.md @@ -0,0 +1,130 @@ +--- +categories: +- Мои проекты +date: '2024-12-12T22:27:49+03:00' +description: '' +image: files/2024-12-12-guessr_logo.webp +location: Казань +tags: +- IT +- Проект выходного дня +title: Guessr +--- + +На недавних выходных я запилил очередной «проект выходного дня». На этот раз — +аналог известного сервиса GeoGuessr, но в отличие от него, все точки +сконцентрированы в моей родной Казани. Ну и я не использую панорамы, а +фотографии мест. + +Я обещал выложить исходники, и в общем, вот они: +https://git.neonxp.ru/guessr.git/ + +## Немного про разработку + +Первым встал вопрос, откуда брать данные, а именно фотографии и координаты +точек. Пару лет назад нашу страну покинул такой проект, как Ingress, +представлявший собой гео игру в дополненной реальности. В свою очередь, я +посчитал, что раз проект решил отказаться от нас, как игроков, я посчитал +морально оправданным ~~спиз~~экспропреировать кусочек их данных, а именно +спарсил с их карты intel.ingress.com т.н. «порталы», которые, по сути и есть эти +самые геоточки с фотографиями. + +Дамп я загнал в Postgresql с подключенным расширением +[Postgis](https://postgis.net/). + +Ну а далее написал достаточно простой API на Golang, который реализует следующие +методы: + +- Создание новой игровой сессии, в ответ ставится кука внутри которой + зашифровано текущее состояние — ник, количество очков, ID текущего + угадываемого объекта (в начале пустое). + ```http + POST /api/state + Content-Type: application/json + + { + "username": "NeonXP" + } + ``` + +- Получение состояния. Просто возвращает вышеуказанные параметры + ```http + GET /api/state + ``` + +- Выдача нового объекта для угадывания. При этом возвращается ссылка на фото и + обновляется состояние, тем что в него вписывается ID объекта + ```http + POST /api/next + ``` + +- Угадывание. Собственно, на вход передаются координаты куда на карте указал + игрок. А в ответ возвращается: + - Название объекта + - Расстояние от переданной точки до реального размещения объекта + - Geojson строка в которой зашифрована линия соединяющая точку и объект (нужна + для отрисовки красной линии на карте) + + При этом высчитываются очки которые получает игрок за попытку по формуле + max(1000-d, 0), где d - расстояние между выбранной точкой и объектом в метрах. + То есть, если разница меньше 1000м, то чем ближе - тем больше очков (максимум + 1000 очков за 1 очень точное угадывание). + ```http + POST /api/guess + Content-Type: application/json + + { + "lat": 55.123, + "lon": 49.123 + } + ``` + +Вот в общем-то и всё API! + +Из интересностей, при выборе очередной точки у неё в БД увеличивается счетчик, а +сам select выбирает случайную точку только среди тех точек, где этот счетчик +минимальный. То есть, пока не будут выданы игрокам все точки, уже выбранные +заново не будут выданы. Вот это место в коде: +https://git.neonxp.ru/guessr.git/tree/pkg/service/places.go#n26 (стр. 26-32) + +```go +err = btx.NewSelect(). + ColumnExpr(`p.guid, p.img`). + Model(r). + Where(`p.count = (SELECT MIN(pl.count) FROM places pl WHERE pl.deleted_at IS NULL)`). + OrderExpr(`RANDOM()`). + Limit(1). + Scan(ctx, r) +``` + +Ещё я бы отметил то, что я решил по максимуму логику вынести в БД, и, например, +при угадывании расстояние до точки, а также вышеупомянутый geojson формируются +так же на стороне БД: +https://git.neonxp.ru/guessr.git/tree/pkg/service/places.go#n50 (стр. 50-59) + +```go +err := p.db.NewSelect(). + Model(&model.Place{GUID: guid}). + WherePK("guid"). + ColumnExpr(`p.name, p.guid, p.img, + ST_Distance(ST_MakePoint(?, ?)::geography, p.position::geography)::int AS distance, + ST_AsGeoJSON(ST_MakeLine( + ST_SetSRID(ST_MakePoint(?, ?), 4326), + ST_SetSRID(p.position, 4326) + )) AS geojson`, lon, lat, lon, lat). + Scan(ctx, r) +``` + +## Дальнейшие планы + +В комментах к анонсу ребята накидали достаточно много хороших идей, синтезировав +которые, и добавив свои хотелки я составил примерно такой чеклист: + +- [ ] Авторизация и общая доска лидерства +- [ ] После угадывания спрашивать у игрока «сложность», чтобы потом можно было, + например, настраивать чтобы попадались только простые объекты. И, например, + разное количество очков за простые и сложные объекты +- [ ] Подумать как вынести игру в оффлайн, по типу того же ингресса. Это сложно + и предстоит хорошо это обдумать + +Как-то так :) А впереди новые выходные и новые «проекты выходного дня»!
\ No newline at end of file diff --git a/content/posts/2024-12-15-conditional-operator-go.md b/content/posts/2024-12-15-conditional-operator-go.md new file mode 100644 index 0000000..480ba42 --- /dev/null +++ b/content/posts/2024-12-15-conditional-operator-go.md @@ -0,0 +1,35 @@ +--- +categories: +- Без рубрики +date: '2024-12-15T23:47:08+03:00' +description: '' +image: files/2024-12-15-conditional-operator-go_ternary.webp +location: Казань +tags: +- IT +- Go +title: Тернарник в Go +--- + +Хотите немного ~~наркомании~~ сахара для Go? + +Их есть у меня: + +Тернарный оператор для Go на генериках + +```go +func If[T any](condition bool, thn T, els T) T { + if condition { + return thn + } + return els +} +``` + +[Плейграунд чтобы потыкать](https://go.dev/play/p/sBDnPGHce8I) + +Будет настроение — добавлю в свою либку https://neonxp.ru/go/extra , а пока, +как-то так держите. + +**Не стоит** использовать в реальном коде. Я лично не вижу никакого оправдания +для использования, кроме как покекать. diff --git a/content/posts/2024-12-15-posse.md b/content/posts/2024-12-15-posse.md new file mode 100644 index 0000000..023e1e2 --- /dev/null +++ b/content/posts/2024-12-15-posse.md @@ -0,0 +1,81 @@ +--- +categories: +- Блог +date: '2024-12-15T22:10:46+03:00' +description: '' +image: files/2024-12-15-posse_posse.webp +location: Казань +tags: +- блог +- разное +title: POSSE +--- + +Решил я перейти к использованию практики POSSE. Что это такое? Аббревиатура +расшифровывается примерно следующими способами: + +**P** - Publish или Post, **OS** - Own Site, **SE** - Syndicate Elsewhere (мне +больше нравится, Share Everywhere) + +Это практика, когда изначально любой материал публикуется на полностью +подконтрольном собственном сайте, а только затем переразмещаяется на всякие +социальные сети, типа ВК, Телеги и прочих Мастодонов. + +<!--more--> + +## Почему это важно? + +* Во-первых, **платформы ненадежны**. Любая платформа в любой момент может + сделать что угодно с вашим контентом, или закрыться. +* Во-вторых, **право собственности**. Не секрет, что у платформ весьма вольное + представление об авторском праве на материалы размещаемые пользователями. С + одной стороны, у них неограниченное право распоряжения контентом для любых + целей, а с другой никакой ответственности за содержание контента. Не слишком + ли кучеряво? А следуя POSSE, я и все кто следуют POSSE — сохраняют + первоисточник под своим контролем, отдавая платформам лишь небольшой огрызок + от контента. Да, у меня не больно какой-то великий контент, за который стоит + трястись, но я всё равно предпочту сохранить за собой все права на него. +* В-третьих, **за пользователем остаётся право** выбирать где ему удобнее + следить за контентом. Либо на первоисточнике, с помощью божественного RSS (к + чему я бы хотел призывать), либо на удобной платформе куда происходит + синдикация. +* В-четвёртых, ... А давайте, я не буду пересказывать вот эту статью[1]? 😉 В + общем, это правильная и нужная практика. Как минимум, на долгосрок. Платформы + приходят и уходят, а файлы (в виде markdown моего блога) останутся на всегда. + +=> https://indieweb.org/POSSE [1] + +## Что я сделал чтобы следовать POSSE? + +Ну для начала, у меня сильно чесались руки переделать дизайн блога. Вроде, +получилось так, как я и хотел, в стиле сайтов начала-середины 2010х. Просто +потому что могу, кто же мне тут что запретит 😉. Тем самым я улучшил UX блога, +до хотя бы терпимого. Походу дела, при редизайне, я порасставил правильных тегов +и микроформатов для правильной синдикации с другими платформами. + +Далее, я перепилил немного улучшил программку, которую написал уже достаточно +давно, которая читает RSS моего блога и отправляет новые посты в Телеграм канал. +Вот она, если что: [2] + +=> https://git.neonxp.ru/posse [2] + +Кстати, в очередной раз напоминаю о RSS ленте [3] блога. Эта лента — это самый +правильный способ подписки на блог! + +=> https://neonxp.ru/feed/ [3] + +Так же из этой ленты автоматически подтягиваются посты в VK группу. Это сделано +встроенным механизмом VK, за что им определенно респект! Не часто можно +встретить нечто подобное на закрытых платформах (помним, же как Google убивал +RSS?)! + +Так же в ближайших планах и запилить WebMentions и прочие плюшки с ИндиВеба. + +Ну пока, как то так `¯\_(ツ)_/¯` + +Есть что сказать? Внизу есть форма для невозбранного комментирования. + +## Ссылки по теме + +- https://indieweb.org/POSSE +- https://www.theverge.com/2023/10/23/23928550/posse-posting-activitypub-standard-twitter-tumblr-mastodon diff --git a/content/posts/2024-12-17-infra.md b/content/posts/2024-12-17-infra.md new file mode 100644 index 0000000..8998c27 --- /dev/null +++ b/content/posts/2024-12-17-infra.md @@ -0,0 +1,120 @@ +--- +categories: +- Мета +date: '2024-12-17T21:07:53+03:00' +description: '' +draft: true +image: files/2024-12-17-infra_cover.webp +location: Казань +tags: +- блог +- IT +title: Инфраструктура блога +--- + +Сегодня я хочу рассказать как устроен этот блог и вообще моя инфраструктура. + +## Сервер + +Во-первых, недавно я почти полностью переехал с арендуемого сервера, на свой +собственный, сервер, который просто стоит у меня в комнате. + +Именно он вынесен в заголовочное изображение и целиком помещается, даже не на +ладони, а просто на кончиках пальцев! + +Конкретно, железо: + +* **OrangePi 3B 8Gb** — выбран в первую очередь за свою дешевизну и, самое + главное, M.2 разъём +* **NVME SSD 1Tb** — собственно, жесткий диск моего микросервера +* **Корпус с активным охлаждением** — не самое необходимое, но хотелось, чтобы + выглядело красиво + +<!-- more --> + +## Программное обеспечение + +По сути, на первом уровне, установлены armbian +(https://www.armbian.com/orangepi3b/), веб—сервер Caddy +(https://caddyserver.com/), да Docker. Всё остальное уже внутри Docker'а. + + +## Caddy + +Caddy у меня работает в основном как reverse-proxy для Docker'а. Без лишних +слов, вот конфиг: + +``` +{ + log { + output file /var/log/caddy/access.log + level debug + } + email i@neonxp.ru +} +neonxp.ru:80 { + redir https://neonxp.ru +} +neonxp.ru:443 { + tls i@neonxp.ru + root * /var/www/neonxp.ru + encode gzip + rewrite /feed/ /posts/index.xml + file_server +} +comments.neonxp.ru { + reverse_proxy localhost:8008 + tls i@neonxp.ru +} +``` + +Из него я убрал всё, что не относится к непосредственно блогу. + +Сам блог у меня собирается с помощью Hugo и загружается в `/var/www/neonxp.ru` с +помощью rsync[^4], а оттуда уже раздается с помощью Caddy. + +[^4]: https://git.neonxp.ru/blog.git/tree/Makefile#n11 + +## Docker + +А вот и мой compose в котором разворачивается остальная инфраструктура для блога + +```yaml +services: + remark42: + image: umputun/remark42:latest + restart: unless-stopped + container_name: "remark42" + ports: + - 8008:8080 + env_file: remark42.env + volumes: + - remark42:/srv/var + posse: + image: registry.neonxp.ru/posse + restart: unless-stopped + container_name: posse + env_file: posse.env + volumes: + - ./seq.txt:/store/seq.txt +volumes: + remark42: +``` + +Как понятно из этого docker-compose.yml — дополнительно поднимаются два +контейнера: + +* remark42 — система комментариев +* posse — моя программка, которая чекает RSS блога и репостит его в Telegram + +## Остальное + +Конечно же, на этой железке крутится не только блог, но и несколько других +сервисов для личного использования + +* Nextcloud — личное облако +* Vaultwarden — хранилище паролей +* SOPDS — личная библиотека Либрусека +* Git хостинг и Container registry — для разработки и хранения кода + +Но об этом я расскажу в другой раз 😉 diff --git a/content/posts/2024-12-30-irc.md b/content/posts/2024-12-30-irc.md new file mode 100644 index 0000000..d58ecf6 --- /dev/null +++ b/content/posts/2024-12-30-irc.md @@ -0,0 +1,75 @@ +--- +categories: +- Заметка +date: '2024-12-30T14:54:08+03:00' +description: '' +draft: true +image: files/2024-12-30-irc_logo.webp +location: Казань +tags: +- IRC +- IT +title: IRC +--- + +Когда-то единственным способом общения в сети в режиме реального времени был +исключительнольно протокол IRC. И всем бы он был хорош — простой, лёгкий, может +работать на чём угодно. Но времена изменились и мы погрязли во всяких +телеграммах да вотсаппах (пока не запрещенные на территории России, к +сожалению). + +Это грустно, но закономерно. Но делает ли это ИРКу плохой? Да нет конечно! И +лично меня притягивают именно такие надёжные и простые вещи — открытые, +текстовые протоколы, софт для которых можно написать чуть ли не на коленке для +любого электрочайника. + +Например, даже на таких устройствах[^1], я вполне себе могу представить клиент к +ИРКе, но не представлю клиента телеграма. +[^1]: https://club.hugeping.ru/blog/IYMX9ZdAnn0dA1RBO5JH#IYMX9ZdAnn0dA1RBO5JH + +И недавно я обнаружил, что IRC не только не умер, но и развивается, +осовременивается! Сейчас есть актуальная современная версия протокола +[IRCv3](https://ircv3.net/), которая не потеряла былой простоты и +интерперабельности! + +# Мой IRC + +Короче, не затягивая сильно, я запустил для теста небольшой свой сервачок, куда +и приглашаю забежать на огонёк и посидеть в ламповой олдскульной атмосфере: + +В любом современном IRC клиенте: + +* Сервер: `irc.neonxp.ru` +* Порт: `6667` текстовый, `6697` TLS +* Кодировка: `utf8` + +Регистрация есть через NickServ но опциональная. + +# Чем он хорош? + +Ну помимо вышеуказанных простоты и интерперабельности протокола, можно выделить +и то, что поскольку общение чисто текстовое, без всяких гифок, картинок и +прочего. Казалось бы, это же скорее минус? А вот и не обязательно. В каком-то +роде это мотивирует к конструктивному общению, когда надо хоть немного включать +мозг и думать что писать. Таким обазом, повышается осмысленность общения и +появляется определённая самодисциплина. Примерно так же, как и в переписке по +e-mail, что я тоже весьма и весьма уважаю. + +# Станет ли оно популярным? + +Да нет, конечно! Это всегда будет исключительно нишевая гиковская игрушка. И это +даже хорошо. Лампово. Так же как и обычные текстовые блоги, например. Но это не +значит, что это не имеет право на жизнь. + +Ну и да, это одна из технологий, которые я отношу к тем, что пригодятся +человечеству в случае кризисов. + +# Альтернативы? + +Самая хорошая альтернатива, что я вижу — это протокол Matrix, который выглядит +как новомодный хипстерский IRC с JSON поверх HTTP(S). С моей точки зрения, у +него есть серьёзные недостатки, но считаю, что он вполне себе займёт ту же нишу. + +Всякие телеграммы и прочее завязанное на конкртеного вендора я не рассматриваю +как альтернативы. Да, они удобные, популярные, но мертворожденные, как +технология. diff --git a/content/posts/2024-12-31-new-year.md b/content/posts/2024-12-31-new-year.md new file mode 100644 index 0000000..56e2891 --- /dev/null +++ b/content/posts/2024-12-31-new-year.md @@ -0,0 +1,50 @@ +--- +categories: +- Без рубрики +date: '2024-12-31T15:48:25+03:00' +description: '' +image: 2025.webp +location: Казань +tags: +- разное +title: С Новым Годом! +--- + +Ну что же, друзья, с наступающим! + +В этот день принято подводить итоги года. Ну и я подведу немного: + +- Поступил в институт брака. Раз уж нет классического высшего, что ещё остаётся то ;) +- В аккурат под конец года разрешились проблемы на работе. Причем разрешились + настолько удачно, что я почти что жду окончания новогоднего отпуска, чтобы + скорее начались трудовыебудни. +- Стал активно вести блог. Но всё равно не оставляет подспудное ощущение, что + уже стал надоедать этим тем, кто подписан. После каждого поста жду что кто-то + да отпишется :) Но мне нравится его вести, так что, уже не остановлюсь :) +- Ездили с новоиспеченной супругой на Кавказ. Самое яркое — посетили + обсерваторию в Нижнем Архызе. Под впечатлением, купили по приезду настоящий + телескоп! +- Начали строить свой домик в деревне. Но пока ещё до заселения далеко, вот + только окна поставили. + +Под катом приложу фоточки наиболее ярких моментов, пожалуй. + +<!--more--> + +* Институт брака +  +* Выхожу с работы +  +* Собаньки на Кавказе +  +* Своя личная обсерватория +  +* Домик в деревне +  + +Вот как-то так :) + +А пока, возвращаемся к новогоднему столу и готовимся встретить наступающий 2025 +год! + +Надеюсь, всё у нас у всех будет хорошо в этом наступающем новом году! diff --git a/content/posts/2025-04-05-tabs-or-spaces.md b/content/posts/2025-04-05-tabs-or-spaces.md new file mode 100644 index 0000000..a44e052 --- /dev/null +++ b/content/posts/2025-04-05-tabs-or-spaces.md @@ -0,0 +1,400 @@ +--- +categories: +- Размышления +date: '2025-04-05T16:53:27+03:00' +description: null +image: null +location: Казань +tags: +- размышления +title: Табы или пробелы? +--- + +Так получилось, что с Нового Года я ничего в блог не писал. Тому причина в +личной загруженности, и в не менее личной лени. Так же я делал некоторые +эксперименты над самим блогом, потому что моё внутреннее чувство прекрасного не +даёт мне просто остановиться и не трогать то, что работает. + +Но всё же, я чувствую внутреннюю потребность написать небольшую заметку с +размышлениями, которые недавно приходили ко мне в голову. + +А связаны они с тем, что есть определённые догмы в индустрии, которые непонятно +(ну или понятно) почему появились, и которым слепо следуют, хотя, как будто они +уже не имеют смысла. + +<!--more--> + +## Вечный спор + +Для затравки, «вечный спор» табы или пробелы использовать в коде для отсутпов. +Лично для меня здесь не то что выбор очевиден, для меня очевидно, что и самого +выбора то нет. Конечно же, только табы! Отступ пробелами просто не имеет права +на жизнь, и вот почему: + +* Во-первых, это просто какой-то костыль, использовать пробел не по назначению. + Наверное, не очень очевидно, но назначение пробела — это именно разделение + слов. Невероятно! А наначение таба — как раз таки форматирование отступа. + Давайте использовать инструменты по назначению! +* Во-вторых, и самое главное, как по мне, это гибкость табуляции. Я, как + читающий код, волен сам выбирать размер отступа. Например, если у меня узкий + экран (смартфон, например) — я выберу отступ в 2 *визуальных* пробела. + Наоборот, если бы у меня было слабое зрение — я бы выбрал отступ в бо́льшее + число *визуальных* пробелов. +* В-третьих, исходя из предыдущего пункта, я считаю, что использование именно + пробелов — это диктование автором исходника мне своей воли в виде своих + предпочтений (например, только 4 пробела, и никак иначе!). А какого чёрта? Это + буквально насилие! Зачем? Я считаю, это не допустимо. Пусть у каждого будет + возможность выбирать себе настройки отображения на *своей* машине под *свои* + вкусы, а не вкусы автора! +* В-четвёртых, самое малозначительное — это то, что таб это 1 байт, а пробелов + обычно больше чем 1 байт (от 2 до 8). Я считаю этот аргумент малозначительным, + т.к. уж что что, а места на носителях информации нынче в достатке. Но тем не + менее, это один из аргументов! + +А что по аргументам за пробелы? Да нет их. Ну окей, предположим, что есть. Во +многих кодстайлах (PEP-8, PSR итп) закреплены именно пробелы. Я не понимаю, +почему, вроде как, умные люди которые эти стандарты придумывали так сделали. +Возможно, привычка. Но является ли привычка каких-то людей аргументом? Наверное, +нет. И самое грустное, что эти стандарты уже не поменять, ибо с их +использованием *уже* написаны мегатонны кодов. + +Единственное, меня радует, что хотя бы в стандарте форматирования моего любимого +языка Go этой откровенной чуши нет. В Go отступы приняты табами и только ими. + +Сразу скажу, я говорил только про отступы в начале строки, но не про отступы +внутри строки, например, чтобы выстраивать значения подряд идущих констант в +одну ровную колонку. Там, вроде как, пробелы вполне оправданы. Но это не точно. +Я пока не решил для себя. + +Думаю, здесь насчёт табов и пробелов можно завершить. Если есть что накинуть — +пишите письма, e-mail внизу страницы. + +## Вечный консенсус + +Про табы и пробелы была скорее затравочка. Там, как мне кажется, всё очевидно. +Но есть менее очевидная, но как мне кажется очень родственная тема. Эта тема +вызывает сильно меньше споров, т.к. вроде как в ней уже есть консенсус. Но этот +консенсус ошибочен! + +А говорю я про форматирование длины строк! А именно, т.н. hard-wraps и +soft-wraps. Если коротко, при hard-wraps в текст в точках переноса (например, на +80 или 120 колонке) вставляются символ переноса строк (`\n`), при мягком +переносе текст остается на одной строке, но выглядит так, как будто он разделен +на несколько строк. + +А начну я с небольшой предыстории, как я к этому пришёл. Как я уже писал в +начале, у меня есть постоянное шило в седалище, которое не даёт мне просто +остановиться и использовать то, что работает, как минимум, в контексте этого +блога. И из последнего куда я смотрел — протокол Gemini[1]. Разбирая его, меня +сначала немного удивила его особенность, а именно: + +=> https://geminiprotocol.net/ [1] + +> Text in Gemtext documents is written using "long lines", i.e. you (or your +> editor) shouldn't be inserting newline characters every 80 characters or so. +> Instead, leave it up to the receiving Gemini client to wrap your lines to fit +> the device's screen size and the user's preference. This way Gemtext content +> looks good and is easy to read on desktop monitors, laptop screens, tablets +> and smartphones. + +> Note that while Gemini clients will break up lines of text which are longer +> than the user's screen, they will not join up lines which are shorter than the +> user's screen, like would happen in Markdown, HTML or LaTeX. This means that, +> e.g. "dot point" lists or poems with deliberately short lines will be +> displayed correctly without the author having to do any extra work or the +> client having to be any smarter in order to recognise and handle that kind of +> content correctly. + +Сначала, я подумал, да это же нифига не удобно, что используются длинные строки, +а не склеиваются разделённые одним переносом как в Markdown! Более того, это моё +возмущение подогревалось тем, что я всё это время был сторонником как раз +hard-wraps и форматировал что код, что markdown для блога по 80 или 120 колонке. +Потому что так всегда и везде было принято. Но потом вчитавшись, я понял, что +как раз таки «склеивание» Markdown это максимально неправильное поведение! Оно +порождает такие минусы, как более сложный парсинг, который должен обрабатывать +по разному один и два переноса строк, неочевидность, когда пишешь текст в +редакторе, а отображается он совсем по другому, потенциальные ошибки, когда +абзацы внезапно склеиваются, и т.п. + +При этом, парсинг Gemtext поразительно простой. В общем случае, достаточно +парсить по строке, и не думать о предыдущем состоянии (относится текущая строка +к предыдущему параграфу или таки нет). Единственное исключение — +преформатированный текст, при парсинге которого надо помнить состояние. Но и это +очень просто, достаточно держать единственный флаг который говорит, мы сейчас в +нормальном состоянии или в состоянии преформатированного текста. И переключать +этот флаг когда очередная строка начинается с *```*. Вообще, Gemtext кажется +наиболее правильным и приятным для меня языком разметки. Наверное, я на него +перейду. Но потом, сейчас нет времени. + +К чему я тут углубился в описание формата Gemtext? А вот к чему: только после +прочтения спеки этого формата до меня сошло озарение, что использование длинных, +а не обрезанных по 80 или 120 или ещё какую колонку более правильное не только +для формата разметки, но и для обычного кода! + +И вот аргументы: + +* Во-первых, все редакторы кода поддерживают soft-wrap и каждый волен выставить + для своего личного редактора удобную ему длину строки, а не подчиняться + привычкам автора кода. +* Во-вторых, за длину в 80 символов топят в основном старпёры что-то там + говорящие про терминалы шириной в 80 символов. Только и этот аргумент не + понятен. Когда вы в последнее время видели терминал в 80 символов? Не эмулятор + терминала, а именно сам терминал? Ну даже, хорошо, пусть будет этот терминал в + 80 символов. Но он что, не умеет переносить? Подозреваю, что может. И в чём + тогда проблема? Непонятно. Короче, требование в 80 символов (ну или более + современное в 120) выглядит как высосанное из пальца, потому что под ним нет + реальной основы кроме каких-то там исторических причин на доисторическом + железе. +* В-третьих, см. пункт про насилие автора кода над читателем кода. Например, + опять таки, узкий монитор например. И на нём не soft-wrapped текст может + вызывать горизонтальную прокрутку. И это убого. +* В-четвёртых, да, это усложняет парсинг. Это слабый аргумент, я знаю. Как + пример, правильный парсер Markdown (не буду тут бомбить про количество разных + стандартов Markdown) пишется не то чтобы очень просто. В это же время, + написать парсер Gemtext который полностью покроет спецификацию — дело максимум + часа-двух для любого, кто программирует больше, хотя бы, нескольких месяцев! + +В общем, как и в случае с табо-пробелами я не вижу ни одной достойной причины +делать жесткие переносы строк по какой-то длине! + +Возможно, я что-то упустил — тоже можно по этому поводу поспорить со мной в +электропочте. Возможно, я даже поменяю мнение, но наврядли. + +## Update 06.04.25 + +Как я и просил, один хороший человек, Владислав +(https://t.me/c/1331521959/2285), написал ответ. Прокомментирую его здесь: + +> Мне есть что сказать про ширину таба и 80 символов. + +> Аргумент про разную ширину таба работает слабо: многие стили предполагают его +> фиксированную длину. Если ставить другой, то форматирование ломается. + +> Пример: ядро Linux, где ширина таба 8, и аргументы функций "плывут" при другой +> ширине. + +Я не единожды видел этот аргумент, но он как раз и кажется мне слабым. Большая +ли разница для читающего код, как именно он его видит: + +``` +// tabsize=2 + func someFunc( + one, + two, + three, + ) +... + callOfSomeFunc = someFunc( + "one", + "two", + "three", + ) +``` + +или так + +``` +// tabsize=4 + func someFunc( + one, + two, + three, + ) +... + callOfSomeFunc = someFunc( + "one", + "two", + "three", + ) +``` + +или даже так + +``` +// tabsize=8 + func someFunc( + one, + two, + three, + ) +... + callOfSomeFunc = someFunc( + "one", + "two", + "three", + ) +``` + +Кажется, что для 8 пробелов на таб всё сильно уезжает, но раз человек себе так +настроил — то как будто его право и наверное были основания? + + +> Про 80 символов. Дело вообще не в размере терминала или ширине перфокарты. +> Некоторые программисты разделяют редактор на две вкладки, чтобы смотреть два +> файла. + +И тогда soft-wrap как раз и вместит весь код в каждую из половинок без +горизонтальной прокрутки, о чём я и говорю. + +> Некоторые используют большой шрифт. С шириной в 120 символов мы лишаем из +> возможности удобно читать код. К тому же, я считаю этот аргумент важным, 120 +> символов - это способ замаскировать плохой код. Чувак сделал 5 уровней +> вложенности в коде? Отлично! Главное чтобы в 120 символов влезло. + +Всё так! Возможно, я не очень подробно расписал, но основная моя мысль в том, +что такое жесткое ограничение мне кажется просто надуманным и взятым с потолка. +А если я после функции хочу написать небольшой коммент и он ну никак не влезает +на пяток символов? Новую строку ради этого делать? Ну как-то бредово. А для +указанного случая гораздо лучше бы звучало ограничение в стандарте типа «не +используйте больше 3 уровней вложенности в коде». Это хотя бы имело вполне себе +обоснование, то что скорее всего такой код просто архитектурно неверен и его +стоит пересмотреть. + +> Конечно, можно сказать что есть длинные константы или имена функций, но этот +> спор становится менее однозначным. Как по мне вполне хороший консенсус - это +> 100 символов в строке + +Здесь не согласен. Здесь опять «магическая константа» с потолка. + +> В целом, эти срачи мне кажутся достаточно поверхностными. Они в своем корне +> несут вопрос "как повысить читаемость кода?", но акцентируются на мелочах. + +Согласен. Мелочи. Но почему и бы про мелочи не поговорить :) Из них по +отдельности всё и строится (избитая фраза, да). В больших стандартах обычно +говорится просто декларативно «только пробелы, отступ 4 пробела, длина строк +120» и всё. А зачем и почему — опускается, как будто всем всё и так понятно. Мне +вот не очень. Чувствую себя ребёнком спрашивающим «Почему небо синее?». Потому +что мне кажется, что под этим требованием нет объективного требования кроме «так +принято». А «так принято» я часто и принимаю как валидный аргумент, например, +когда прихожу в какой-то проект, но в сути своей аргументом не является. + +> Хотелось бы иметь какие-то объективные метрики, какая-то работа в этом +> направлении была проделана, но, как я понял, это, во-первых, недостаточно +> точные метрики, а во-вторых, недостаточно развитая история. +> https://seeinglogic.com/posts/visual-readability-patterns/ + +Интересная статья, спасибо, с удовольствием прочитал. В целом, по выводам +(https://seeinglogic.com/posts/visual-readability-patterns/#8-patterns-for-improving-code-readability) +согласен. Метрика по Хольстеду (или как это перевести?) выглядит интересно, тем +что она чётко считается (хотя когда я руками считал, что-то у меня не сошлось с +примером :) ). + +Из объективных метрик, тут вскользь ещё упоминалась цикломатическая сложность, +которая вполне себе имеет право на жизнь. + +А так же, только что пришло в голову что можно читабельность кода оценивать как +вторую (?) производную от отступов по непустым строкам. При этом, чем эта +производная ближе к нулю — тем лучше. + +То есть, грубо говоря вот такой «код»: + +``` +_____ + ________ + _____ + _______ + ___ + ___ + _____ + __ + ____ +___ +``` + +Лучше чем, такой: + +``` +_____ + ________ + _____ + _______ + ___ + ___ + _____ + __ + ____ + ___ +``` + +Это стоит ещё подумать, это буквально пришло в голову только что, пока читал +статью. + +P.S.: Из забавного + +> As others have written, computers are fast and premature optimization is a bad +> thing. + +Сначала они пишут «computers are fast» а потом происходит такое: [2] + +=> https://tonsky.me/blog/disenchantment/ru/ [2] + + +## Update 06.04.25 - 2 + +Со вчерашнего дня я решил дополнить немного ещё. + +Во-первых, хочу немного снизить градус холиворности и радикальности. Ещё раз +упомяну что не вижу проблем для выравнивания пробелами текста внутри строки. То +есть например, вот так: + +``` +→ → ConstWithLongName = 0 +→ → Const1 = 1 +→ → Const2 = 2 +→ → Const3 = 3 +``` + +для меня вполне нормально кажется. Даже более того, табы *внутри* строки кажутся +плохим решением. Я говорю только про отступы в начале строки. + +Во-вторых, насчёт длинных строк. Я расписал немного сумбурно и в одну кашу +смешал как код, так и просто текст. Не стоило так. Хоть это и разные сущности, +но я всё равно считаю жесткое ограничение необоснованным ни там ни там. Но по +разным причинам: + +* Для обычного текста ограничение в N символов выглядит таким же не обоснованым, + как, например, требование автора «Читайте мои тексты только шрифтом Arial + 12pt». Глупость? Глупость. +* Так же встречал, что люди используют это ограничение при написании электронных + писем. Это выглядит как минимум странно. Письмо пишется для кого? Для + получателя, т.е. читателя. Почему отправитель за читателя решает то, как у + него будет отображаться письмо? Я часто читаю почту со смартфона с узким + экраном, но средним шрифтом (чтобы меньше напрягать глаза). И горизонтальная + прокрутка выглядит не очень. Горизонтальная прокрутка вообще почти всегда + выглядит не очень и её стоит избегать всеми силами. +* Для кода же история другая. Я не настолько поехал чтобы требовать всё писать в + одну строку. Если у функции в сигнатуре много (больше одного - двух) + аргументов — то это отличная идея написать их в столбик, а не в длинную линию, + которая ещё неизвестно как перенесётся. Я против именно переноса только из-за + магической константы колиечества символов. + +Да и вообще я ни от кого ничего не требовал. Я предлагаю только задуматься, а +обоснованны ли «общепринятые» вещи? Может, уже прошло какое-то время и ситуация +поменялась и удобнее и эффективнее выбрать что-то другое? + +И как будто стоит абстрактному «читателю», к которому я отсылал, в этом посте, +решать этот вопрос техническими средствами, типа editorconfig + pre-commit хуки +на форматирование в принятый в команде формат? Возможно да. Иначе получится, что +борясь за личную свободу — нарушаешь чужую свободу <del>писать говнокод</del>. + +А .editorconfig я себе такой в home положил: + +```.editorconfig +[*] +indent_style = tab +tab_width = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +soft_wrap = true + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.json] +indent_size = 2 +``` + +Вроде как, покрывает основное. diff --git a/content/posts/2025-05-19-nxpcms-2.md b/content/posts/2025-05-19-nxpcms-2.md new file mode 100644 index 0000000..b92a308 --- /dev/null +++ b/content/posts/2025-05-19-nxpcms-2.md @@ -0,0 +1,19 @@ +--- +date: '2025-05-19T01:00:00+03:00' +title: NXPCMS — моя CMS'ка (ч.2) +--- + +Всё же, не могу не поделиться, какое же это счастье, когда пользуешься своим же +самописным софтом! + +С одной стороны, конечно, когда видишь косячки — понимаешь, что это именно ты +продолбался, и тебе это решать. А с другой стороны, полный контроль и ты +понимаешь, _что_ пошло не так и _как_ это чинить! + +В дополнение предыдущего поста, про принципы системы, хочу добавить то, что +сознательно не буду внедрять ни теги, ни категории. Вместо этого — обычное +дерево файлов + в ближайшем плане полнотекстовый поиск по материалам сайта. +А теги мне так и так казались какой-то порочной фигнёй. + +Вот и поделился радостью. Своя CMS располагает к тому, чтобы писать сюда больше +и чаще ;) diff --git a/content/posts/2025-05-19-nxpcms.md b/content/posts/2025-05-19-nxpcms.md new file mode 100644 index 0000000..d1d30e6 --- /dev/null +++ b/content/posts/2025-05-19-nxpcms.md @@ -0,0 +1,77 @@ +--- +date: '2025-05-19T00:00:00+03:00' +title: NXPCMS — моя CMS'ка (ч.1) +--- + +Долгое время я пользовался Hugo (а одно время, даже WordPress!). И в целом, всем +он меня устраивал. Но недавно, произошло, казалось бы не связанное. Я снова +воспылал интересом к треккерной музыке. При этом я немного полазал по сети, +поспрашивал знакомых и собрал достаточно большую (>80Гб!) коллекцию. Но просто +хранить на диске было скучно и я решил её выложить во внешку. Так появился +shelter.neonxp.ru (сейчас не работает, почему - объясню ниже). + +Сначала список файлов сервил в веб просто Caddy, но у него был недостаток: он не +мог дать послушать треккерный файл без скачивания. Тогда я накидал простенькую +программку, которая так же просто отдаёт содержимое директории, но позволяла +слушать треккерную музыку. Через некоторое время я подумал, а почему бы не +прикрутить к ней и предпросмотр и других файлов? Сказано-сделано. Прикрутил +сначала просмотрщик markdown и txt файлов. Дальше, мысль полетела уже по +накатанной, и подумалось мне, что это же простенькая CMS. В эту сторону проектик +я и стал развивать. И вот вчера я таки перевёл этот сайт на мою собственную CMS! + +Но пишу только сейчас, потому что вчера после переезда уже ничего не хотел +писать, ибо переезд был непрост. И как я уже выше говорил, пока отключил shelter +в пользу этого сайта. Позже заведу и shelter. Особенности моей CMS: +* минимум конфигурации: один бинарник, который при запуске сервит сайт из + текущей рабочей директории. +* структура сайта ~= файловая структура, отсюда и листинг файлов на каждой + странице +* нет какого-то общего файла конфигурации (аля /etc/...), вместо этого для + каждой директории можно создать свой файл .config.json (формат hjson, на самом + деле), который распространяет своё действие на текущую директорию, и на все + вложенные. Вложенные директории могут иметь свои конфиги, которые могут или + частично или полностью переопределять родительский конфиг. Например: +``` +/var/www/neonxp.ru/.config.json + +{ + "title": "NeonXP.log", + "description": "Личный сайт Go-разработчика из Казани", + "index": [ "index.gmi", "index.md", "index.txt"], + "url": "https://neonxp.ru/", +} +``` +а для директории постов важно, чтобы сортировка была в обратном порядке, поэтому +её конфиг выглядит следующим образом: +``` +/var/www/neonxp.ru/posts/.config.json + +{ + "description": "Блог", # <- перезапись родительского конфига + "desc": true, +} +``` +Немного напоминает дедушку Apache2 с его .htaccess :) Но мне это кажется весьма +удачной идеей. +* Основной формат разметки — gemtext. Просто потому что мне он нравится своим + радикальным минимализмом. Минималистично настолько, что его парсер в html для + этой CMS я написал примерно за час с нуля. + +На самом деле, написать свою CMSку — достаточно старая мечта, и в своём прошлом, +я неоднократно это делал, ещё на PHP (ну тогда это было модно). Ну и кто мне +запретит сделать это сейчас, с теми идеями что я указал выше?) По факту +получилось что-то среднее между веб-сервером аля Apache2 и классическими CMS, и +мне это нравится. + +## Что дальше? + +А дальше я буду развивать её в сторону тех фич, что нужны лично мне: +* Доделать миграцию постов и материалов из старого блога. Сейчас всё + импортировано в автоматическом режиме и выглядит откровенно плохо +* Поддержка предпросмотра большего числа форматов файлов +* Хотелось бы сделать Basic авторизацию + загрузку файлов по http +* Раз уж используется gemtext — сделать и поддержку gemini протокола +* Прикрутить cgi или скрипты на lua? А почему-бы и нет? :) Хотя бы сделаю + какую-нибудь олдскульную гостевуху +* Прикрутить все эти клёвые indieweb штуки, которые было весьма проблемно + прикрутить к Hugo блогу в силу его статичности diff --git a/content/posts/2025-06-08-my-setup.md b/content/posts/2025-06-08-my-setup.md new file mode 100644 index 0000000..a4b6dc7 --- /dev/null +++ b/content/posts/2025-06-08-my-setup.md @@ -0,0 +1,31 @@ +--- +date: '2025-06-08T00:00:00+03:00' +tags: +- сетап +- гиковское +title: Мой сетап 2025 +--- + +Давно ничего не писал, да и не было особо о чём. Немного играюсь с нейросетями и +LLM в последнее время. Если выйдет что интересное - напишу об этом. + +А пока хотел написать вот о чём. + +Не помню, чтобы я когда-либо писал о том, какой у меня основной сетап, хотя сам +с удовольствием читал о том, как он организован у других людей. Пожалуй, пришло +время и мне его описать. + +<!--more--> + +- [Ноутбук](/pages/setup/laptop/) +- [Смартфон](/pages/setup/pda/) +- [NAS](/pages/setup/nas/) + +# Окончание + +Это всё что я вспомнил так сходу. По-любому, я что-то забыл, поэтому пост будет +дополняться. + +# UPD [14.06.2025] + +[Пост переехал в постоянный раздел](/pages/setup/) diff --git a/content/posts/2025-08-02-meshtastic.md b/content/posts/2025-08-02-meshtastic.md new file mode 100644 index 0000000..fad4394 --- /dev/null +++ b/content/posts/2025-08-02-meshtastic.md @@ -0,0 +1,66 @@ +--- +cover: /posts/files/meshtastic_img/tbeam.webp +date: '2025-08-02T18:00:00+03:00' +tags: +- meshatastic +- гиковское +title: Meshtastic +--- + +Некоторое время назад наткнулся в блоге [Евгения +Степанищева](https://bolknote.ru/all/myshastik/) на потрясающую штуку - +[Meshtastic](https://meshtastic.org/), или как в народе её называют - +«мышастик». Вкратце, это протокол и, в первую очередь, специальная прошивка для +целого спектра устройств, которые позволяют организовать +[mesh](https://ru.ruwiki.ru/wiki/%D0%AF%D1%87%D0%B5%D0%B8%D1%81%D1%82%D0%B0%D1%8F_%D1%82%D0%BE%D0%BF%D0%BE%D0%BB%D0%BE%D0%B3%D0%B8%D1%8F) +сеть поверх протокола [LoRa](https://ru.ruwiki.ru/wiki/LoRa). + +<!--more--> + +Базово, как это выглядит при использовании: + +У каждого участника сети есть небольшое портативное радиоустройство (готовое или +самодельное - не важно), прошитое специальной прошивкой. К устройству, чаще +всего (но необязательно!) подключен смартфон со специальным одноимённым +приложением. Устройство ищет и старается подключиться к другим подобным +устройствам, а со смартфона можно писать как в публичные, так и в приватные +каналы. Но только текстом, т.к. скорость исчисляется байтами в секунду. При +этом, хоть и радиус каждого устройства достаточно небольшой, но засчёт ячеистой +топологии можно передавать сообщения не только тем, кто в радиусе приёма, но и +тем, кого напрямую устройство «не видит», зато видят соседи или соседи соседей +(в среднем, не больше 7 хопов обычно настраивают). + +Загоревшись, я тут же заказал себе +[T-Beam](https://meshtastic.org/docs/hardware/devices/lilygo/tbeam/). Настройка +оказалась до тупости простой, единственное, что надо учитывать, это то, какие +частоты приняты в конкретном городе. Для Казани - это 868MHz. + +На своём 3D принтере напечатал ти-биму корпус, закрепил его на стену и оставил +вылавливать ноды. За неделю накопилось почти 50 штук! Но при этом, я не скажу, +что качество связи какое-то особо хорошее - регулярно теряются сообщения, да и +единовременно у меня, по сути, только один аплинк до остальной сети. Ну ничего, +я уже заказал антенну получше ;). + + + +Чехол всратоватый потому что я его печатал в наихудшем качестве, на +неотрегулированном принтере. Главное - свою задачу защиты выполняет. + +Понятное дело, что это просто игрушка и никакого практического применения у неё +нет, а в основном чате - бо́льшую часть времени сплошные «Пинг, меня слышно?». +Но! Очень забавная игрушка. И да, теоретически может позволить экстренно +связаться там, где другой сети просто нет. Просто, это не мой случай. Хотя, +недавно оказался в ситуации, когда и электричества дома не было (а с ним и +домашней сети) и одновременно была БПЛА опасность, а значит, никакущая мобильная +сеть. То есть, практически, идеальные условия для мышастика :). + +Несмотря на это, я всё же не удержался и заказал ещё два устройства, но в этот +раз попроще. Брошу один в машину, а второй или супруге отдам, или в рюкзаке +носить буду носить. Буду своими скромными силами хоть немного, да расширять сеть +своими устройствами. + +Так же ездил на стройку своего будущего дома с устройством, но, ожидаемо, там +оно сеть не увидело. Значит буду думать или о высокой антенне на крыше, или ещё +что выдумывать, чтобы из своих пердей «дотянуться» до большой городской сети. + +А к чему я про него рассказываю? А просто так. Забавная гиковская штука. diff --git a/content/posts/2025-08-05-lets-code-3d.md b/content/posts/2025-08-05-lets-code-3d.md new file mode 100644 index 0000000..3809861 --- /dev/null +++ b/content/posts/2025-08-05-lets-code-3d.md @@ -0,0 +1,154 @@ +--- +cover: /posts/files/lets-code-3d_img/4.jpg +date: '2025-08-05T20:00:00+03:00' +tags: +- 3D печать +- гиковское +title: Давай запрограммируем деталь? +--- + +Некоторое время назад я по глупости запорол свои бокорезы, так, что для +откусывания ножек радио деталей они больше не годятся. К счастью, стоят они +совершенно не дорого, и тем же днём были заказанные новые в небезызвестном +сервисе, который в девичестве содержал в себе название моего родного города. При +получении я немного огорчился тем, что в комплекте к ним не шёл колпачок, +который был у предыдущих, хотя выглядят они идентично. + +Штош. Я решил восполнить этот недостаток с интересом для себя и решил этот +колпачок самостоятельно спроектировать и напечатать на 3D принтере, который есть +у меня на хозяйстве. + +<!--more--> + + Бокорезы и +колпачок + +## Проектирование + +Обычно, детали для печати проектируются в CAD программах твердотельного +моделирования. Но я в них совершенно не умею, хотя и сын +инженеров-конструкторов. Но зато, к счастью, я программист. И вроде, не самый +худший! Посему, я решил воспользоваться свободной программой для твердотельного +моделирования [OpenSCAD](https://openscad.org/). К счастью, в репозитории +любимого дистрибутива (как, в прочем, и в большинстве других репозиториев) он +присутствовал. Что же в нём необычного? А необычное в нём то, что деталь в нём +не _рисуется_, а именно что _программируется_. Для понимания, приведу простой +пример: + +```openscad +cube([25,35,55]); // Нарисовать куб размерами 25мм х 25мм х 55мм +``` + +Просто? Очень! А учитывая что язык полноценный, с циклами и условиями +«напрограммировать» в нём можно многое. + +## Первая версия + +Вот и я не стал долго думать, открыл [мануал с +оффсайта](https://openscad.org/documentation.html), обмерил штангенциркулем +оригинальный колпачок и пошёл <del>проектировать</del> программировать. На всё +про всё у меня ушло где-то с полчаса. И у меня получилась первая версия +колпачка. Максимально простая и дубовая. Первая куцая версия: + + + +## Вторая версия + +В принципе, на этом можно было и остановиться, ведь свою функцию он выполняет. +Но у меня сработал мой перфекционист и я подумал, что было бы неплохо вырезы в +корпусе сделать один под другим, а не на одной линии, чтобы бокорез сидел ровно, +а не под углом. Потом пришла мысль, что было бы неплохо ещё и параметризировать +модель, чтобы было легко менять её размеры, а не хардкодить их. И вот получилась +вторая, и на текущий момент окончательная версия: + + + +И результат «в железе», то есть в пластике :) + + + +А сам код, думаю, он достаточно понятен ([исходники](/posts/files/source.scad) и +[STL модель](/posts/files/result.stl) я прикладываю к этому посту): + +```openscad +// Толщина стенки +wall = 2; + +// Высота внешняя +height = 12; + +// Длина основной части (внутренняя) +l1 = 15; + +// Длина носика (внутренняя) +l2 = 20; + +// Ширина у основания (внутренняя) +w = 15; + +// Толщина метала бокорезов +toolWidth = 2.1; + +// Нижняя крышка +cover(0); + +// Верхняя крышка +cover(height - wall); + +// Корпус +difference() { + linear_extrude(height) + polygon( + [ // Полигон идёт против часовой стрелки + [0, 0], + [wall, 0], + [wall, l1], + [w / 2 + wall, l1 + l2], // Внутренний кончик носика + [w + wall, l1], + [w + wall, 0], + [w + wall * 2, 0], + [w + wall * 2, l1], + [w / 2 + wall / 2 + wall, l1 + l2 + wall], // Внешний кончик носика + [w / 2 - wall / 2 + wall, l1 + l2 + wall], + [0, l1], + ] + ); + + // Вырезы + translate([w + wall, 0, height / 2 - toolWidth]) // Правый вырез чуть ниже середины + cube([wall, l1 / 2, toolWidth]); + translate([0, 0, height / 2]) // Левый вырез чуть выше середины + cube([wall, l1 / 2, toolWidth]); +} + + +// Крышка +module cover(z) { + translate([0, 0, z]) + linear_extrude(wall) + polygon( + [ // Полигон идёт против часовой стрелки + [0, 0], + [w + wall * 2, 0], + [w + wall * 2, l1], + [w / 2 + wall / 2 + wall, l1 + l2 + wall], + [w / 2 - wall / 2 + wall, l1 + l2 + wall], + [0, l1], + ] + ); +} +``` + +Надеюсь, я кому-то показал что даже без специального конструкторского +образования, но умея программировать - можно получать не только эфимерные +программки, но и вполне себе физические предметы, которые пригождаются в быту. +И если интересно, подбиваю экономику: напечатано 2 колпачка (1 и 2 версия), +каждый весом по 4 грамма, то есть примерно по 4₽ за штуку. Печатал пластиком +PLA, как моим самым любимым. + +## Ссылки + +- Сайт OpenSCAD - https://openscad.org/ +- Документация - https://openscad.org/documentation.html +- Шпаргала по функциям - https://openscad.org/cheatsheet/index.html +- Библиотеки - https://openscad.org/libraries.html diff --git a/content/posts/2025-08-09-makeup-organizer.md b/content/posts/2025-08-09-makeup-organizer.md new file mode 100644 index 0000000..b6a3feb --- /dev/null +++ b/content/posts/2025-08-09-makeup-organizer.md @@ -0,0 +1,38 @@ +--- +cover: /posts/files/makeup-organizer_img/2.png +date: '2025-08-09T16:00:00+03:00' +tags: +- 3D печать +- гиковское +title: Ещё немного печати +--- + +Я продолжаю погружаться в печать не просто готовых моделей из интернета, что не +очень интересно, но так же и в проектирование и печать собственных изделий. +Сегодня моя любимая супруга попросила напечатать ей органайзер для косметики. +Показала референс, какой она хочет. ТЗ понятное, размеры подобрали, осталось +дело за малым — непосредственно запрограммировать изделие. + +<!--more--> + +На практике, это пока самое сложное по детализации изделие из всех, что я делал. +Код получился кривоватым, но рисует то, что нужно. + +Вот так результат в редакторе: + + + +А вот так в слайсере: + + + +Ну и впервые решил добавить «клеймо мастера» со своим логотипом. Впредь, буду +добавлять его на все свои изделия, которые запроектированы именно мной. + + + +Фотографий готового изделия пока нет, ибо печататься ему ещё минимум 10 часов. +Фото я приложу позже отдельным постом ;) + +**P.S.** [архив с исходниками и готовым для печати STL +прилагаю](/posts/files/organizer.tar.zst) diff --git a/content/posts/2025-09-01-travel-1.md b/content/posts/2025-09-01-travel-1.md new file mode 100644 index 0000000..7d86c1f --- /dev/null +++ b/content/posts/2025-09-01-travel-1.md @@ -0,0 +1,142 @@ +--- +cover: /posts/files/2025-travel-1_img/preview_1.webp +date: '2025-09-01T20:00:00+03:00' +tags: +- тревелблог +title: 'Поездка по Кавказу. Часть 1: САО РАН и Аланское городище в Нижнем Архызе' +--- + +[](/posts/files/2025-travel-1_img/1.webp) + +## САО РАН + +Первая настоящая вылазка за этот отпуск. В этот раз мы ездили снова на [САО РАН +(Специальная Астрофизическая Обсерватория Российской Академии +Наук)](https://ru.ruwiki.ru/wiki/Специальная_астрофизическая_обсерватория_РАН) в +посёлке Нижний Архыз. В том году мы уже там бывали, но, во-первых, прикосновение +к настоящей науке вдохновляет каждый раз, и я не знаю на какой бы раз мне туда +надоело ездить. А, во-вторых, в этот раз был другой научный сотрудник, с +совершенно другой лекцией, так что, скучать не пришлось! Кстати, этот телескоп — +с самым большим в Евразии диаметром главного зеркала: аж 6 метров и весом в 42 +тонны! Высота купола — 53 метра, а высота самого телескопа в вертикальном +положении (как на фото) — более 40 метров! + + + +<!--more--> + +[Место на +карте](https://yandex.ru/maps/?l=sat%2Cskl&ll=41.440447%2C43.646825&pt=41.4404472%2C43.6468250&z=14) + +Дальше — небольшой фотоотчёт и расскажу о втором месте, где побывал. + +[](/posts/files/2025-travel-1_img/2.webp) + +Внезапная встреча + +[](/posts/files/2025-travel-1_img/3.webp) + +Стадо козочек и барашков + +[](/posts/files/2025-travel-1_img/4.webp) + +Купол САО + +[](/posts/files/2025-travel-1_img/5.webp) + +Обсерватория поменьше + +[](/posts/files/2025-travel-1_img/6.webp) + +Огромный кран, с помощью которого устанавливается зеркало + +[](/posts/files/2025-travel-1_img/7.webp) + +И вот он сам! Его величество — [БТА (Большой Телескоп +Азимутальный)](<https://ru.ruwiki.ru/wiki/БТА_(телескоп)>)! + +Внезапно, в самый разгар лекции, купол начал поворачиваться! Видимо, +предполагались какие-то профилактические работы перед очередной рабочей сменой +обсерватории (работает она только по ночам и в ясную погоду, что логично). В +копилку этой версии говорит и тот факт, что после поворота на главное зеркало +поднялся специалист и проводил непонятные мне, дилетанту, работы. Возможно, +ремонтно-профилактические. + +После экскурсии ещё смотрели на солнце (через специальный фильтр, конечно же!) и +я впервые своими глазами увидел настоящие пятна на солнце! + +## Нижне-Архызское городище + +Сразу после САО мы поехали в [Нижне-Архызское +городище](https://ru.ruwiki.ru/wiki/Нижне-Архызское_городище), где находятся +руины храмов и других построек X-XII веков. + +[Место на +карте](https://yandex.ru/maps/?ll=41.47500,43.68528&pt=41.47500,43.68528&spn=0.1,0.1&l=sat,skl) + +Поскольку, я не историк, и рассказать мне про них сверх данных из Рувики нечего +— просто опубликую фотографии. + +### Средний храм + +[](/posts/files/2025-travel-1_img/8.webp) + +[](/posts/files/2025-travel-1_img/9.webp) + +[](/posts/files/2025-travel-1_img/10.webp) + +[](/posts/files/2025-travel-1_img/11.webp) + +[](/posts/files/2025-travel-1_img/20.webp) + +### Северный храм + +[](/posts/files/2025-travel-1_img/17.webp) + +[](/posts/files/2025-travel-1_img/12.webp) + +[](/posts/files/2025-travel-1_img/13.webp) + +[](/posts/files/2025-travel-1_img/14.webp) + +[](/posts/files/2025-travel-1_img/15.webp) + +[](/posts/files/2025-travel-1_img/16.webp) + +### Солярный круг + +[](/posts/files/2025-travel-1_img/18.webp) + +[](/posts/files/2025-travel-1_img/19.webp) + +### Половецкая баба + +[](/posts/files/2025-travel-1_img/21.webp) + +[](/posts/files/2025-travel-1_img/22.webp) + +[](/posts/files/2025-travel-1_img/23.webp) + +Продолжение поездки следует... diff --git a/content/posts/2025-10-06-ai.md b/content/posts/2025-10-06-ai.md new file mode 100644 index 0000000..d6b30e0 --- /dev/null +++ b/content/posts/2025-10-06-ai.md @@ -0,0 +1,124 @@ +--- +cover: /posts/files/hype_curve.png +date: '2025-10-06T22:00:00Z' +tags: +- размышления +- разное +- ИИ +title: Размышления о будущем ИИ +--- + +Немного моих размышлений про будущее ИИ как технологии, а не философии. + +Как водится, когда речь о размышлениях — буду сначала вводить тезисы, а потом, +синтез. + +<!--more--> + +# Тезис первый + +Хоть сам и не пользуюсь таким инструментом, как Cursor из третьих рук я узнал, +что там не просто внутри нейронка, а целый их ансамбль. Более того, что главное, +там есть автовыбор того, какая именно БЯМ (большая языковая модель) будет +отвечать за конкретный запрос. И вот этот момент меня заинтересовал. Я +задумался, как именно это может быть реализовано. Первая мысль, как самая +очевидная была в том, что внутри помимо больших моделей, есть и маленькая, +единственная функция которой (а может и не единственная, но, значит, основная) — +классифицировать запрос (окей, промпт) по тому, какая из больших моделей возьмёт +работу на себя. + +Мысль эта мне показалась немного диковатой, т.к. показалось, что это несколько +пушкой по воробьям, и подумал, что может есть другое, более классическое, +алгоритмическое решение. Думал-думал, ничего хорошего в голову не пришло. +Поэтому всё же взял за рабочую гипотезу, что таки да, классифицирует маленькая и +дешевая моделька. Маленькая и дешёвая да, но точно не локальная. Ведь даже для +маленьких моделек нужны определённые мощности, которые есть далеко не у всех. + +И вот здесь-то мысль и полетела дальше! Представилось что в будущем всё больше и +больше «классические» алгоритмические задачи будут «закрывать» нейронками там +где надо и не надо. Чем-то напоминает ситуацию в электронике, когда экономически +часто более целесообразно не придумывать схему на элементарных радиодеталях, а +воткнуть просто унифицированный микроконтроллер, который с правильной прошивкой +заменяет целую кучку рассыпухи. + +Но продолжение мысли пока попридержу. А пока... + +# Тезис второй + +Я человек молодой, но более менее успел застать такой интересный переферийный +девайс, как [математический +сопроцессор](https://ru.ruwiki.ru/wiki/Математический_сопроцессор). Назначение +устройства — позволять компьютеру выполнять операции над вещественными числами. +Представляете, когда-то процессоры такие операции сами не умели выполнять! Но +надо делать скидку на время и то, что такие операции были, наверное, не всем +нужными, а скорее уделом специалистов, которые знали зачем им эта железка и +целенаправленно её докупали. Сейчас любой, даже самый слабейший процессор имеет +этот сопроцессор внутри себя. + +Ничего не напоминает? + +Нейронки и видеокарты! + +А вот сейчас, соединим тезисы и придём к ... + +# Синтез + +Нейронки, однозначно, вошли в нашу жизнь уже на долго. + + + +Мы сейчас, по моей оценке, где-то между пиком и дном, причём ближе именно к +пику. Затем, неизбежно будет дно разочарований. Но это не столь важно сейчас. +Важнее — следующий этап. Выйдет ли технология на плато продуктивности? Скорее +всего, да. Всё же, помимо того, что это «ыыы прикольная штука», это ещё и вещь +со вполне очевидной прикладной пользой. Не буду вдаваться в подробности, но я +имею в виду обработку естественного языка (классификация, суммаризация, +генерация), а так же «нечёткие» алгоритмы, когда путь к решению не задан заранее +(агенты и всё такое). + +И по той причине, что мы ещё не на плато, есть ощущение некоего «дикого запада». +Ещё не устаканились методы и подходы, практически ежедневно что-то появляется +новое, а старое исчезает. Горизонт - буквально пара месяцев. И инструменты всё +ещё крайне сырые. Под инструментами я имею в виду даже не программные +инструменты, а то на чём это запускается. А запускаются нейронки на видеокартах! +ВИДЕОкарты, Карл! Лично мне это выглядит несколько костыльным. Причём даже +мощные профессиональные решения типа A100, H100 за миллионы рублей это по сути +именно что видеокарты, хоть и без видеовыходов. Даже у криптовалют когда они +были на хайпе и майнились на тех же несчастных видеокартах достаточно быстро +появились специализированные решения в виде +[айсиков](https://ru.ruwiki.ru/wiki/Интегральная_схема_специального_назначения). +Ждут ли нас специализированные решения для задач ИИ? Конечно. Даже более того, +они уже есть. У того же Huawei. Устройства, которые заточены только для нейронок +и никак не способные в графику. И это, ИМХО, правильный путь, туда индустрия и +пойдёт. + +Но это, опять таки, всё ещё настоящее. А что там в будущем? + +А в будущем, думаю, нас ждёт то, что специализированные устройства для инференса +(запуска готовых моделей, грубо говоря) буду встроены непосредственно во все +потребительские компьютеры и даже носимую технику, типа телефонов. Ровно так же, +как было с математическими сопроцессорами, которые начинали отдельным +устройством, а сейчас уже давно — просто небольшая часть на кристалле +процессора. А применение им вижу как раз таки в том, чтобы там постоянно сидела +небольшая (ну небольшая для того, будущего времени, для нас настоящих, скорее +всего, весьма большая) моделька, к которой по вполне стандартизированным API ОС +будет обращаться прикладной софт, чтобы выполнять какие-то свои прикладные +задачи. Типа той, про которую я говорил в самом начале, по классификации того, в +какую большую модель пойдёт запрос пользователя. Более того, кажется, что со +временем это станет настолько общим местом, что многий софт и не запустится на +железе без встроенного «интеллектуального сопроцессора». Как сейчас не +запустится многий софт без, даже не математического сопроцессора (где вы найдете +процессор без него?), а, например, каких нибудь SSE2 инструкций (я, честно +говоря, не очень знаю зачем они, но, подозреваю, без них многое не заработает). +А значит в этом самом будущем эти «интеллектуальные сопроцессоры» будут просто +базовой частью любой ЭВМ (ну нравится мне эта аббревиатура). + +Да, можно возразить, что никак физически невозможно впихнуть такую +вычислительную мощь в маленький кристалл на плате ноутбука, а, тем более, +телефона. На это я отвечу просто: не знаю. Может быть и невозможно и такого не +будет. Но ведь когда-то казалось невозможным что компьютер не будет занимать +несколько комнат, а умещаться в кармане джинс каждого человека! Причём, тот что +в джинсах, ещё и на много порядков будет мощнее! Так что я бы ничего не +исключал. + +А как вы думаете? diff --git a/content/posts/2025-10-11-blog.md b/content/posts/2025-10-11-blog.md new file mode 100644 index 0000000..2a67127 --- /dev/null +++ b/content/posts/2025-10-11-blog.md @@ -0,0 +1,32 @@ +--- +date: '2025-10-11' +draft: true +tags: +- блог +title: Очередная смена движка блога +--- + +Ну не совсем так. Скорее сильно переделал NXPCMS. По сути, превратил её в +статический генератор по типу Hugo. Только с максимальной поддержкой Obsidian. +Как ссылок, так и его варианта Markdown. + +<!--more--> + +А раз изменения настолько кардинальные — не грешно и дать проекту новое имя. +Встречайте: [YASSG. Yet Another Static Site +Generator](https://gitverse.ru/neonxp/yassg). Оригинально, да. + +Далее сделал pipeline чтобы при коммите нового поста в git - автоматически блог +собирался и деплоился. А этим постом я по сути проверю сейчас как это работает. + +Сейчас последовательность такая: + +1. пишу новый пост в Obsidian (достаточно удобно) +2. Из него же с помощью специального Git плагина коммичу в репозиторий + (https://gitverse.ru/neonxp/sites) +3. В этом репозитории запускается пайплайн который скачивает последний билд моей + новой CMS (https://gitverse.ru/neonxp/yassg/releases) и с помощью неё + собирает Obsidian Vault в статический сайт и деплоит его на сервер. +4. Всё! + +Получается достаточно удобно! diff --git a/content/posts/2025-10-18-the-ghost-in-the-machine.md b/content/posts/2025-10-18-the-ghost-in-the-machine.md new file mode 100644 index 0000000..b8dff9f --- /dev/null +++ b/content/posts/2025-10-18-the-ghost-in-the-machine.md @@ -0,0 +1,184 @@ +--- +cover: /posts/files/laughing-man.jpeg +date: '2025-10-18' +tags: +- книги +- размышления +- не_моё +- ИИ +title: Душа в машине +--- + +Станислав Лем + + + +<!--more--> + +Понятием "душа в машине" - the ghost in the machine - некоторые психологи +(английские) закрепляют убеждение в том, что человек якобы является существом +"двойственным", т.е. состоящим из "материи" и "души". + +Сознание не является технологической проблемой, потому что конструктора не +интересует, чувствует ли машина, а только интересует, действует ли она. Таким +образом "технология сознания", как бы это сказать, может появиться только +мимоходом, когда окажется, что определенный класс кибернетических машин обладает +субъективным миром психических переживаний. + +Но каким образом можно узнать о наличии сознания в машине? Эта проблема имеет не +только абстрактно-философское значение, ибо предположение, что какая-то машина, +которая отправляется на лом из-за того, что ремонт не оплачивается, имеет +сознание, превращает наше решение уничтожить материальный предмет, типа +граммофона, в акт уничтожения индивидуальности, осознанного убийства. Кто-то мог +бы оснастить граммофон пластинкой и выключателем таким образом, что, если бы мы +сдвинули его с места, то услышали бы крики: "Ах, умоляю, подари мне жизнь!". Как +можно отличить такой, без сомнения, бездушный аппарат от мыслящей машины? Только +вступая с ней в разговор. Английский математик Аллан Тьюринг (Allan Turing) в +своей работе "Может ли машина мыслить?" предлагает в качестве решающего критерия +"игру в имитацию", которая основывается на том, что мы задаем Кому-то +произвольные вопросы и на основании ответов должны сделать заключение, является +ли этот Кто-то человеком или машиной. Если мы не сможем отличить машину от +человека, то следует признать, что машина ведет себя как человек, то есть что +она обладает сознанием. + +Отметим со своей стороны, что игру можно усложнить. Можно предположить два вида +машин. Первый вид является "обычной" цифровой машиной, которая устроена как +человеческий мозг; с ней можно играть в шахматы, разговаривать о книгах, о мире +и вообще на все темы. Если бы мы ее вскрыли, то увидели бы огромное количество +соединений элементов, подобно соединениям нейронов в мозгу, кроме того - блоки +ее памяти и т.д. и т.п. + +Второй вид машины совсем другой. Это увеличенный до размера планеты (или +космоса) граммофон. Она имеет очень много, например, сто триллионов, записанных +ответов на всевозможные вопросы. Таким образом, когда мы спрашиваем, машина +ничего "не понимает", а только форма вопроса, т.е. очередность вибраций нашего +голоса, приводит в движение передатчик, который запускает пластинку или ленту с +записанным ответом. Не будем задумываться о технической стороне вопроса. +Понятно, что такая машина неэкономична, что ее никто не создаст, потому что это +невозможно и главное - неизвестно, зачем ее создавать. Но нас интересует +теоретическая сторона. Потому что, если вывод о том, имеет ли машина сознание, +делается на основе поведения, а не внутреннего строения, не придем ли мы +неосмотрительно к выводу, что "космический граммофон" обладает им - и тем самым +выскажем нонсенс? (А скорее неправду). + +Можно ли, однако, запрограммировать все возможные вопросы? Без сомнения, средний +человек не отвечает в обычной жизни даже на один их биллион. Мы же на всякий +случай записали их во много раз больше. Что же делать? Мы должны вести нашу игру +по достаточно развитой стратегии. Мы задаем машине (то есть Кому-то, потому что +не знаем, с кем имеем дело; разговор ведется, например, по телефону) вопрос, +любит ли она анекдоты. Машина отвечает, скажем, что да, она любит хорошие +анекдоты. Рассказываем ей анекдот. Машина смеется (т.е смеется голос в трубке). +Или у ней был этот анекдот записан и это позволило ей правильно отреагировать, +т.е. засмеяться, или это в самом деле мыслящая машина (или человек, ибо мы этого +не знаем). Мы разговариваем с машиной какое-то время, а потом неожиданно +спрашиваем, припоминает ли она анекдот, который мы ей рассказали. Она должна его +помнить, если она действительно мыслит. Она скажет, что помнит. Мы попросим, +чтобы она повторила его своими словами. Вот это уже очень трудно +запрограммировать, потому что таким образом мы вынуждаем конструктора +"космограммофона" записать не только отдельные ответы на возможные вопросы, но и +целые последовательности разговоров, которые могут вестись. Это требует, +конечно, памяти, т.е. дисков или лент, которых, может, и вся солнечная система +не вместит. Положим, машина не может повторить нашего анекдота. И мы тем самым +разоблачаем, что она - граммофон. Задетый конструктор берется за +усовершенствование машины таким образом, что пристраивает ей такую память, +благодаря которой она сможет вкратце повторить сказанное. Но таким образом он +сделал первый шаг в направлении от машины-граммофона к машине мыслящей. Так как +бездушная машина не может признать идентичными вопросы аналогичного содержания, +но сформулированные даже с незначительными формальными отклонениями, типа: +"Вчера было хорошо на улице?", "Вчера была прекрасная погода?", "Погожим ли был +предыдущий день?" и т. д. и т. п., то для машины бездушной они будут вопросами +различными, а для машины мыслящей идентичными. Конструктор вновь разоблаченной +машины вынужден опять ее перерабатывать. В конце концов, после долгой серии +переделок, он введет в машину способности индукции и дедукции, способность +ассоциации, схватывания тождественной "формы" по-разному сформулированных, но +одинаковых по содержанию высказываний, пока в результате не получит машину, +которая просто будет "обычной" мыслящей машиной. + +Так появляется интересная проблема: когда именно в машине появилось сознание? +Предположим, что конструктор не переделывал эти машины, а относил каждую в музей +и следующую модель создавал с начала. В музее стоит 10 000 машин, потому что +столько было очередных моделей. Результатом стал плавный переход от "бездушного +автомата" типа играющего шкафа к "машине, которая мыслит". Должны ли мы признать +машиной, имеющей сознание, машину номер 7852 или только номер 9973? Они +отличаются друг от друга тем, что первая не умела объяснить, почему она смеется +над рассказанным анекдотом, а только говорила, что анекдот очень смешен, а +вторая умела. Но некоторые люди смеются над шутками, хотя и не могут объяснить, +что именно в них смешно, потому что, как известно, теория юмора - это твердый +орешек. Разве эти люди тоже лишены сознания? Нет же, они, наверное, просто не +очень быстро реагируют или малообразованные, их ум не обладает навыками +аналитического подхода к проблемам; но мы спрашиваем не о том, умная ли машина +или скорее туповатая, мы только спрашиваем, имеет ли она сознание или нет. + +Казалось бы, следует признать, что модель номер 1 имеет ноль сознания, модель +номер 10 000 имеет полное сознание, а все средние имеют "все больше" сознания. +Это утверждение показывает, насколько безнадежной является мысль о том, что +сознание можно точно локализовать. Отсоединение отдельных элементов ("нейронов") +машины спровоцирует только слабые, количественные изменения ("ослабления") +сознания так же, как это делает в живом мозге прогрессирующий процесс болезни +или нож хирурга. Проблема не имеет ничего общего ни с использованным для +конструкции материалом, ни с размерами "мыслящего" устройства. Электрическую +мыслящую машину можно построить из отдельных блоков, соответствующих, положим, +мозговым извилинам. Теперь разделим эти блоки и разместим на всей Земле так, что +один находится в Москве, второй в Париже, третий в Мельбурне, четвертый в +Иокогаме и т. д. Отделенные друг от друга, эти блоки "психически мертвы", а +соединенные (например, телефонными кабелями) они стали бы одной, интегральной +"индивидуальностью", единым "мыслящим гомеостатом". Сознание такой машины +находится ни в Москве, ни в Париже, ни в Иокогаме, но, в определенном смысле, в +каждом из этих городов и, в определенном смысле, ни в одном из них. Потому что о +нем трудно сказать, что оно, как Висла, имеет протяженность от Татр до +Балтийского моря. Впрочем, подобная проблема демонстрирует, хотя и не так ярко, +человеческий мозг, потому что кровеносные сосуды, белковые молекулы и ткани +находятся внутри мозга, но не внутри сознания, и опять-таки нельзя сказать, что +сознание находится под самым куполом черепа или, скорее всего, ниже, над ушами, +по обеим сторонам головы. Оно "рассеяно" по всему гомеостату, по его +функциональной сети. Ничего больше заявить на эту тему не получится, если мы +хотим соединить сознание с возможностью рассуждать. + +Вышеприведенный текст, скопированный из моей "Суммы технологии", был написан в +середине 1963 года. С точки зрения сегодняшней ситуации он представляет очень +сильное упрощение дороги, которую мы должны пройти, чтобы дойти до имитации +описанной мной цели. Мы уже предполагаем, что "сознание" и "интеллект" - это в +определенном смысле отдельные сути бытия. Мы знаем, что существуют достаточно +различные состояния сознания, даже если шкала их находится между сном и +реальностью. Но и сон, точнее, мечта во сне, может характеризоваться +разнообразной насыщенностью конкретностей, которые имитируют реальность, +сознательно переживаемую наяву. В свою очередь, сознание наяву, что каждый знает +по собственному опыту, даже если он не является ни психологом, ни психиатром, +может также иметь очень различные состояния. Человек в состоянии болезненного +жара может осознавать свое состояние, то есть то, что его сознание подверглось +нарушению. Различные химические средства могут самым разным образом формировать +человеческое сознание. Кроме того, следует отметить, что есть множество +действий, которые человек может делать машинально, то есть четко и неосознанно. +Сознание водителя автомобиля, особенно быстрого, "не успевает" за реакциями +этого водителя в ситуациях с неожиданной последовательностью событий. Вместе с +тем машинально можно делать глупости, мы называем их чаще всего "действиями по +рассеянности". + +Все это я сказал в отношении моего текста тридцатипятилетней давности, в котором +я задумался над "ростками" сознания в машине, и делал я это потому, что мне +казалось, что люди очень отличаются друг от друга уровнем умственных +способностей, а сознание всем дано приблизительно похожее. + +Дороги напрямик, по прямой и восходящей линии, от полного автомата, каким +является компьютер, к машине, которой мы могли бы приписать сознание, нет. +Вместе с тем работу нашего мозга мы уже знаем настолько, чтобы узнать то, что +так называемая каллотомия, или рассечение большой белой спайки, соединяющей +полушария мозга, не ликвидирует сознания, но создает в разделенных полушариях +две его разновидности. Кроме того, мы знаем, что мозг является системой, +построенной из огромного количества функциональных модулей, которые в отдельных +окрестностях мозга создали среду, формирующую сознание. Уточню сказанное +примером. Существует часть коры мозга, способствующая тому, что мы видим цвета. +Повреждение этого модуля приводит к тому, что таким образом пораженный человек +видит все без цвета, как в черно-белом кино. Чем точнее мы узнаем специфику +функциональной ориентации модулей мозга, тем с большим удивлением узнаем, как, с +точки зрения инженерной экономии, хаотично устроен мозг, хотя мы при осознании +самих себя не отдаем себе в этом отчета. Сегодня нам кажется, что отдельные +модули, функционально похожие на модули мозга, мы уже сможем конструировать. +Обычно это псевдонейронные сети различной сложности. Вместе с тем мы еще не +умеем ни создать их в достаточном количестве, ни соединить их таким образом, +чтобы созданное произведение смогло имитировать сознание. Следовательно, прямой +дороги от бездумного автомата к сознательно мыслящей машине нет. Есть, однако, +много сложных дорог, которые в будущем приведут нас к цели и, может быть, эту +цель превзойдут. О такой возможности я написал книгу "Голем XIV". + +Краков, 7 июля 1998 года. diff --git a/content/posts/2025-11-03-blog-deploy.md b/content/posts/2025-11-03-blog-deploy.md new file mode 100644 index 0000000..2b394d6 --- /dev/null +++ b/content/posts/2025-11-03-blog-deploy.md @@ -0,0 +1,46 @@ +--- +date: '2025-11-03' +tags: +- блог +title: Деплой блога +--- + +А ещё, я решил поделиться тем как я пишу в блог. Потому что, почему бы и нет. + +<!--more--> + +Во-первых, у меня есть такой вот Makefile просто в корне home: + +```Makefile +new-post: + @printf "Введите имя поста (латиницей, без пробелов) [new-post]: "; \ + read postname; \ + if [ -z "$$postname" ]; then \ + postname="new-post"; \ + fi; \ + date=$$(date +%Y-%m-%d); \ + file="neonxp.ru/posts/$$date-$$postname.md"; \ + echo "---" > "$$file"; \ + echo "title: " >> "$$file"; \ + echo "date: $$date" >> "$$file"; \ + echo "tags: []" >> "$$file"; \ + echo "---" >> "$$file"; \ + echo "" >> "$$file"; \ + echo "---" >> "$$file"; \ + echo "Комментариев в блоге не предусмотрено, но вы всегда можете написать мне на e-mail [i@neonxp.ru](mailto:i@neonxp.ru) или в джаббер [i@neonxp.ru](xmpp://i@neonxp.ru)" >> "$$file"; \ + nvim "$$file" + +publish-post: + yassg generate + scp -r /home/neonxp/.local/share/yassg/* neonxp.ru:/var/www/neonxp.ru/ +``` + +И, соответственно, когда я хочу написать новый пост, я вызываю `make new-post`, +скрипт у меня спрашивает имя файла, а затем открывает любимый neovim, в котором +я уже и пишу сам текст поста. + +Затем я вызываю `make publish-post` и сначала мой генератор статических сайтов +[YASSG](http://gitverse.ru/neonxp/yassg/) собирает сайт в статический HTML, а +потом отправляет всё на сервер в директорию, из которой сайт раздаётся. + +Очень просто! diff --git a/content/posts/2025-11-03-my-setup.md b/content/posts/2025-11-03-my-setup.md new file mode 100644 index 0000000..a375155 --- /dev/null +++ b/content/posts/2025-11-03-my-setup.md @@ -0,0 +1,21 @@ +--- +date: '2025-11-03' +tags: +- гиковское +- сетап +title: Обновления по сетапу +--- + +Со времени [последнего поста](/posts/2025-06-08-my-setup/) про мой +[сетап](/pages/setup/laptop/) произошли достаточно серьезные изменения. + +<!--more--> + +Я перешёл таки с KDE сначала на Hyprland, а затем окончательно осел на Gnome (48 +на текущий момент). ОС осталась та же — AltLinux P11. Что что, а Alt меня +всецело устраивает. + +Первое время с Гнома прям отплёвывался после KDE то, но потом то ли привык, то +ли просто как-то проникся внутренней эстетикой, но, в общем, мне стало заходить. + +По железу обновлений, к сожалению, нет. diff --git a/content/posts/2025-11-04-blog-deploy-2.md b/content/posts/2025-11-04-blog-deploy-2.md new file mode 100644 index 0000000..b692d94 --- /dev/null +++ b/content/posts/2025-11-04-blog-deploy-2.md @@ -0,0 +1,50 @@ +--- +date: '2025-11-04' +tags: +- блог +title: Деплой блога — пересмотр +--- + +После [вчерашнего поста](2025-11-03-blog-deploy) мне написал один [хороший +человек](http://www.stargrave.org) с дельным замечанием, что не стоит для этих +целей использовать make. Действительно так. И предложил хорошее решение, что +это стоило сделать просто sh скриптами. + +У меня только один вопрос. А почему я сам-то так сначала не сделал? Это же +буквально на поверхности! + +Штош, бывает, затупил. Да и привык для всех гвоздей использовать этот молоток. + +<!--more--> + +Обновлённые скрипты: + +~/.local/bin/new-post + +```sh +#!/bin/sh -e +postname="${@:-new-post}" +date=$(date +%Y-%m-%d) +fn="neonxp.ru/posts/$date-$postname.md" +cat >$fn <<EOF +--- +title: +date: $date +tags: [] +--- + +--- +EOF +$EDITOR $fn +``` + +~/.local/bin/deploy-blog + +```sh +#!/bin/sh -e +yassg generate +scp -r /home/neonxp/.local/share/yassg/* neonxp.ru:/var/www/neonxp.ru/ +``` + +Сейчас раздумываю, а почему бы этот функционал не включить в сам yassg, +например, аналогом хуков? Надо будет обмозговать. diff --git a/content/posts/2025-11-09-migration.md b/content/posts/2025-11-09-migration.md new file mode 100644 index 0000000..aec1df0 --- /dev/null +++ b/content/posts/2025-11-09-migration.md @@ -0,0 +1,55 @@ +--- +comments: true +date: '2025-11-09' +tags: +- блог +- разное +title: Переезд? +--- + +В последнее время всё больше и больше думаю, что у меня перебор серверных +мощностей. Да и софтовое хозяйство там немного в бардаке по историческим +причинам. Есть желание капитально прибраться. + +<!--more--> + +Сейчас у меня основная железка на хозяйстве это Intel Xeon E3-1230 3.2 ГГц, 16 +ГБ DDR3ECC, 4x1TB HDD SATA в виде арендуемого Dedicated сервера. + +И вроде неплохо, а вроде как и перебор. Зачем мне так много? + +У меня сейчас крутится несколько сайтов (ну по сути 0 нагрузки), Jabber сервер +(Prosody), почта (MOX), Mumble сервер да DNS (CoreDNS). + +~~Кажется, что под всё это хватит и небольшой VDS'ки.~~ + +UPD: посмотрел цены, в общем, выигрыша по деньгам особо не будет. VDS стоят +как-то неадекватно дорого. Видимо, придётся остаться на текущем дедике, просто +основательно почистить его, видимо. + +~~Переезд я буду делать, скорее всего, поэтапно:~~ + +Переезд получится единомоментным, т.к. переезд будет на тоже самое железо, а не +на другое. + +1. ~~Заведу VDS'ку.~~ Новую заводить не буду, а почищу начисто текущий сервер. +2. Настрою почту (MOX хорош, конечно, но я хочу чего-то более кондового, типа + Postfix) - перекину на новый сервер MX записи. +3. Настрою Jabber (Prosody, как был - так и останется, он достаточно хорош) - + перекину на него SRV записи. +4. Так же естественно, надо не забыть Coturn сервер для звонков по Jabber. +5. И только после этого надо переносить Git (Forgejo), и сайты. При этом + перееду с Caddy на Angie. Перекину уже A (и даже AAAA!) записи. +6. Перенос CoreDNS. +7. Проверяю что всё хорошо и если так, то отказываюсь от арендованного сервака. + +Надеюсь, ничего не забыл. А если и забыл — значит оно мне и не нужно, в +общем-то. + +Что по итогу? Самое главное — сброшу груз старья, который накопился за годы +аренды дедика. И что тоже приятно, но не главное — оптимизирую расходы. Причём, +наверное, раз в 10. + +По времени — думаю, растянется на неделю-две, т.к. свободного времени на всё +это у меня, как всегда, катастрофически мало. Так что, если какие-то мои +сервисы будут не доступны — значит я в процессе переезда. diff --git a/content/posts/2025-11-23-org.md b/content/posts/2025-11-23-org.md new file mode 100644 index 0000000..95b54ee --- /dev/null +++ b/content/posts/2025-11-23-org.md @@ -0,0 +1,253 @@ +--- +comments: true +date: '2025-11-23' +tags: +- гиковское +- моё +title: Личный органайзер +--- + +Пришло в голову, почему бы не рассказать как у меня организован личный +органайзер. + +Для начала стоит очертить то, какие у меня потребности от органайзера: + +- Вести список ежедневных, еженедельных, ежегодных, а так же, одноразовых + событий +- Вести быстрый список ближайших задач (ToDo список). Под быстрым, я + подразумеваю то, что внести новый пункт в него я могу не дольше, чем за пару + десятков секунд. Если это будет требовать бо́льших усилий, то я себя знаю: я + это быстро заброшу, т.к. это станет для меня не помощью, а повинностью. +- Место для быстрых заметок в формате «бесконечного текстовика». Аналогично, + это должно быть под рукой в быстром доступе. Obsidian себя показал _слишком_ + медленным. Настолько, что мне стало проще запоминать, чем испытывать свои + нервы каждый раз, наблюдая его длительный запуск. Да, звучит на первый взгляд + глупо, но у меня так: запуск Obsidian длительностью в десяток секунд + окончательно отбил у меня желание вовсе запускать его. + +Что же делать? Искать идеальный для себя инструмент? Идеального для _себя_ +точно не найду. Написать самому, ведь «яжпрограммист»? Можно, но откровенно +жаль время. Что же делать-то? + +<!--more--> + +К счастью, я вспомнил что у меня же unix-подобная операционная система, в +поставке которой огромное количество небольших программ, которые прекрасно +выполняют какую-то небольшую функцию и при этом отлично стыкуются друг с другом +через стандартный текстовый поток! Грешно не воспользоваться наработками +гораздо более умных, чем я, программистов! + +# ToDo + +Проще всего оказалось с этим. Просто поставил себе +[todo.txt](https://github.com/todotxt/todo.txt-cli). Хоть я и говорил выше, что +идеального инструмента я не найду, но я тогда немного слукавил. Для ToDo этот +инструмент _почти_ идеален. Всё что мне надо, кроме одного нюанса, там есть. А +вот тот самый нюанс, я когда-нибудь исправлю. Возможно. + +Для удобства я себе в zsh добавил следующие alias: + +```zsh +alias t=todo.sh +``` + +Таким образом, чтобы добавить задачку я просто пишу `t add текст задачи`. Куда +уж проще и быстрее? + +# Календарь + +Идею организации календаря я подсмотрел в программе calendar, которая идёт в +комплекте с BSD системами, но не идёт в конкретно моей ОС. Да, наверняка, можно +и к себе притащить, но я из спортивного интереса хотел решить задачу +максимально встроенными и стандартными инструментами. + +Лонг стори шорт: + +~/calendar.txt + +``` +01-28 ДР Лены +10-18 ДР Мамы +05-24 Годовщина свадьбы +11-23 Ежегодное событие + +Пн 15 Еженедельный мит +Пн 20-21 Чтение + +Вт 13 Архком +Вт 15 Грумминг +Вт 20-21 Чтение + +Ср 13 Техразвитие +Ср 20-21 Чтение + +Чт 15 Грумминг +Чт 20-21 Чтение + +Пт 20-21 Чтение +Сб 20-21 Чтение +Вс 20-21 Чтение + +2025-11-19 10 клуб амбассадоров +2025-11-17 16:30-17:30 Встреча c 16:30 до 17:30 +2025-11-24 10:15 Golang Техком +2025-11-24 11-12 Анализ логики состояния +2025-11-23 21 Написать в блог о своём календаре +``` + +Пояснения: + +- `mm-dd\t\tСобытие` - некие ежегодные события, у которых указаны только месяц + и день месяца +- `Пн\tвремя\tСобытие` - еженедельное событие. Про формат времени - будет ниже. +- `yyyy-mm-dd\tвремя\tсобытие` - разовые события в конкретную дату и время. + +Формат времени: его я подсмотрел у формата +[calendar.txt](https://terokarvinen.com/2021/calendar-txt/), то есть, запись +формата `15` - это означает что событие начнётся в 15 часов, `20-21` - событие +длится с 20 до 21 часа вечера. С минутами, которые не обязательны, думаю, всё +понятно из примера. + +Формат сам по себе абсолютно не жёсткий, допускает много вольностей. Главное, +всё сводится к тому, что у него 1 строка - 1 событие и сама строка состоит из 3 +полей разделённых табом (в формате calendar.txt предлагается точка, для меня +это показалось неприемлемым, т.к. я записываю в события и ссылки на созвоны, а +ссылка включает в себя минимум одну точку) + +В принципе, тут уже можно было бы и остановиться и жить с просто текстовиком, +но так было бы не интересно. Я написал на языке оболочки несколько полезных +скриптов. Они настолько маленькие, что я просто приведу их здесь: + +~/.local/bin/calendar + +```sh +#! /bin/sh + +cur=${2:-`date +%Y-%m-%d`} +file=${1:-~/calendar.txt} +grep \ + -e "^$(date +%Y-%m-%d -d $cur)"\ + -e "^$(date +%a -d $cur)"\ + -e "^$(date +%m-%d -d $cur)" $file |\ +sort -n -k 2 |\ +cut -f2- | fold -w 80 -s +``` + +Собственно, это главный скрипт, который собирает для текущей (или явно +указанной) даты все релевантные события, сортирует их по времени и выводит +форматированным списком. Примерно так: + +``` +% calendar + Ежегодное событие +20-21 Чтение +21 Написать в блог о своём календаре +``` + +На этом я не остановился, но сделал ещё парочку вспомогательных скриптов, +использующих его за основу: + +~/.local/bin/today + +```sh +#!/bin/sh +echo "Календарь:" +echo -e ''$_{1..80}'\b-' +cal +echo -e ''$_{1..80}'\b-' +echo "События дня:" +echo -e ''$_{1..80}'\b-' + +calendar + +echo -e ''$_{1..80}'\b-' + +echo "ToDo:" +todo.sh ls +``` + +Делает по сути тоже самое что и просто calendar, только ещё и рисует красивый +графический календарик и показывает список ToDo задач. + +~/.local/bin/week + +```sh +#!/bin/sh + +echo "На 7 дней:" +for i in {0..6} +do + d=`date +%Y-%m-%d -d "+ $i day"` + echo -e ''$_{1..80}'\b-' + echo $d + echo -e ''$_{1..80}'\b-' + calendar ~/calendar.txt $d +done +echo -e ''$_{1..80}'\b-' +echo "ToDo:" +todo.sh ls +``` + +Выводит план на 7 дней вперёд. + +## Редактирование календаря + +Здесь тоже предельно просто: добавил в zshrc такой алиас: + +```zsh +alias ev='nvim +/`date +"%Y-%m-%d"` ~/calendar.txt' +``` + +и просто по команде `ev` открывается neovim и готов принимать новое событие. +Хотя это и не самая частая операция. + +# Быстрые заметки + +Тут тоже всё просто: + +```zsh +alias qn='nvim "+normal G" ~/quicknote.txt' +``` + +Соответственно, по команде `qn` открывается мой текстовик для заметок на самой +последней строке. Можно дописать или поискать что-то с конца. На самом деле +очень удобно! + +# Мобильный? + +Я бы хотел все эти мои текстовики иметь и на мобильном устройстве. Даже не для +редактирования, а например, свериться со списком задач / событий. + +Тут чуть сложнее. Для синхронизации с мобильным устройством я сделал такой финт: + +1. Все текстовики у меня лежат не в домашней директории, на самом деле, а в + некой директории из которой симлинками уже прокинуты в корень домашней + директории. +2. Директория эта добавлена в Syncthing который синхронизирует её с NAS и + мобильным устройством. +3. На мобильном устройстве стоит замечательная программа + [Markor](https://f-droid.org/packages/net.gsantner.markor/) которая нативно + понимает формат todo.txt, ну и достаточно неплохо позволяет смотреть + редактировать файлы calendar.txt и quicknote.txt. + +# Чтобы хотелось ещё? + +- Как я упоминал, в todo.txt для меня есть неприятный нюанс который я бы хотел + исправить, а именно, вложенные задачи, когда у одной задачи может быть + сколько угодно дочерних, у которых, так же могут быть дочерние. Пока думаю, + расширить формат табуляцией в начале строки. Количество \t - уровень + вложенности. Но тогда придётся модифицировать todo.txt-cli который я + использую. И непонятно как это проглотит Markor. Можно конечно использовать + встроенную возможность задавать key-value значения. Тогда будет что-то типа + `Подзадача parent:2`. Это, как будто, самый правильный способ, который и + рекомендуется разработчиками формата, но получается слишком многословно, а + даже если сократить до `p:2` - всё равно надо в голове держать номер + родительской задачи. Так себе. Не знаю ещё как поступлю, но как-то поступлю. +- Было бы неплохо прикрутить парсинг ICS файлов из почты для автоматического + добавления событий в календарь. Это сто́ит сделать однозначно! +- Ну и очень желательно сделать скрипт который по крону за 15 минут до события + напомнит о нём, через какой-нибудь `notify-send`. Это, на самом деле, из всех + хотелок самая приоритетная для меня сейчас. + +Если будет интересно, я могу здесь рассказывать о том, что сделал из этих +хотелок. diff --git a/content/posts/2025-12-02-httpsocalypse.md b/content/posts/2025-12-02-httpsocalypse.md new file mode 100644 index 0000000..f38fa36 --- /dev/null +++ b/content/posts/2025-12-02-httpsocalypse.md @@ -0,0 +1,31 @@ +--- +comments: true +date: '2025-12-02' +tags: +- размышления +title: HTTPS и конец интернетов +--- + +[Let's Encrypt уменьшит срок действия сертификатов до 45 +дней.](https://www.opennet.ru/opennews/art.shtml?num=64363) + +А потом будет на неделю, на день, на запрос... Короче, да, по сути амерская +конторка будет решать на какой сайт будет возможно зайти, а на какой нет. А то +что хромые браузеры сделают невозможным заход на сайты без валидного +(заверенного _кем надо_, конечно же) сертификата, я уже и не сомневаюсь. + +Во истину говорю вам: грядут последние дни интернета. Ну точнее WWW, если +говорить конкретнее, но это уже душнилово. + +<!--more--> + +Но это не сильно то печаль. Интернет, всё равно, для большинства уже скукожился +до двух десятков «сервисов». А для энтузиастов будут «свободные» браузеры, +свободные не только с точки зрения кода, но и политики стран НАТО. + +К счастью, у нас есть Librewolf, Palemoon, Dilo, да Lynx наконец. Без +интернета (окей, WWW) не останемся. + +Так же можно рассмотреть и альтернативные технологии типа gopher, gemini. Но +это, как-нибудь, в другой раз. А пока запасаемся попкорном и смотрим как горит +<del>мир</del> интернет. diff --git a/content/posts/2025-12-21-sicktech.md b/content/posts/2025-12-21-sicktech.md new file mode 100644 index 0000000..6a7428c --- /dev/null +++ b/content/posts/2025-12-21-sicktech.md @@ -0,0 +1,228 @@ +--- +comments: true +cover: /posts/files/2025-12-21-sicktech.png +date: '2025-12-21T18:40:26+03:00' +tags: +- it +- размышления +- sicktech +- лонгрид +title: Про здоровые и нездоровые технологии +--- + +Я заметил, что часто стал в речи употреблять словосочетания «здоровая +технология» или «нездоровая технология». Но при этом, я не задумывался о том, а +что же именно это для меня значит, и как определить что есть здоровая, а что +нездоровая технология. Я классифицировал исключительно интуитивно, исходя из +принципа «я так чувствую». + +Но раз есть классификация, то должны быть и критерии. Так? Так. И критерии я +опираясь на собственное ощущение, вроде как, нашёл. + +<!--more--> + +# Критерии + +Не буду сильно уходить в сторону, для меня основные критерий «здоровости» — то, +предполагает ли технология сохранение у пользователя контроля над ней. Даже +можно сказать более витиевато — превращает ли технология пользователя в +потребителя? + +Это был первый критерий. И он же главный. Из него вытекают уже такие критерии +как потенциальная познаваемость, ремонтопригодность, и даже, внезапно, +надёжность и долговечность. + +# Примеры + +Пройдёмся немного по примерам. Начнём с «нездоровых» технологий и конкретных +примеров. + +## Трактора John Deere + + + +Это достаточно известная история, про то, как производитель напрямую влияет на +то, как пользователь пользуется своей собственностью. А именно, запрещал +самостоятельный ремонт тракторов своего производства своим клиентом, кроме как с +помощью «сертифицированного» специалиста. Здесь буквально прослеживается +критерий превращения пользователя из обладателя собственности в потребителя в +некотором роде услуги «владения трактором». Когда ты вроде и заплатил за него +полную стоимость, но владеешь им на пол шишечки, ведь ты не имеешь права +распоряжаться им так, как ты хочешь (например, самостоятельно ремонтировать, раз +у тебя подходящая квалификация). Притом, что фермеры, в большинстве своём, или +достаточно квалифицированны или могли бы найти такого квалифицированного +человека поблизости, а не ждать дни, пока до них доберётся специалист и починит. + +[Подробная история](https://habr.com/ru/companies/itelma/articles/477638/) + +## Машины для мороженного в McDonalds + + + +Здесь примерно та же история, только чуть больше налёта монополии. Краткая суть +в том, что владелец франшизы McDonalds принуждал своих франчайзи закупать +исключительно конкретные машины конкретного производителя. Всё было бы не так +плохо, если бы эти машины регулярно не ломались, а обслуживать их мог опять таки +«сертифицированный» техник. Опять таки пользователь превращался в бесправного +потребителя. Причём, можно было бы сказать, что это же хорошо, ведь пользователь +только что-то сломает, но не сделает хорошо. Этот аргумент ломается об то, что +как раз таки для этих машин предприимчивая пара людей начала производить +устройство, которое делает этим машинам «jail-break» который затем позволял +успешно чинить и эксплуатировать автоматы дальше. И ничего страшного не +произошло! Во всяком случае, именно для пользователей. Производителю, конечно +же, это не понравилось. И да, суды, давление монополиста и всё как мы любим. + +[Подробная история](https://habr.com/ru/articles/557746/) + +## BMW и некоторые другие автоконцерны + + + +Здесь немного другая история. Думаю, все и так слышали про эту историю, которая +бы могла быть шуткой, но оказалась реальностью. Я говорю про _подписку_ на, мать +его, подогрев сидений. Ну и другие опции, я не вдавался подробно, не люблю эту +марку автомобилей. Можете хоть бить, хоть резать, но такое мне не влезает в +голову! Человек _уже_ купил автомобиль и всё что в нём находится. В том числе и +нагревательные элементы в креслах! Почему он _должен_ покупать право на +включение устройства которое он и так купил? Я этому не вижу ни одного +оправдания. Мне плевать на хотелки жирных баварских подсвинков и я обеими руками +поддерживаю хакеров, которые [джейл-брейкают](https://habr.com/ru/news/678362/) +в автомобилях эти функции! + +Так же [недавно были +новости](https://www.gazeta.ru/auto/2025/12/03/22117009.shtml?utm_auth=true) про +то, что владельцы других немецких автомобилей в России остались по сути с +дорогой грудой металла. + +## Принтеры и их зачипованные картриджи + +Эта проблема известна всем владельцам принтеров. 99% (источник статистики: мой +потолок) современных принтеров не будут печатать неродным картриджем или +картриджем, в который залиты чернила или засыпан тонер повторно (т.к. никому, +кроме производителя, не нужный чип на картридже сказал что картридж уже Б/У). + +## Apple + +Мне нужно пояснять? Думаю, нет. Хорошо хоть что железо у них достойное и +достаточно надёжное, насколько я могу судить. Это хоть как-то оправдывает их +право на существование. + +## Проприетарный софт + +Тоже в пояснениях не нуждается. Хуже только прориетарный софт от амеров. + +## <что-то> по подписке + +Как бы не было это удобно, но подписочная модель, по определению ставит +пользователя <del>раком</del> в позу бесправного потребителя. И не надо мне +писать про удобство игр, фильмов, музыки, книг «по подписке». Нет, это никогда +не будет хорошим выбором. Никогда. А впрочем, я не склонен осуждать тех кто этим +пользуется, если человек отдаёт себе отчёт в том что с одной стороны он ничем не +владеет, а с другой стороны он во власти капиталистических свиней, которые и +деньги за подписку возьмут и личные данные куда надо продадут. Тут уж каждый сам +решает, или смотреть условный нетфликс пока ему добрый дядя разрешает, или +покупать тоже самое с чуть меньшим удобством на _зелённом нетфликсе_. + +# А что делать-то? + +На самом деле, решение то весьма простое: нужно более сознательно подходить к +выбору как софта, так и железа. Нужно стараться отвечать себе на вопрос «А что я +буду делать, если производитель или поставщик вдруг исчезнет или решит, что не +хочет иметь со мной дел?». В общем случае нужно выбирать то, что ремонтопригодно +(а в случае софта — свободно), имеет в свободном доступе исчерпывающие +документации и руководства, а так же независимых поставщиков запасных деталей и +сервисного обслуживания. Так же, в идеале, если нет возможности вообще не +зависеть от вендора, то выбирать всегда локального, то есть российского или, на +худой конец, из дружественных стран типа РБ или КНР. Конечно, это не защитит от +рисков что вендор исчезнет и оставит нас без поддержки, но сильно их сократит. +Но опять же, это если без вендора ну совсем никак. И самое худшее что можно +сделать — довериться вендорам стран НАТО. В этом случае это хорошая заявка на +премию Дарвина, не иначе. + +Нужны примеры? Их есть у меня, даже из личного опыта: + +- Мой МФУ купленный сто лет назад до сих пор служит мне верой и правдой, потому + что в своё время я озаботился тем, чтобы выбрать модель, которая выпущена + ровно до конкретной даты и поддерживает неоригинальные картриджи, которые + стоят три копейки и даже поддерживают самостоятельную засыпку тонера. А + ремонтопригодность у него такая, что я буквально могу починить его или сам, + или в ремонтной мастерской в моём доме. +- Автомобиль. Мой автомобиль, конечно, технически сложное устройство и сам я в + нём мало что починю. Но он и не настолько технически сложный, чтобы его не + починили в произвольном, даже неавторизованном, сервисном центре. При этом, у + него нет никакой зависимости от «облаков» и он полностью автономен в этом + отношении и не зависит от воли производителя. А насколько мне известно, в + стране более чем достаточно запасных частей и узлов для него. Так что, + длительная эксплуатация не будет проблемой. Тем более уже более семи лет + автомобилю и за это время он показал себя только с лучшей стороны. +- Все художественные книги у меня в формате fb2, который не подразумевает + поддержки DRM, а технические книги в формате PDF, который хоть и умеет вроде + как в DRM, но я с ним не сталкивался в своей библиотеке. И да, у меня именно + локальная библиотека продублированная на NAS и на резервный носитель. +- Аналогично, музыка. Да, хоть и в большинстве своём в формате mp3, а не ogg + (просто руки не доходят конвертнуть), но она именно локальная. На ноуте, + телефоне и на флешке для прослушивания в автомобиле. И что в случае с + музыкой, что в случае книг (а так же и сериалов и прочего) — я _точно_ знаю + что само по себе, а точнее по воле какого-то «правообладателя», никуда не + денется от меня. Точнее, сохранность лежит целиком в моих руках, а не чьих-то + ещё. +- Игры? Только те, что не подразумевают обязательной работы с интернетом, читай + «игр-сервисов». Это или старьё типа старых Fallout, или свободные игры типа + OpenTTD да Hedgewars. Так же у меня есть Nintendo Switch. Но у неё зарезан + интернет и играю исключительно с физических картриджей, которые никуда не + денутся. А чтобы не сломались — у меня есть [MigSwitch](https://migflash.ru/) + и дампер картриджей для него. +- Ноутбук? Тут сложнее. Я постарался взять самый ремонтопригодный ноутбук из + доступных мне, с самым большим запасом прочности, чтобы служил мне не менее + десятилетия. Но тут без гарантий. Поэтому, если критерий долгосрочной + доступности сервиса для вас критичен, можно рассмотреть местных + производителей, которые хотя бы не исчезнут с рынка в результате геополитики и + не оставят нас без сервисного обслуживания. Например, ICL. Сам не проверял, + это только мысли. +- E-mail. Самая здоровая технология для коммуникации. Во-первых, + децентрализованная, во-вторых, полностью открытая и свободная. В-третьих, + асинхронная и дающая мне возможность читать и отвечать на неё когда именно мне + удобно. А я такое очень ценю. Да, в современном мире чаты и синхронное общение + неизбежны, но я не делаю на них большую ставку и если, а точнее, когда + очередной мессенджер помрёт или будет заблокирован, для меня это не будет + трагедией, потому что я и не жду от них что они будут со мной всегда. Ну, + окей, jabber и irc — это тоже здоровые технологии, только ими почти никто не + пользуется. А для голосового общения есть свободные SIP да Mumble. Но ими тоже + пользуется полтора калеки. Вообще, про коммуникации, наверное, стоит будет + написать подробнее потом, тема очень обширная. +- Всегда, когда возможно, выбираю именно лицензированное под GPL. Не MIT или + Apache. А именно GPL. Но здесь, скорее вкусовщина. + +Примеров здоровых технологий меньше и они меньше описаны, но это сознательно. Я +собираюсь в дальнейшем, отдельными постами писать именно преимущественно о них, +а тему «нездоровых» касаться сильно меньше. Поэтому нездоровые расписал сразу, +чтобы больше особо не возвращаться. + +# Вместо заключения + +Надеюсь, я смог дать хотя бы примерное представление о том, что я считаю +«здоровыми» и «нездоровыми» технологиями. Ожидаю ли я что люди когда-нибудь +поумнеют и начнут использовать исключительно здоровые технологии? Да нет +конечно. Здесь каждый сам кузнец собственного счастья. + +Я хотел ещё много что написать как примеры здоровых и нездоровых технологий, но +тогда я бы пост бы не выпустил примерно никогда, так как тема бесконечная. + +Если есть желание, предлагаю обсудить со мной пост или по +[e-mail](mailto:i@neonxp.ru) или там, где вы можете достичь меня. Позже в этом +блоге, я всё же сделаю удобные комментарии, но это совсем другая история. + +А напоследок я хочу дать несколько ссылок в тему: + +- [Очень хороший набор примеров «вредных» и «менее вредных» вещей от Сергея + Матвеева](http://www.stargrave.org/Harmful.html). В принципе, это примерно те + же «нездоровые» и «здоровые» технологии. В целом, почти со всем я согласен, а + поэтому могу рекомендовать список как даже некоторое руководство по выбору + «здоровых» технологий. +- [Пост на французском про «низкие + технологии»](https://ploum.net/2025-05-16-manifeste-lowtech.html) — тоже на + очень близкую тему. + +Если есть схожие по теме материалы — не стесняйтесь советовать, тема для меня +интересная, с удовольствием ознакомлюсь. А сам её продолжу, возможно, уже на +следующей неделе! diff --git a/content/posts/2025-12-23-comments.md b/content/posts/2025-12-23-comments.md new file mode 100644 index 0000000..9a82612 --- /dev/null +++ b/content/posts/2025-12-23-comments.md @@ -0,0 +1,12 @@ +--- +comments: true +date: '2025-12-23T21:22:57+03:00' +tags: +- блог +title: Появились комментарии в блоге +--- + +Наконец-то сделал комментарии в этом блоге. Да, максимально по гиковски. Просто +через отправку e-mail на адрес специального бота. Потом, для удобства наверное +придумаю и другим способом. А может и так оставлю, ведь e-mail самый лучший и +самый универсальный способ связи, как ни крути. diff --git a/content/posts/2025-12-24-email.md b/content/posts/2025-12-24-email.md new file mode 100644 index 0000000..a4011d3 --- /dev/null +++ b/content/posts/2025-12-24-email.md @@ -0,0 +1,99 @@ +--- +comments: true +date: '2025-12-24T21:00:22+03:00' +tags: +- разное +- sicktech +title: Почему я люблю e-mail? +--- + +Я действительно очень люблю старую-добрую электронную почту. И вот почему. + +<!--more--> + +Во-первых, как и всё, что делалось в раннее время Интернета — e-mail достаточно +простая и открытая технология. А я очень ценю и то и то. В простой и открытой +технологии _возможно_ разобраться, в отличие от закрытых проприетарных систем. + +Во-вторых, она действительно децентрализованная, а значит отвечает духу того, +как проектировался Интернет ещё до того, как корпорации захватили его и +подчинили своей монополии. Если оглянуться шире — многие ранние технологии +интернета так же децентрализованные. Например, WWW, DNS, внезапно, IRC, XMPP... +Да много примеров. Ещё мне приходит в голову GIT, который спроектирован так, +что может работать поверх, практически, чего угодно! И да, в том числе, даже +поверх просто e-mail! + +В-третьих, она достаточно универсальна. По факту, e-mail это просто контейнер, +внутри которого можно положить практиески что угодно. Например, то что из себя +обычно представляет письмо — это или обычный текстовый файл (plaintext) или HTML +страничка (это более распространённые письма которые содержат богатую разметку). +Так же в том же письме могут быть приложены используемые в нём картинки, или, +например, файл содержащий приглашение на определённое событие в формате iCal. +И в последнем случае, наверняка, почтовая программа корректно обработает этот +файл и добавит событие в календарь. А так же, если пользователь решит принять +или отклонить приглашение, ответ уйдёт так же по e-mail. Что интересно, в какой +бы корпорации я не работал, именно так и работает система событий и календари. + +В-четвёртых, и это для меня самое главное. Почта, в отличие от всяких разных +мессенджеров, не предполагает синхронного общения! А это значит что? Что у меня +есть прорва времени чтобы _неторопясь_ в комфортном для меня режиме собраться с +мыслями, неторопясь аккуратно написать письмо. Проверить. Перепроверить. Ещё +подумать. И только после этого направить адресату. И выбросить из головы. + +Я человек в принципе неторопливый, и это меня устраивает гораздо больше, чем +незримое эмоциональное давление от осознания того, что мой собеседник _видит_ в +мессенджере что я уже прочитал его сообщение и ждёт ответа. А тянуть при этом и +задерживать человека очень не хочется. Это НЕ комфортно. + +Мне такое неторопливое общение кажется очень и очень уютным. По этой же причине, +мне так сильно импонирует вести обычный классический текстовый блог, когда более +модно вести условный телеграм канал. Канал то у меня тоже есть, но он или для +уведомлений о новых записях блога, или для совсем уж быстрых коротких заметок. + +Конечно, я не сумасшедший, и понимаю что есть множество вопросов, которые +действительно требуют и быстрого ответа и синхронного общения. Конечно же, в +таком случае использовать почту только потому что это почта — глупо и +нерационально. Здесь я не буду спорить. Всё так. + +Но общение — это гораздо более широкое понятие, и оно может быть _разным_. И +если есть возможность — я предпочту комфортное общение без психолгоического и +эмоционального давления. + +# Немного про другие коммуникации + +И да, в контексте, [предпредыдущего поста](/posts/2025-12-21-sicktech/), e-mail +— это однозначно «здоровая технология». Но не единственная. Раз уж немного +отклонился в сторону, приведу из этой же области ещё немного «здоровых» из +области коммуникаций: + +- [IRC](https://ru.ruwiki.ru/wiki/IRC) — радикальная простота протокола, + универсальность и распределённость. А в последнее время даже пытается ожить с + новой версией [IRCv3](https://ircv3.net/). +- [Jabber](https://ru.ruwiki.ru/wiki/XMPP) — уже не так просто, зато так же + открыто и децентрализованно. Так же, в последние годы обрёл второе дыхание и + развивается. +- [Mumble](https://ru.ruwiki.ru/wiki/Mumble) — свободный голосовой чат. Имеет + отличное качество звука при очень низкой задержке и потрелении трафика. + +Это далеко не исчерпывающий список, но он по факту покрывает основные +потребности в коммуникации. + +## И снова про e-mail и внезапная экономия там, где её не ждёшь + +Возращаясь к e-mail, я написал этот пост не как призыв всем всё бросить и писать +только письма, а только как напоминание, что такой инструмент есть (и есть по +факту почти у всех!) и нужно не забывать о нём и использовать тогда, когда он +уместен. + +Шутка про то, что «Эту часовую встречу можно было заменить просто одним емейлом» +— далеко не шутка, и очень часто так и есть. Таким образом, в некотором роде, +даже учитывая общий неторопливый стиль переписки — она, внезапно, может и помочь +сэкономить время! Не самый очевидный вывод, но так и есть. + +И да, постарайтесь не загаживать свой почтовый ящик ненужными автоматическими +сообщениями. Если не охото удалять то, что потенциально может пригодиться через +N лет — просто отправьте в архив. Разгребать свой почтовый ящик и, если нужно, +отвечать на письма — достаточно приятная рутина, которая отнимает не так уж +много времени. И да, весьма уютная и медитативная рутина, а результат, когда +непрочитанных писем нет, и письма аккуратно разложены по папкам или удалены — +приносит ощущение хорошо сделанного полезного дела. Попробуйте ;) diff --git a/content/posts/2025-12-27-osm.md b/content/posts/2025-12-27-osm.md new file mode 100644 index 0000000..1c9881c --- /dev/null +++ b/content/posts/2025-12-27-osm.md @@ -0,0 +1,40 @@ +--- +comments: true +cover: /posts/files/2025-12-27-osm_img/photo.jpg +date: '2025-12-27T19:42:04+03:00' +tags: +- прогулки +title: Сходили на ярмарку OSM +--- + +Сегодня с супругой съездили для интереса на ярмарку +[OSM](https://kzngo.ru/event/market-osm-14307) (нет, это не OpenStreetMaps, а +OpenSpaceMarket :) ) в гастрокомплексе «Кайт». Ярмарка не скажу что была сильно +большой, но нам понравилось. Купили всякого не сильно много, поднос да пару +ароматных свечек с зимними ароматами. Фотографии покупок да и самой прогулки +прилагаю. Всё таки у нас очень красивый город! + +P.S. на фотку можно кликнуть, тогда она откроется в большем размере. + +<!--more--> + +## Покупочки + +[](/posts/files/2025-12-27-osm_img/photo.jpg) + +## Прогулка + +[](/posts/files/2025-12-27-osm_img/photo_1.jpg) +[](/posts/files/2025-12-27-osm_img/photo_2.jpg) +[](/posts/files/2025-12-27-osm_img/photo_3.jpg) +[](/posts/files/2025-12-27-osm_img/photo_4.jpg) +[](/posts/files/2025-12-27-osm_img/photo_5.jpg) +[](/posts/files/2025-12-27-osm_img/photo_6.jpg) +[](/posts/files/2025-12-27-osm_img/photo_7.jpg) +[](/posts/files/2025-12-27-osm_img/photo_8.jpg) +[](/posts/files/2025-12-27-osm_img/photo_9.jpg) +[](/posts/files/2025-12-27-osm_img/photo_10.jpg) +[](/posts/files/2025-12-27-osm_img/photo_11.jpg) +[](/posts/files/2025-12-27-osm_img/photo_12.jpg) +[](/posts/files/2025-12-27-osm_img/photo_13.jpg) +[](/posts/files/2025-12-27-osm_img/photo_14.jpg) diff --git a/content/posts/2025-12-28-philharmonic-park.md b/content/posts/2025-12-28-philharmonic-park.md new file mode 100644 index 0000000..c692ecc --- /dev/null +++ b/content/posts/2025-12-28-philharmonic-park.md @@ -0,0 +1,32 @@ +--- +comments: true +cover: /posts/files/2025-12-28-philharmonic-park_img/photo_1_2025-12-28_21-35-40.jpg +date: '2025-12-28T21:37:44+03:00' +tags: +- прогулки +title: Прогулка в сквере филармонии +--- + +Вчера [гуляли с супругой на +набережной](https://neonxp.ru/posts/2025-12-27-osm/), а сегодня чисто случайно +решили прогуляться в сквере филармонии им.Тукая. Людей было немного, что только +создавало больше уюта и ламповости этому хорошо украшенному скверику. К +сожалению, гулять прям долго-долго особо не вышло, т.к. хоть и всего -7°, но +продрогли сильно, т.к. гулять изначально не планировали и не были одеты +соответственно. Сейчас дописываю этот пост и пытаюсь отогреться :) + +Как водится, дальше будет немного фотографий :) + +<!--more--> + +[](/posts/files/2025-12-28-philharmonic-park_img/photo_2_2025-12-28_21-35-40.png) +[](/posts/files/2025-12-28-philharmonic-park_img/photo_3_2025-12-28_21-35-40.png) +[](/posts/files/2025-12-28-philharmonic-park_img/photo_5_2025-12-28_21-35-40.jpg) +[](/posts/files/2025-12-28-philharmonic-park_img/photo_9_2025-12-28_21-35-40.jpg) +[](/posts/files/2025-12-28-philharmonic-park_img/photo_19_2025-12-28_21-35-40.jpg) +[](/posts/files/2025-12-28-philharmonic-park_img/photo_20_2025-12-28_21-35-40.jpg) +[](/posts/files/2025-12-28-philharmonic-park_img/photo_25_2025-12-28_21-35-40.jpg) +[](/posts/files/2025-12-28-philharmonic-park_img/photo_27_2025-12-28_21-35-40.jpg) +[](/posts/files/2025-12-28-philharmonic-park_img/photo_30_2025-12-28_21-35-40.jpg) +[](/posts/files/2025-12-28-philharmonic-park_img/photo_31_2025-12-28_21-35-40.jpg) +[](/posts/files/2025-12-28-philharmonic-park_img/photo_32_2025-12-28_21-35-40.png) diff --git a/content/posts/2025-12-29-newyear-excel.md b/content/posts/2025-12-29-newyear-excel.md new file mode 100644 index 0000000..76916b2 --- /dev/null +++ b/content/posts/2025-12-29-newyear-excel.md @@ -0,0 +1,22 @@ +--- +comments: true +cover: /posts/files/2025-12-29-newyear-excel_img/cover1.png +date: '2025-12-29T11:47:14+03:00' +tags: +- разное +title: Новогодний Excel +--- + +По мотивам одного недавнего поста с Пикабу, который мне уже лень искать. Немного +предновогоднего офискора вам. Открываем в Excel или в LibreOffice +соответствующий файл и жмём несколько раз клавишу F9. + +Делал я именно в LibreOffice, поэтому только там я точно уверен что работает как задуманно. + +- [Версия для MS Excel](/posts/files/2025-12-29-newyear-excel_img/new_year.xlsx) +- [Версия для LibreOffice + Calc](/posts/files/2025-12-29-newyear-excel_img/new_year.ods) +- [Онлайн версия в Документах + Mail.Ru](https://cloud.mail.ru/public/SF1M/ob3EeqtNF) + + diff --git a/content/posts/2025-12-31-new-year.md b/content/posts/2025-12-31-new-year.md new file mode 100644 index 0000000..7a3c3b0 --- /dev/null +++ b/content/posts/2025-12-31-new-year.md @@ -0,0 +1,28 @@ +--- +comments: true +cover: /posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_31_2025-12-28_21-35-40.jpg +date: '2025-12-31T15:27:17+03:00' +tags: +- разное +title: С Новым Годом! +--- + +С наступающим Новым Годом! + + + +Предыдущий год был весьма и весьма разным. Было и хорошее и плохое. Отчёт по +году как в [прошлом году](/posts/2024-12-31-new-year/) делать не буду. Честно +говоря, нет особо моральных сил на это. + +<!--more--> + +Уходящий год для меня стал годом чередования работы и семейных забот. В целом, +всё хорошо, но хотелось бы, чтобы следующий год был всё же проще, чего и всем +желаю! + +А так желаю всем сил, крепкого здоровья и больше удачи! Желаю чтобы беды +обходили ваш дом стороной. + +А пока, отправляюсь резать салатики и заниматься прочими домашними заботами, не +всё же у компьютера сидеть :) diff --git a/content/posts/2025-12-31-qchat.md b/content/posts/2025-12-31-qchat.md new file mode 100644 index 0000000..8344933 --- /dev/null +++ b/content/posts/2025-12-31-qchat.md @@ -0,0 +1,46 @@ +--- +comments: true +date: '2025-12-31T14:55:45+03:00' +tags: +- гиковское +- IT +title: qChat — чатик поверх SSH +--- + +Для собственного интереса написал на Go реализацию IRC-подобного чата, который +работает поверх прокола SSH. + +Умеет пока немного, но много я от него и не хотел. Основная идея в том, чтобы +его можно было запустить без конфигурации, с минимальными усилиями на любой +«картошке» и чтобы он могу обслуживать, например, небольшую группу пользователей +в одной локальной сети (хотя может работать и по интернету). + +Получилось очень гиковски и лампово. Так сказать, чатик на случай ядерной войны +:) + +<!--more--> + +Приглашаю забежать на огонёк, если умеете пользоваться SSH: + +``` +ssh neonxp.ru -p 1337 +``` + +Продублирую из README описание основных команд: + +- `/help` - эта справка. +- `/join [chan]` - подключиться к каналу [chan]. Если его нет, он будет создан. +- `/chans` - список каналов. +- `/users` - список пользователей на сервере (не на канале, а именно на + сервере). +- `/me [message]` - отправка сообщения как бы от третьего лица. + +Форматирование сообщений: + +- `*Полужирный*` +- `+Курсив+` +- `-Зачёркнутый текст-` +- `_Подчёркнутый текст_` + +Про техническое описание — есть отдельная страница: +[/projects/qchat/](/projects/qchat/) diff --git a/content/posts/_index.md b/content/posts/_index.md new file mode 100644 index 0000000..0fbcdff --- /dev/null +++ b/content/posts/_index.md @@ -0,0 +1,8 @@ +--- +title: Блог +cascade: + params: + comments: true +--- + +Просто мой блог diff --git a/content/posts/files/2021-02-13-jsonnet_logo.webp b/content/posts/files/2021-02-13-jsonnet_logo.webp Binary files differnew file mode 100644 index 0000000..45c63a6 --- /dev/null +++ b/content/posts/files/2021-02-13-jsonnet_logo.webp diff --git a/content/posts/files/2024-07-13-joplin_joplin.webp b/content/posts/files/2024-07-13-joplin_joplin.webp Binary files differnew file mode 100644 index 0000000..33326b7 --- /dev/null +++ b/content/posts/files/2024-07-13-joplin_joplin.webp diff --git a/content/posts/files/2024-10-06-цитатник-рунета_bash_org.webp b/content/posts/files/2024-10-06-цитатник-рунета_bash_org.webp Binary files differnew file mode 100644 index 0000000..e142633 --- /dev/null +++ b/content/posts/files/2024-10-06-цитатник-рунета_bash_org.webp diff --git a/content/posts/files/2024-10-17-книги-2_Rama16wiki.webp b/content/posts/files/2024-10-17-книги-2_Rama16wiki.webp Binary files differnew file mode 100644 index 0000000..b4d9ce9 --- /dev/null +++ b/content/posts/files/2024-10-17-книги-2_Rama16wiki.webp diff --git a/content/posts/files/2024-11-17-obsidian_img/logo.webp b/content/posts/files/2024-11-17-obsidian_img/logo.webp Binary files differnew file mode 100644 index 0000000..d5c747a --- /dev/null +++ b/content/posts/files/2024-11-17-obsidian_img/logo.webp diff --git a/content/posts/files/2024-11-17-obsidian_img/publish.webp b/content/posts/files/2024-11-17-obsidian_img/publish.webp Binary files differnew file mode 100644 index 0000000..f8add88 --- /dev/null +++ b/content/posts/files/2024-11-17-obsidian_img/publish.webp diff --git a/content/posts/files/2024-11-17-obsidian_img/templater.webp b/content/posts/files/2024-11-17-obsidian_img/templater.webp Binary files differnew file mode 100644 index 0000000..facdd86 --- /dev/null +++ b/content/posts/files/2024-11-17-obsidian_img/templater.webp diff --git a/content/posts/files/2024-11-27-hyperlocality_img/90e.webp b/content/posts/files/2024-11-27-hyperlocality_img/90e.webp Binary files differnew file mode 100644 index 0000000..948d808 --- /dev/null +++ b/content/posts/files/2024-11-27-hyperlocality_img/90e.webp diff --git a/content/posts/files/2024-11-27-hyperlocality_img/braindance.webp b/content/posts/files/2024-11-27-hyperlocality_img/braindance.webp Binary files differnew file mode 100644 index 0000000..965e145 --- /dev/null +++ b/content/posts/files/2024-11-27-hyperlocality_img/braindance.webp diff --git a/content/posts/files/2024-11-27-hyperlocality_img/camp.webp b/content/posts/files/2024-11-27-hyperlocality_img/camp.webp Binary files differnew file mode 100644 index 0000000..a07d8ed --- /dev/null +++ b/content/posts/files/2024-11-27-hyperlocality_img/camp.webp diff --git a/content/posts/files/2024-11-27-hyperlocality_img/in-internet.webp b/content/posts/files/2024-11-27-hyperlocality_img/in-internet.webp Binary files differnew file mode 100644 index 0000000..56d8c50 --- /dev/null +++ b/content/posts/files/2024-11-27-hyperlocality_img/in-internet.webp diff --git a/content/posts/files/2024-11-29-hobbies_dozor.webp b/content/posts/files/2024-11-29-hobbies_dozor.webp Binary files differnew file mode 100644 index 0000000..eab9913 --- /dev/null +++ b/content/posts/files/2024-11-29-hobbies_dozor.webp diff --git a/content/posts/files/2024-12-12-guessr_logo.webp b/content/posts/files/2024-12-12-guessr_logo.webp Binary files differnew file mode 100644 index 0000000..3a414cd --- /dev/null +++ b/content/posts/files/2024-12-12-guessr_logo.webp diff --git a/content/posts/files/2024-12-15-conditional-operator-go_ternary.webp b/content/posts/files/2024-12-15-conditional-operator-go_ternary.webp Binary files differnew file mode 100644 index 0000000..5eeea58 --- /dev/null +++ b/content/posts/files/2024-12-15-conditional-operator-go_ternary.webp diff --git a/content/posts/files/2024-12-15-posse_posse.webp b/content/posts/files/2024-12-15-posse_posse.webp Binary files differnew file mode 100644 index 0000000..aad6230 --- /dev/null +++ b/content/posts/files/2024-12-15-posse_posse.webp diff --git a/content/posts/files/2024-12-17-infra_cover.webp b/content/posts/files/2024-12-17-infra_cover.webp Binary files differnew file mode 100644 index 0000000..c02c8be --- /dev/null +++ b/content/posts/files/2024-12-17-infra_cover.webp diff --git a/content/posts/files/2024-12-30-irc_logo.webp b/content/posts/files/2024-12-30-irc_logo.webp Binary files differnew file mode 100644 index 0000000..26c4182 --- /dev/null +++ b/content/posts/files/2024-12-30-irc_logo.webp diff --git a/content/posts/files/2024-12-31-new-year_img/1.webp b/content/posts/files/2024-12-31-new-year_img/1.webp Binary files differnew file mode 100644 index 0000000..f320b83 --- /dev/null +++ b/content/posts/files/2024-12-31-new-year_img/1.webp diff --git a/content/posts/files/2024-12-31-new-year_img/2.webp b/content/posts/files/2024-12-31-new-year_img/2.webp Binary files differnew file mode 100644 index 0000000..49a1ed1 --- /dev/null +++ b/content/posts/files/2024-12-31-new-year_img/2.webp diff --git a/content/posts/files/2024-12-31-new-year_img/2025.webp b/content/posts/files/2024-12-31-new-year_img/2025.webp Binary files differnew file mode 100644 index 0000000..9cc3c0b --- /dev/null +++ b/content/posts/files/2024-12-31-new-year_img/2025.webp diff --git a/content/posts/files/2024-12-31-new-year_img/3.webp b/content/posts/files/2024-12-31-new-year_img/3.webp Binary files differnew file mode 100644 index 0000000..64dedc8 --- /dev/null +++ b/content/posts/files/2024-12-31-new-year_img/3.webp diff --git a/content/posts/files/2024-12-31-new-year_img/4.webp b/content/posts/files/2024-12-31-new-year_img/4.webp Binary files differnew file mode 100644 index 0000000..b5af0b5 --- /dev/null +++ b/content/posts/files/2024-12-31-new-year_img/4.webp diff --git a/content/posts/files/2024-12-31-new-year_img/5.webp b/content/posts/files/2024-12-31-new-year_img/5.webp Binary files differnew file mode 100644 index 0000000..86a8c2d --- /dev/null +++ b/content/posts/files/2024-12-31-new-year_img/5.webp diff --git a/content/posts/files/2025-12-21-img1.jpg b/content/posts/files/2025-12-21-img1.jpg Binary files differnew file mode 100644 index 0000000..2dd29f9 --- /dev/null +++ b/content/posts/files/2025-12-21-img1.jpg diff --git a/content/posts/files/2025-12-21-img2.jpg b/content/posts/files/2025-12-21-img2.jpg Binary files differnew file mode 100644 index 0000000..0ea8467 --- /dev/null +++ b/content/posts/files/2025-12-21-img2.jpg diff --git a/content/posts/files/2025-12-21-img3.png b/content/posts/files/2025-12-21-img3.png Binary files differnew file mode 100644 index 0000000..3e41caf --- /dev/null +++ b/content/posts/files/2025-12-21-img3.png diff --git a/content/posts/files/2025-12-21-sicktech.png b/content/posts/files/2025-12-21-sicktech.png Binary files differnew file mode 100644 index 0000000..32c7b9f --- /dev/null +++ b/content/posts/files/2025-12-21-sicktech.png diff --git a/content/posts/files/2025-12-27-osm_img/photo.jpg b/content/posts/files/2025-12-27-osm_img/photo.jpg Binary files differnew file mode 100644 index 0000000..4ea3f89 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/photo.jpg diff --git a/content/posts/files/2025-12-27-osm_img/photo_1.jpg b/content/posts/files/2025-12-27-osm_img/photo_1.jpg Binary files differnew file mode 100644 index 0000000..d14ed56 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/photo_1.jpg diff --git a/content/posts/files/2025-12-27-osm_img/photo_10.jpg b/content/posts/files/2025-12-27-osm_img/photo_10.jpg Binary files differnew file mode 100644 index 0000000..66a5727 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/photo_10.jpg diff --git a/content/posts/files/2025-12-27-osm_img/photo_11.jpg b/content/posts/files/2025-12-27-osm_img/photo_11.jpg Binary files differnew file mode 100644 index 0000000..aed7c5b --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/photo_11.jpg diff --git a/content/posts/files/2025-12-27-osm_img/photo_12.jpg b/content/posts/files/2025-12-27-osm_img/photo_12.jpg Binary files differnew file mode 100644 index 0000000..b78b33b --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/photo_12.jpg diff --git a/content/posts/files/2025-12-27-osm_img/photo_13.jpg b/content/posts/files/2025-12-27-osm_img/photo_13.jpg Binary files differnew file mode 100644 index 0000000..6bb6bd0 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/photo_13.jpg diff --git a/content/posts/files/2025-12-27-osm_img/photo_14.jpg b/content/posts/files/2025-12-27-osm_img/photo_14.jpg Binary files differnew file mode 100644 index 0000000..806471f --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/photo_14.jpg diff --git a/content/posts/files/2025-12-27-osm_img/photo_2.jpg b/content/posts/files/2025-12-27-osm_img/photo_2.jpg Binary files differnew file mode 100644 index 0000000..d4f788a --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/photo_2.jpg diff --git a/content/posts/files/2025-12-27-osm_img/photo_3.jpg b/content/posts/files/2025-12-27-osm_img/photo_3.jpg Binary files differnew file mode 100644 index 0000000..74ef585 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/photo_3.jpg diff --git a/content/posts/files/2025-12-27-osm_img/photo_4.jpg b/content/posts/files/2025-12-27-osm_img/photo_4.jpg Binary files differnew file mode 100644 index 0000000..ea5a5d0 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/photo_4.jpg diff --git a/content/posts/files/2025-12-27-osm_img/photo_5.jpg b/content/posts/files/2025-12-27-osm_img/photo_5.jpg Binary files differnew file mode 100644 index 0000000..462b570 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/photo_5.jpg diff --git a/content/posts/files/2025-12-27-osm_img/photo_6.jpg b/content/posts/files/2025-12-27-osm_img/photo_6.jpg Binary files differnew file mode 100644 index 0000000..253af73 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/photo_6.jpg diff --git a/content/posts/files/2025-12-27-osm_img/photo_7.jpg b/content/posts/files/2025-12-27-osm_img/photo_7.jpg Binary files differnew file mode 100644 index 0000000..cd92319 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/photo_7.jpg diff --git a/content/posts/files/2025-12-27-osm_img/photo_8.jpg b/content/posts/files/2025-12-27-osm_img/photo_8.jpg Binary files differnew file mode 100644 index 0000000..bebc4b3 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/photo_8.jpg diff --git a/content/posts/files/2025-12-27-osm_img/photo_9.jpg b/content/posts/files/2025-12-27-osm_img/photo_9.jpg Binary files differnew file mode 100644 index 0000000..8e4358b --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/photo_9.jpg diff --git a/content/posts/files/2025-12-27-osm_img/thumbs/photo.jpg b/content/posts/files/2025-12-27-osm_img/thumbs/photo.jpg Binary files differnew file mode 100644 index 0000000..ae1a7eb --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/thumbs/photo.jpg diff --git a/content/posts/files/2025-12-27-osm_img/thumbs/photo_1.jpg b/content/posts/files/2025-12-27-osm_img/thumbs/photo_1.jpg Binary files differnew file mode 100644 index 0000000..7d3df90 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/thumbs/photo_1.jpg diff --git a/content/posts/files/2025-12-27-osm_img/thumbs/photo_10.jpg b/content/posts/files/2025-12-27-osm_img/thumbs/photo_10.jpg Binary files differnew file mode 100644 index 0000000..ee69188 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/thumbs/photo_10.jpg diff --git a/content/posts/files/2025-12-27-osm_img/thumbs/photo_11.jpg b/content/posts/files/2025-12-27-osm_img/thumbs/photo_11.jpg Binary files differnew file mode 100644 index 0000000..72c288f --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/thumbs/photo_11.jpg diff --git a/content/posts/files/2025-12-27-osm_img/thumbs/photo_12.jpg b/content/posts/files/2025-12-27-osm_img/thumbs/photo_12.jpg Binary files differnew file mode 100644 index 0000000..8bbd32c --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/thumbs/photo_12.jpg diff --git a/content/posts/files/2025-12-27-osm_img/thumbs/photo_13.jpg b/content/posts/files/2025-12-27-osm_img/thumbs/photo_13.jpg Binary files differnew file mode 100644 index 0000000..1608d5b --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/thumbs/photo_13.jpg diff --git a/content/posts/files/2025-12-27-osm_img/thumbs/photo_14.jpg b/content/posts/files/2025-12-27-osm_img/thumbs/photo_14.jpg Binary files differnew file mode 100644 index 0000000..5cab91a --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/thumbs/photo_14.jpg diff --git a/content/posts/files/2025-12-27-osm_img/thumbs/photo_2.jpg b/content/posts/files/2025-12-27-osm_img/thumbs/photo_2.jpg Binary files differnew file mode 100644 index 0000000..870578b --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/thumbs/photo_2.jpg diff --git a/content/posts/files/2025-12-27-osm_img/thumbs/photo_3.jpg b/content/posts/files/2025-12-27-osm_img/thumbs/photo_3.jpg Binary files differnew file mode 100644 index 0000000..44b2637 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/thumbs/photo_3.jpg diff --git a/content/posts/files/2025-12-27-osm_img/thumbs/photo_4.jpg b/content/posts/files/2025-12-27-osm_img/thumbs/photo_4.jpg Binary files differnew file mode 100644 index 0000000..462b1bc --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/thumbs/photo_4.jpg diff --git a/content/posts/files/2025-12-27-osm_img/thumbs/photo_5.jpg b/content/posts/files/2025-12-27-osm_img/thumbs/photo_5.jpg Binary files differnew file mode 100644 index 0000000..4c9d1d6 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/thumbs/photo_5.jpg diff --git a/content/posts/files/2025-12-27-osm_img/thumbs/photo_6.jpg b/content/posts/files/2025-12-27-osm_img/thumbs/photo_6.jpg Binary files differnew file mode 100644 index 0000000..9ad1280 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/thumbs/photo_6.jpg diff --git a/content/posts/files/2025-12-27-osm_img/thumbs/photo_7.jpg b/content/posts/files/2025-12-27-osm_img/thumbs/photo_7.jpg Binary files differnew file mode 100644 index 0000000..7054792 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/thumbs/photo_7.jpg diff --git a/content/posts/files/2025-12-27-osm_img/thumbs/photo_8.jpg b/content/posts/files/2025-12-27-osm_img/thumbs/photo_8.jpg Binary files differnew file mode 100644 index 0000000..edd7f59 --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/thumbs/photo_8.jpg diff --git a/content/posts/files/2025-12-27-osm_img/thumbs/photo_9.jpg b/content/posts/files/2025-12-27-osm_img/thumbs/photo_9.jpg Binary files differnew file mode 100644 index 0000000..859bade --- /dev/null +++ b/content/posts/files/2025-12-27-osm_img/thumbs/photo_9.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/photo_19_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/photo_19_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..a7602ed --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/photo_19_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/photo_1_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/photo_1_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..d0ff6f2 --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/photo_1_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/photo_20_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/photo_20_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..7007409 --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/photo_20_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/photo_25_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/photo_25_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..17d8e06 --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/photo_25_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/photo_27_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/photo_27_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..87658fc --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/photo_27_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/photo_2_2025-12-28_21-35-40.png b/content/posts/files/2025-12-28-philharmonic-park_img/photo_2_2025-12-28_21-35-40.png Binary files differnew file mode 100644 index 0000000..de29bfb --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/photo_2_2025-12-28_21-35-40.png diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/photo_30_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/photo_30_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..e028728 --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/photo_30_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/photo_31_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/photo_31_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..0c83ba7 --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/photo_31_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/photo_32_2025-12-28_21-35-40.png b/content/posts/files/2025-12-28-philharmonic-park_img/photo_32_2025-12-28_21-35-40.png Binary files differnew file mode 100644 index 0000000..00eddcc --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/photo_32_2025-12-28_21-35-40.png diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/photo_3_2025-12-28_21-35-40.png b/content/posts/files/2025-12-28-philharmonic-park_img/photo_3_2025-12-28_21-35-40.png Binary files differnew file mode 100644 index 0000000..a01eae9 --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/photo_3_2025-12-28_21-35-40.png diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/photo_5_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/photo_5_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..615da4b --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/photo_5_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/photo_9_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/photo_9_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..69ad012 --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/photo_9_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_19_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_19_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..7768b6e --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_19_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_1_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_1_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..369d229 --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_1_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_20_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_20_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..35e1cd5 --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_20_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_25_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_25_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..ccb1005 --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_25_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_27_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_27_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..066be5c --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_27_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_2_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_2_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..ccab7b5 --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_2_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_30_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_30_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..71aebf0 --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_30_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_31_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_31_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..f9ef2da --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_31_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_32_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_32_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..381faff --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_32_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_3_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_3_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..f9e8a43 --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_3_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_5_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_5_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..302bb88 --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_5_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_9_2025-12-28_21-35-40.jpg b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_9_2025-12-28_21-35-40.jpg Binary files differnew file mode 100644 index 0000000..3c6b8f1 --- /dev/null +++ b/content/posts/files/2025-12-28-philharmonic-park_img/thumbs/photo_9_2025-12-28_21-35-40.jpg diff --git a/content/posts/files/2025-12-29-newyear-excel_img/cover.png b/content/posts/files/2025-12-29-newyear-excel_img/cover.png Binary files differnew file mode 100644 index 0000000..ae331d5 --- /dev/null +++ b/content/posts/files/2025-12-29-newyear-excel_img/cover.png diff --git a/content/posts/files/2025-12-29-newyear-excel_img/cover1.png b/content/posts/files/2025-12-29-newyear-excel_img/cover1.png Binary files differnew file mode 100644 index 0000000..b252f6e --- /dev/null +++ b/content/posts/files/2025-12-29-newyear-excel_img/cover1.png diff --git a/content/posts/files/2025-12-29-newyear-excel_img/new_year.ods b/content/posts/files/2025-12-29-newyear-excel_img/new_year.ods Binary files differnew file mode 100644 index 0000000..2d8aad2 --- /dev/null +++ b/content/posts/files/2025-12-29-newyear-excel_img/new_year.ods diff --git a/content/posts/files/2025-12-29-newyear-excel_img/new_year.xlsx b/content/posts/files/2025-12-29-newyear-excel_img/new_year.xlsx Binary files differnew file mode 100644 index 0000000..ca4469c --- /dev/null +++ b/content/posts/files/2025-12-29-newyear-excel_img/new_year.xlsx diff --git a/content/posts/files/2025-travel-1_img/1.webp b/content/posts/files/2025-travel-1_img/1.webp Binary files differnew file mode 100644 index 0000000..bc037f2 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/1.webp diff --git a/content/posts/files/2025-travel-1_img/10.webp b/content/posts/files/2025-travel-1_img/10.webp Binary files differnew file mode 100644 index 0000000..32bb46f --- /dev/null +++ b/content/posts/files/2025-travel-1_img/10.webp diff --git a/content/posts/files/2025-travel-1_img/11.webp b/content/posts/files/2025-travel-1_img/11.webp Binary files differnew file mode 100644 index 0000000..12f0933 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/11.webp diff --git a/content/posts/files/2025-travel-1_img/12.webp b/content/posts/files/2025-travel-1_img/12.webp Binary files differnew file mode 100644 index 0000000..c797bf3 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/12.webp diff --git a/content/posts/files/2025-travel-1_img/13.webp b/content/posts/files/2025-travel-1_img/13.webp Binary files differnew file mode 100644 index 0000000..96db2e5 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/13.webp diff --git a/content/posts/files/2025-travel-1_img/14.webp b/content/posts/files/2025-travel-1_img/14.webp Binary files differnew file mode 100644 index 0000000..66a53d8 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/14.webp diff --git a/content/posts/files/2025-travel-1_img/15.webp b/content/posts/files/2025-travel-1_img/15.webp Binary files differnew file mode 100644 index 0000000..a71672b --- /dev/null +++ b/content/posts/files/2025-travel-1_img/15.webp diff --git a/content/posts/files/2025-travel-1_img/16.webp b/content/posts/files/2025-travel-1_img/16.webp Binary files differnew file mode 100644 index 0000000..e690ba3 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/16.webp diff --git a/content/posts/files/2025-travel-1_img/17.webp b/content/posts/files/2025-travel-1_img/17.webp Binary files differnew file mode 100644 index 0000000..81fa05b --- /dev/null +++ b/content/posts/files/2025-travel-1_img/17.webp diff --git a/content/posts/files/2025-travel-1_img/18.webp b/content/posts/files/2025-travel-1_img/18.webp Binary files differnew file mode 100644 index 0000000..5905999 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/18.webp diff --git a/content/posts/files/2025-travel-1_img/19.webp b/content/posts/files/2025-travel-1_img/19.webp Binary files differnew file mode 100644 index 0000000..ab80bd9 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/19.webp diff --git a/content/posts/files/2025-travel-1_img/2.webp b/content/posts/files/2025-travel-1_img/2.webp Binary files differnew file mode 100644 index 0000000..b9ecc86 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/2.webp diff --git a/content/posts/files/2025-travel-1_img/20.webp b/content/posts/files/2025-travel-1_img/20.webp Binary files differnew file mode 100644 index 0000000..1cc4e35 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/20.webp diff --git a/content/posts/files/2025-travel-1_img/21.webp b/content/posts/files/2025-travel-1_img/21.webp Binary files differnew file mode 100644 index 0000000..0f3cc2c --- /dev/null +++ b/content/posts/files/2025-travel-1_img/21.webp diff --git a/content/posts/files/2025-travel-1_img/22.webp b/content/posts/files/2025-travel-1_img/22.webp Binary files differnew file mode 100644 index 0000000..b2d032d --- /dev/null +++ b/content/posts/files/2025-travel-1_img/22.webp diff --git a/content/posts/files/2025-travel-1_img/23.webp b/content/posts/files/2025-travel-1_img/23.webp Binary files differnew file mode 100644 index 0000000..00ad3f9 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/23.webp diff --git a/content/posts/files/2025-travel-1_img/3.webp b/content/posts/files/2025-travel-1_img/3.webp Binary files differnew file mode 100644 index 0000000..37ec362 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/3.webp diff --git a/content/posts/files/2025-travel-1_img/4.webp b/content/posts/files/2025-travel-1_img/4.webp Binary files differnew file mode 100644 index 0000000..dfb29bd --- /dev/null +++ b/content/posts/files/2025-travel-1_img/4.webp diff --git a/content/posts/files/2025-travel-1_img/5.webp b/content/posts/files/2025-travel-1_img/5.webp Binary files differnew file mode 100644 index 0000000..b5e1a8f --- /dev/null +++ b/content/posts/files/2025-travel-1_img/5.webp diff --git a/content/posts/files/2025-travel-1_img/6.webp b/content/posts/files/2025-travel-1_img/6.webp Binary files differnew file mode 100644 index 0000000..6b6bbca --- /dev/null +++ b/content/posts/files/2025-travel-1_img/6.webp diff --git a/content/posts/files/2025-travel-1_img/7.webp b/content/posts/files/2025-travel-1_img/7.webp Binary files differnew file mode 100644 index 0000000..fcce61f --- /dev/null +++ b/content/posts/files/2025-travel-1_img/7.webp diff --git a/content/posts/files/2025-travel-1_img/8.webp b/content/posts/files/2025-travel-1_img/8.webp Binary files differnew file mode 100644 index 0000000..b777d4c --- /dev/null +++ b/content/posts/files/2025-travel-1_img/8.webp diff --git a/content/posts/files/2025-travel-1_img/9.webp b/content/posts/files/2025-travel-1_img/9.webp Binary files differnew file mode 100644 index 0000000..f40de11 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/9.webp diff --git a/content/posts/files/2025-travel-1_img/preview_1.webp b/content/posts/files/2025-travel-1_img/preview_1.webp Binary files differnew file mode 100644 index 0000000..c18dc0c --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_1.webp diff --git a/content/posts/files/2025-travel-1_img/preview_10.webp b/content/posts/files/2025-travel-1_img/preview_10.webp Binary files differnew file mode 100644 index 0000000..4d23293 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_10.webp diff --git a/content/posts/files/2025-travel-1_img/preview_11.webp b/content/posts/files/2025-travel-1_img/preview_11.webp Binary files differnew file mode 100644 index 0000000..07e38b6 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_11.webp diff --git a/content/posts/files/2025-travel-1_img/preview_12.webp b/content/posts/files/2025-travel-1_img/preview_12.webp Binary files differnew file mode 100644 index 0000000..7a23c41 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_12.webp diff --git a/content/posts/files/2025-travel-1_img/preview_13.webp b/content/posts/files/2025-travel-1_img/preview_13.webp Binary files differnew file mode 100644 index 0000000..953fac9 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_13.webp diff --git a/content/posts/files/2025-travel-1_img/preview_14.webp b/content/posts/files/2025-travel-1_img/preview_14.webp Binary files differnew file mode 100644 index 0000000..08cb032 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_14.webp diff --git a/content/posts/files/2025-travel-1_img/preview_15.webp b/content/posts/files/2025-travel-1_img/preview_15.webp Binary files differnew file mode 100644 index 0000000..627702d --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_15.webp diff --git a/content/posts/files/2025-travel-1_img/preview_16.webp b/content/posts/files/2025-travel-1_img/preview_16.webp Binary files differnew file mode 100644 index 0000000..2bfa651 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_16.webp diff --git a/content/posts/files/2025-travel-1_img/preview_17.webp b/content/posts/files/2025-travel-1_img/preview_17.webp Binary files differnew file mode 100644 index 0000000..814ace2 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_17.webp diff --git a/content/posts/files/2025-travel-1_img/preview_18.webp b/content/posts/files/2025-travel-1_img/preview_18.webp Binary files differnew file mode 100644 index 0000000..82422a8 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_18.webp diff --git a/content/posts/files/2025-travel-1_img/preview_19.webp b/content/posts/files/2025-travel-1_img/preview_19.webp Binary files differnew file mode 100644 index 0000000..b234b0c --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_19.webp diff --git a/content/posts/files/2025-travel-1_img/preview_2.webp b/content/posts/files/2025-travel-1_img/preview_2.webp Binary files differnew file mode 100644 index 0000000..c730b03 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_2.webp diff --git a/content/posts/files/2025-travel-1_img/preview_20.webp b/content/posts/files/2025-travel-1_img/preview_20.webp Binary files differnew file mode 100644 index 0000000..266a83e --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_20.webp diff --git a/content/posts/files/2025-travel-1_img/preview_21.webp b/content/posts/files/2025-travel-1_img/preview_21.webp Binary files differnew file mode 100644 index 0000000..3cab02c --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_21.webp diff --git a/content/posts/files/2025-travel-1_img/preview_22.webp b/content/posts/files/2025-travel-1_img/preview_22.webp Binary files differnew file mode 100644 index 0000000..5530efa --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_22.webp diff --git a/content/posts/files/2025-travel-1_img/preview_23.webp b/content/posts/files/2025-travel-1_img/preview_23.webp Binary files differnew file mode 100644 index 0000000..aca8d32 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_23.webp diff --git a/content/posts/files/2025-travel-1_img/preview_3.webp b/content/posts/files/2025-travel-1_img/preview_3.webp Binary files differnew file mode 100644 index 0000000..511e4c4 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_3.webp diff --git a/content/posts/files/2025-travel-1_img/preview_4.webp b/content/posts/files/2025-travel-1_img/preview_4.webp Binary files differnew file mode 100644 index 0000000..bfbe20e --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_4.webp diff --git a/content/posts/files/2025-travel-1_img/preview_5.webp b/content/posts/files/2025-travel-1_img/preview_5.webp Binary files differnew file mode 100644 index 0000000..5d5811d --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_5.webp diff --git a/content/posts/files/2025-travel-1_img/preview_6.webp b/content/posts/files/2025-travel-1_img/preview_6.webp Binary files differnew file mode 100644 index 0000000..076e35b --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_6.webp diff --git a/content/posts/files/2025-travel-1_img/preview_7.webp b/content/posts/files/2025-travel-1_img/preview_7.webp Binary files differnew file mode 100644 index 0000000..02cd89a --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_7.webp diff --git a/content/posts/files/2025-travel-1_img/preview_8.webp b/content/posts/files/2025-travel-1_img/preview_8.webp Binary files differnew file mode 100644 index 0000000..4c87281 --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_8.webp diff --git a/content/posts/files/2025-travel-1_img/preview_9.webp b/content/posts/files/2025-travel-1_img/preview_9.webp Binary files differnew file mode 100644 index 0000000..3791fbb --- /dev/null +++ b/content/posts/files/2025-travel-1_img/preview_9.webp diff --git a/content/posts/files/2025-travel-1_img/saologo.png b/content/posts/files/2025-travel-1_img/saologo.png Binary files differnew file mode 100644 index 0000000..3fceeeb --- /dev/null +++ b/content/posts/files/2025-travel-1_img/saologo.png diff --git a/content/posts/files/hype_curve.png b/content/posts/files/hype_curve.png Binary files differnew file mode 100644 index 0000000..9147d5e --- /dev/null +++ b/content/posts/files/hype_curve.png diff --git a/content/posts/files/laughing-man.jpeg b/content/posts/files/laughing-man.jpeg Binary files differnew file mode 100644 index 0000000..652603c --- /dev/null +++ b/content/posts/files/laughing-man.jpeg diff --git a/content/posts/files/lets-code-3d_img/1.jpg b/content/posts/files/lets-code-3d_img/1.jpg Binary files differnew file mode 100644 index 0000000..87f41e7 --- /dev/null +++ b/content/posts/files/lets-code-3d_img/1.jpg diff --git a/content/posts/files/lets-code-3d_img/2.jpg b/content/posts/files/lets-code-3d_img/2.jpg Binary files differnew file mode 100644 index 0000000..db14947 --- /dev/null +++ b/content/posts/files/lets-code-3d_img/2.jpg diff --git a/content/posts/files/lets-code-3d_img/3.png b/content/posts/files/lets-code-3d_img/3.png Binary files differnew file mode 100644 index 0000000..9887043 --- /dev/null +++ b/content/posts/files/lets-code-3d_img/3.png diff --git a/content/posts/files/lets-code-3d_img/4.jpg b/content/posts/files/lets-code-3d_img/4.jpg Binary files differnew file mode 100644 index 0000000..4e7ebc7 --- /dev/null +++ b/content/posts/files/lets-code-3d_img/4.jpg diff --git a/content/posts/files/lets-code-3d_result.stl b/content/posts/files/lets-code-3d_result.stl Binary files differnew file mode 100644 index 0000000..fdd4d19 --- /dev/null +++ b/content/posts/files/lets-code-3d_result.stl diff --git a/content/posts/files/lets-code-3d_source.scad b/content/posts/files/lets-code-3d_source.scad new file mode 100644 index 0000000..b651cb3 --- /dev/null +++ b/content/posts/files/lets-code-3d_source.scad @@ -0,0 +1,66 @@ +// Толщина стенки +wall = 2; + +// Высота внешняя +height = 12; + +// Длина основной части (внутренняя) +l1 = 15; + +// Длина носика (внутренняя) +l2 = 20; + +// Ширина у основания (внутренняя) +w = 15; + +// Толщина метала бокорезов +toolWidth = 2.1; + +// Нижняя крышка +cover(0); + +// Верхняя крышка +cover(height - wall); + +// Корпус +difference() { + linear_extrude(height) + polygon( + [ // Полигон идёт против часовой стрелки + [0, 0], + [wall, 0], + [wall, l1], + [w / 2 + wall, l1 + l2], // Внутренний кончик носика + [w + wall, l1], + [w + wall, 0], + [w + wall * 2, 0], + [w + wall * 2, l1], + [w / 2 + wall / 2 + wall, l1 + l2 + wall], // Внешний кончик носика + [w / 2 - wall / 2 + wall, l1 + l2 + wall], + [0, l1], + ] + ); + + // Вырезы + translate([w + wall, 0, height / 2 - toolWidth]) // Правый вырез чуть ниже середины + cube([wall, l1 / 2, toolWidth]); + translate([0, 0, height / 2]) // Левый вырез чуть выше середины + cube([wall, l1 / 2, toolWidth]); +} + + +// Крышка +module cover(z) { + translate([0, 0, z]) + linear_extrude(wall) + polygon( + [ // Полигон идёт против часовой стрелки + [0, 0], + [w + wall * 2, 0], + [w + wall * 2, l1], + [w / 2 + wall / 2 + wall, l1 + l2 + wall], + [w / 2 - wall / 2 + wall, l1 + l2 + wall], + [0, l1], + ] + ); +} diff --git a/content/posts/files/makeup-organizer_img/1.png b/content/posts/files/makeup-organizer_img/1.png Binary files differnew file mode 100644 index 0000000..8f26704 --- /dev/null +++ b/content/posts/files/makeup-organizer_img/1.png diff --git a/content/posts/files/makeup-organizer_img/2.png b/content/posts/files/makeup-organizer_img/2.png Binary files differnew file mode 100644 index 0000000..6613744 --- /dev/null +++ b/content/posts/files/makeup-organizer_img/2.png diff --git a/content/posts/files/makeup-organizer_img/3.png b/content/posts/files/makeup-organizer_img/3.png Binary files differnew file mode 100644 index 0000000..993135e --- /dev/null +++ b/content/posts/files/makeup-organizer_img/3.png diff --git a/content/posts/files/makeup-organizer_organizer.tar.zst b/content/posts/files/makeup-organizer_organizer.tar.zst Binary files differnew file mode 100644 index 0000000..7e2e2bb --- /dev/null +++ b/content/posts/files/makeup-organizer_organizer.tar.zst diff --git a/content/posts/files/meshtastic_img/tbeam.jpg b/content/posts/files/meshtastic_img/tbeam.jpg Binary files differnew file mode 100644 index 0000000..214dc21 --- /dev/null +++ b/content/posts/files/meshtastic_img/tbeam.jpg diff --git a/content/posts/files/meshtastic_img/tbeam.webp b/content/posts/files/meshtastic_img/tbeam.webp Binary files differnew file mode 100644 index 0000000..7e89d94 --- /dev/null +++ b/content/posts/files/meshtastic_img/tbeam.webp diff --git a/content/projects/_index.md b/content/projects/_index.md new file mode 100644 index 0000000..71e1ed3 --- /dev/null +++ b/content/projects/_index.md @@ -0,0 +1,9 @@ +--- +order: "40" +title: Проекты +--- + +Разные мои проекты + +Ещё больше — на <a href="https://gitrepo.ru/NeonXP">git репозитории</a> +и на <a href="https://go.neonxp.ru/">моих go пакетах</a>. diff --git a/content/projects/games/bubblebreaker.p8.png b/content/projects/games/bubblebreaker.p8.png Binary files differnew file mode 100644 index 0000000..9bcf747 --- /dev/null +++ b/content/projects/games/bubblebreaker.p8.png diff --git a/content/projects/games/gameof15.p8.png b/content/projects/games/gameof15.p8.png Binary files differnew file mode 100644 index 0000000..5c9dfa0 --- /dev/null +++ b/content/projects/games/gameof15.p8.png diff --git a/content/projects/games/index.md b/content/projects/games/index.md new file mode 100644 index 0000000..4533a08 --- /dev/null +++ b/content/projects/games/index.md @@ -0,0 +1,30 @@ +--- +title: PICO-8 +--- + +Мои небольшие игрушки на прекрасном движке PICO-8 + +[](/projects/games/bubblebreaker.p8.png) +[](/projects/games/gameof15.p8.png) +[](/projects/games/lines.p8.png) +[](/projects/games/snake.p8.png) + +И на всякий случай дистрибутивы самих приложений. Ведь их больше не купить у нас в стране, а значит это не пиратство, а корсарство! Яррр! + +# PICO-8 + +| Linux | macOS | Windows | +|-------|-------|---------| +|[Скачать](/files/pico-8_0_2_6b_amd64.zip)|[Скачать](/files/pico-8_0_2_6b_osx.zip)|[Скачать](/files/pico-8_0_2_6b_windows.zip)| + +# Picotron + +| Linux | macOS | Windows | +|-------|-------|---------| +|[Скачать](/files/picotron_0_1_0g_amd64.zip)|[Скачать](/files/picotron_0_1_0g_osx.zip)|[Скачать](/files/picotron_0_1_0g_windows.zip)| + +# Voxatron + +| Linux | macOS | Windows | +|-------|-------|---------| +|[Скачать](/files/voxatron_0_3_5b_amd64.zip)|[Скачать](/files/voxatron_0_3_5b_osx.zip)|[Скачать](/files/voxatron_0_3_5b_windows.zip)| diff --git a/content/projects/games/lines.p8.png b/content/projects/games/lines.p8.png Binary files differnew file mode 100644 index 0000000..aa5ea39 --- /dev/null +++ b/content/projects/games/lines.p8.png diff --git a/content/projects/games/snake.p8.png b/content/projects/games/snake.p8.png Binary files differnew file mode 100644 index 0000000..14831dc --- /dev/null +++ b/content/projects/games/snake.p8.png diff --git a/content/projects/qchat.md b/content/projects/qchat.md new file mode 100644 index 0000000..ed3b9a0 --- /dev/null +++ b/content/projects/qchat.md @@ -0,0 +1,89 @@ +--- +title: 'qChat - quick chat' +--- + +Репозиторий: https://gitrepo.ru/NeonXP/qChat + +Очень маленький и минималистичный чат, который реализует собой чат поверх SSH. + +Внешних зависимостей нет, должен работать на любой картошке. + +Подключение к демонстрационному чату: + +``` +ssh neonxp.ru -p 1337 +``` + +## Установка и запуск + +Просто скачайте и запустите бинарник для соответствующей платформы. При первом +запуске в текущей рабочей директории будет создан конфиг файл с умолчальной +конфигурацией. При последующих запусках — будет он использоваться и не +пересоздаваться. + +В конфиге лежит приватный ключ! Его нужно хранить в секрете. Остальные параметры +там — дефолтный список каналов и название сервера. Их можно менять. + +### Ссылки для скачивания + +#### v0.0.2 [Исходники](https://gitrepo.ru/NeonXP/qChat/archive/v0.0.2.tar.gz) + +Готовые бинарники: + +- [Linux amd64](/files/qchat/v0.0.2/qchat-linux-amd64.tar.gz) +- [Linux arm64](/files/qchat/v0.0.2/qchat-linux-arm64.tar.gz) +- [Linux x86](/files/qchat/v0.0.2/qchat-linux-386.tar.gz) +- [Linux arm/v6](/files/qchat/v0.0.2/qchat-linux-arm-v6.tar.gz) +- [Linux arm/v7](/files/qchat/v0.0.2/qchat-linux-arm-v7.tar.gz) +- [macOS amd64](/files/qchat/v0.0.2/qchat-darwin-amd64.tar.gz) +- [macOS arm64](/files/qchat/v0.0.2/qchat-darwin-arm64.tar.gz) +- [Windows x32](/files/qchat/v0.0.2/qchat-windows-386.zip) +- [Windows x64](/files/qchat/v0.0.2/qchat-windows-amd64.zip) +- [dragonfly amd64](/files/qchat/v0.0.2/qchat-dragonfly-amd64.tar.gz) +- [FreeBSD amd64](/files/qchat/v0.0.2/qchat-freebsd-amd64.tar.gz) +- [FreeBSD arm64](/files/qchat/v0.0.2/qchat-freebsd-arm64.tar.gz) +- [NetBSD amd64](/files/qchat/v0.0.2/qchat-netbsd-amd64.tar.gz) +- [NetBSD arm64](/files/qchat/v0.0.2/qchat-netbsd-arm64.tar.gz) +- [OpenBSD amd64](/files/qchat/v0.0.2/qchat-openbsd-amd64.tar.gz) +- [OpenBSD arm64](/files/qchat/v0.0.2/qchat-openbsd-arm64.tar.gz) +- [Solaris amd64](/files/qchat/v0.0.2/qchat-solaris-amd64.tar.gz) + +## Установка с помощью Docker + +``` +docker volume create qchat_conf +docker run -d --name qchat -p 1337:1337 -v qchat_conf:/etc/qchat gitrepo.ru/neonxp/qchat /app/qchat -config /etc/qchat/config.json +``` + +## Подключение к чату + +Для подключения к чату достаточно стандартного клиента ssh. Во всех адекватных +ОС он есть из коробки. Для Windows - можно использовать Putty. + +``` +ssh [имя_пользователя@]хост -p 1337 +``` + +Например, при локально запущенном чате: + +``` +ssh localhost -p 1337 +``` + +Подойдёт любой эмулятор терминала совместимый с VT100. + +## Команды сервера + +Полную справку так же можно получить с помощью команды `/help`. + +- `/join [chan]` - подключиться к каналу [chan]. Если его нет, он будет создан. +- `/chans` - список каналов +- `/users` - список пользователей на сервере (не на канале, а именно на сервере) +- `/me [message]` - отправка сообщения как бы от третьего лица + +## Форматирование сообщений + +- `*Полужирный*` +- `+Курсив+` +- `-Зачёркнутый текст-` +- `_Подчёркнутый текст_` diff --git a/hugo.yaml b/hugo.yaml new file mode 100644 index 0000000..a0fc763 --- /dev/null +++ b/hugo.yaml @@ -0,0 +1,69 @@ +baseURL: https://neonxp.ru/ +languageCode: ru-ru +defaultContentLanguage: ru +title: ~/NeonXP.log +theme: neonxp +enableEmoji: true +menus: + main: + - name: Главная + pageRef: / + weight: 10 + - name: Блог + pageRef: /posts + weight: 20 + - name: Проекты + pageRef: /projects + weight: 30 + - name: Разное + pageRef: /pages + weight: 40 + - name: Go + url: https://go.neonxp.ru/ + weight: 50 + - name: Я + pageRef: /me + weight: 60 + - name: Atom + url: /feed/ + pre: <img src="/img/atom.svg" class="menu-icon" /> + weight: 70 +module: + hugoVersion: + extended: true + min: 0.146.0 +markup: + goldmark: + renderer: + unsafe: true +pagination: + disableAliases: false + pagerSize: 10 + path: page + +outputs: + home: + - html + section: + - html + - atom + page: + - html + taxonomy: + - html + term: + - html + +outputFormats: + atom: + mediaType: "application/atom+xml" + baseName: "atom" + isHTML: false + isPlainText: false + noUgly: true + rel: "alternate" + +mediaTypes: + application/atom+xml: + suffixes: + - xml diff --git a/static/files/0x96BF11A67E3C75F6.asc b/static/files/0x96BF11A67E3C75F6.asc new file mode 100644 index 0000000..8455e83 --- /dev/null +++ b/static/files/0x96BF11A67E3C75F6.asc @@ -0,0 +1,528 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZ51kmBYJKwYBBAHaRw8BAQdAWC8YWKkC1cJm0s5LbzrZJ9sKLgvCMBk/L9ph +AMAIb2S0KkFsZXhhbmRlciBLaXJ5dWtoaW4gKE5lb25YUCkgPGlAbmVvbnhwLnJ1 +PoiWBBMWCgA+AhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAFiEEnkkLvi8f +gskV+PRAlr8Rpn48dfYFAmf9lCMCGQEACgkQlr8Rpn48dfbHnwD/QIl35s9bMQe+ +d0DJ72p1jGbmP/J9EKNtnpFAvUAlnsUBAIwvUDupMmYwecYIe81txrCUQyrr3A1/ +6TNGtYo/ZC0K0f8AAF5q/wAAXmUBEAABAQAAAAAAAAAAAAAAAP/Y/+AAEEpGSUYA +AQEBABgAGAAA//4AHFNvZnR3YXJlOiB3d3cuaW5rc2NhcGUub3Jn/9sAQwADAgID +AgIDAwMDBAMDBAUIBQUEBAUKBwcGCAwKDAwLCgsLDQ4SEA0OEQ4LCxAWEBETFBUV +FQwPFxgWFBgSFBUU/9sAQwEDBAQFBAUJBQUJFA0LDRQUFBQUFBQUFBQUFBQUFBQU +FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU/8AAEQgBAAEAAwEiAAIR +AQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMC +BAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJ +ChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3 +eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS +09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAA +AAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEH +YXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVG +R0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKj +pKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX2 +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 +=mwxh +-----END PGP PUBLIC KEY BLOCK----- diff --git a/static/files/logo512.webp b/static/files/logo512.webp Binary files differnew file mode 100644 index 0000000..21d360f --- /dev/null +++ b/static/files/logo512.webp diff --git a/static/files/photo.webp b/static/files/photo.webp Binary files differnew file mode 100644 index 0000000..123ca99 --- /dev/null +++ b/static/files/photo.webp diff --git a/static/files/vcard.vcf b/static/files/vcard.vcf new file mode 100644 index 0000000..b6cd47c --- /dev/null +++ b/static/files/vcard.vcf @@ -0,0 +1,18 @@ +BEGIN:VCARD
+VERSION:3.0
+FN;CHARSET=UTF-8:Александр Кирюхин
+N;CHARSET=UTF-8:Кирюхин;Александр;;;
+NICKNAME;CHARSET=UTF-8:NeonXP
+GENDER:M
+BDAY:19890506
+EMAIL;CHARSET=UTF-8;type=HOME,INTERNET:i@neonxp.ru
+LOGO:https://neonxp.ru/img/logo512.webp
+ADR;CHARSET=UTF-8;TYPE=HOME:;;;Казань;Татарстан;;Российская Федерация
+ROLE;CHARSET=UTF-8:Главный разработчик
+ORG;CHARSET=UTF-8:Совкомбанк Технологии
+URL;CHARSET=UTF-8:https://neonxp.ru
+X-FEED;CHARSET=UTF-8:https://neonxp.ru/feed/
+SOURCE:https://neonxp.ru/files/vcard.vcf
+PHOTO:https://neonxp.ru/files/photo.webp
+REV:2025-06-14T01:00:00.000Z
+END:VCARD
diff --git a/static/img/atom.svg b/static/img/atom.svg new file mode 100644 index 0000000..1509b98 --- /dev/null +++ b/static/img/atom.svg @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg width="800px" height="800px" viewBox="0 0 44 44" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+
+ <title>RSS-color</title>
+ <desc>Created with Sketch.</desc>
+ <defs>
+
+</defs>
+ <g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Color-" transform="translate(-800.000000, -760.000000)" fill="#FF9A00">
+ <path d="M800.000471,797.714286 C800.000471,794.243 802.81487,791.428571 806.286118,791.428571 C809.757367,791.428571 812.571765,794.243 812.571765,797.714286 C812.571765,801.185571 809.757367,804 806.286118,804 C802.81487,804 800.000471,801.185571 800.000471,797.714286 Z M844,804 L835.619661,804 C835.619661,784.358714 819.641547,768.380429 800.000471,768.380429 L800.000471,760 C824.261497,760 844,779.738714 844,804 Z M829.333543,804 L820.953204,804 C820.953204,792.446857 811.553019,783.048143 800,783.048143 L800,774.666143 C816.174541,774.666143 829.333543,787.825286 829.333543,804 Z" id="RSS">
+
+</path>
+ </g>
+ </g>
+</svg>
\ No newline at end of file diff --git a/themes/neonxp/archetypes/default.md b/themes/neonxp/archetypes/default.md new file mode 100644 index 0000000..25b6752 --- /dev/null +++ b/themes/neonxp/archetypes/default.md @@ -0,0 +1,5 @@ ++++ +date = '{{ .Date }}' +draft = true +title = '{{ replace .File.ContentBaseName "-" " " | title }}' ++++ diff --git a/themes/neonxp/assets/css/main.css b/themes/neonxp/assets/css/main.css new file mode 100644 index 0000000..6778acd --- /dev/null +++ b/themes/neonxp/assets/css/main.css @@ -0,0 +1,183 @@ +:root { + --border-radius: 4px; + --shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +@media (prefers-color-scheme: dark) { + :root { + --shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + } +} + +nav { + margin-top: 1rem; +} + +nav ul { + display: flex; + padding: 0; + margin: 0; +} + +nav ul li { + list-style: none; +} + +nav ul li a { + padding: 0.5rem 1rem; + margin: 0 1rem; + border: 0.1px solid var(--link-color); + border-radius: var(--border-radius); + text-decoration: none; +} + +nav ul li a:hover { + background-color: color-mix(in srgb, var(--link-color) 10%, transparent); +} + +nav ul li:first-child a { + margin-left: 0; +} + +@media (max-width: 900px) { + nav ul { + display: block; + } + + + nav ul li a { + margin: 0rem 0rem !important; + display: block; + border-radius: 0; + } + + nav ul li:first-child a { + border-top-left-radius: var(--border-radius); + border-top-right-radius: var(--border-radius); + } + + nav ul li:last-child a { + border-bottom-left-radius: var(--border-radius); + border-bottom-right-radius: var(--border-radius); + } +} + +@media (max-width: 480px) { + + header, + footer { + padding: 1rem !important; + } + + nav ul { + display: block; + } + + nav ul li a { + margin: 0rem 0rem !important; + display: block; + border-radius: 0; + } + + nav ul li:first-child a { + border-top-left-radius: var(--border-radius); + border-top-right-radius: var(--border-radius); + } + + nav ul li:last-child a { + border-bottom-left-radius: var(--border-radius); + border-bottom-right-radius: var(--border-radius); + } +} + +ul.taxonomy { + display: flex; + padding: 0; + margin: 0.5rem 0; +} + +ul.taxonomy li { + list-style: none; +} + +ul.taxonomy li a { + padding: 0.5rem 1rem; + margin: 0 1rem; + border: 0.1px solid var(--link-color); + border-radius: var(--border-radius); + text-decoration: none; +} + +ul.taxonomy li a:hover { + background-color: color-mix(in srgb, var(--link-color) 10%, transparent); +} + +ul.taxonomy li:first-child a { + margin-left: 0; +} + + +ul.pagination { + display: flex; + padding: 0; + margin: 0.5rem 0; +} + +ul.pagination li { + list-style: none; +} + +ul.pagination li.active a { + background-color: color-mix(in srgb, var(--link-color) 10%, transparent); +} + +ul.pagination li a { + padding: 0.5rem 1rem; + margin: 0 1rem; + border: 0.1px solid var(--link-color); + border-radius: var(--border-radius); + text-decoration: none; +} + +ul.pagination li a:hover { + background-color: color-mix(in srgb, var(--link-color) 10%, transparent); +} + +ul.pagination li:first-child a { + margin-left: 0; +} + +pre { + border-radius: var(--border-radius); + border: 0.1px solid var(--border); + box-shadow: var(--shadow); + padding: 0.5rem; +} + +article { + border-radius: var(--border-radius); + box-shadow: var(--shadow); +} + +img { + border-radius: var(--border-radius); +} + +.menu-icon { + height: 16px; + width: 16px; + border-radius: 0; +} + +a.btn-primary { + display: inline-block; + padding: 0.5rem 1rem; + margin: 1rem 0; + border: 0.1px solid var(--link-color); + border-radius: var(--border-radius); + text-decoration: none; +} + +a.btn-primary:hover { + background-color: color-mix(in srgb, var(--link-color) 10%, transparent); +}
\ No newline at end of file diff --git a/themes/neonxp/assets/css/paper.css b/themes/neonxp/assets/css/paper.css new file mode 100644 index 0000000..91d9eef --- /dev/null +++ b/themes/neonxp/assets/css/paper.css @@ -0,0 +1,161 @@ +html, +body { + padding: 0px; + margin: 0px; + border: none; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + /* line-height: 1; */ +} + +:root { + --bg: #ffffff; + --surface: #f8f9fa; + --border: #e3e3e3; + --text: #2e2e2e; + --text-secondary: #6c757d; + --link-color: rgb(40, 117, 251); +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #1e1e1e; + --surface: #262626; + --border: #3a3a3a; + --text: #e0e0e0; + --text-secondary: #a8a8a8; + --shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + --link-color: rgb(103, 158, 254); + } +} + +@media print { + + *, + *:before, + *:after, + *:first-letter, + p:first-line, + div:first-line, + blockquote:first-line, + li:first-line { + background: transparent !important; + color: #000 !important; + box-shadow: none !important; + text-shadow: none !important; + } + + :root { + --main-color: #000; + --background-color: #fff; + --main-background-color: #fff; + } + + main { + font-size: 12pt !important; + line-height: 1.4 !important; + --background-color: #fff !important; + } + + header { + display: none; + } + + footer { + display: none; + } + + article { + page-break-after: always; + border: 0 !important; + padding: 0 !important; + } + + a[href] { + color: #000; + } +} + +html { + background-color: var(--bg); + display: flex; + justify-content: center; + flex-direction: row; +} + +body { + color: var(--text); + font-family: Arial, + Helvetica, + sans-serif; + line-height: 1.5; + width: 100%; + max-width: 1200px; +} + +a { + color: var(--link-color); +} + +header, +footer { + padding: 0 2rem; +} + +main { + padding: 0 2rem; +} + +article { + background-color: var(--surface); + padding: 1.5em 2em; + margin: 1em 0; + border: 0.1px solid var(--border); +} + + + +p { + orphans: 3; + widows: 4; +} + +img { + max-width: 100%; +} + +@media (max-width: 900px) { + main { + font-size: 15px; + line-height: 1.5; + padding: 0 2em; + } +} + +@media (max-width: 480px) { + main { + font-size: 14px; + line-height: 1.4; + padding: 0 1em; + } +} + + +pre, +code { + max-width: 100%; + overflow-x: scroll; +}
\ No newline at end of file diff --git a/themes/neonxp/hugo.yaml b/themes/neonxp/hugo.yaml new file mode 100644 index 0000000..4177323 --- /dev/null +++ b/themes/neonxp/hugo.yaml @@ -0,0 +1,18 @@ +baseURL: https://example.org/ +languageCode: en-US +title: My New Hugo Site +menus: + main: + - name: Home + pageRef: / + weight: 10 + - name: Posts + pageRef: /posts + weight: 20 + - name: Tags + pageRef: /tags + weight: 30 +module: + hugoVersion: + extended: false + min: 0.146.0 diff --git a/themes/neonxp/layouts/_default/section.atom.xml b/themes/neonxp/layouts/_default/section.atom.xml new file mode 100644 index 0000000..9ddb7d2 --- /dev/null +++ b/themes/neonxp/layouts/_default/section.atom.xml @@ -0,0 +1,23 @@ +{{ print "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>"| safeHTML }} +{{ print "<?xml-stylesheet type=\"text/css\" href=\"/css/atom.css\" ?>"| safeHTML }} +<feed xmlns="http://www.w3.org/2005/Atom"> + <title>{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}</title> + <link rel="self" href="{{ .Permalink }}"/> + <updated>{{ .Date.Format "2006-01-02T15:04:05-0700" | safeHTML }}</updated> + <author> + <name>Alexander NeonXP Kiryukhin</name> + <email>i@neonxp.ru</email> + <uri>https://neonxp.ru/</uri> + </author> + <id>{{ .Permalink }}</id> + {{ range first 15 .Data.Pages }} + <entry> + <title>{{ .Title }}</title> + <link rel="alternate" href="{{ .Permalink }}"/> + <id>{{ .Permalink }}</id> + <published>{{ .Date.Format "2006-01-02T15:04:05-0700" | safeHTML }}</published> + <updated>{{ .Lastmod.Format "2006-01-02T15:04:05-0700" | safeHTML }}</updated> + <summary>{{ .Content | html }}</summary> + </entry> + {{ end }} +</feed> diff --git a/themes/neonxp/layouts/_partials/footer.html b/themes/neonxp/layouts/_partials/footer.html new file mode 100644 index 0000000..2c30eaa --- /dev/null +++ b/themes/neonxp/layouts/_partials/footer.html @@ -0,0 +1 @@ +<p>© 2007 — {{ now.Year }} Александр NeonXP Кирюхин. <a href="mailto:i@neonxp.ru">i@neonxp.ru</a></p>
\ No newline at end of file diff --git a/themes/neonxp/layouts/_partials/head.html b/themes/neonxp/layouts/_partials/head.html new file mode 100644 index 0000000..7826e2a --- /dev/null +++ b/themes/neonxp/layouts/_partials/head.html @@ -0,0 +1,12 @@ +<meta charset="utf-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<link rel="icon" href="/favicon.ico" type="image/x-icon"> +{{ partial "opengraph.html" . }} +<title> + {{ if .IsHome }} + {{ site.Title }} + {{ else }} + {{ printf "%s | %s" .Title site.Title }} + {{ end }} +</title> +{{ partialCached "head/css.html" . }} {{ partialCached "head/js.html" . }}
\ No newline at end of file diff --git a/themes/neonxp/layouts/_partials/head/css.html b/themes/neonxp/layouts/_partials/head/css.html new file mode 100644 index 0000000..9324758 --- /dev/null +++ b/themes/neonxp/layouts/_partials/head/css.html @@ -0,0 +1,27 @@ +{{- with resources.Get "css/reset.css" }} +{{- if hugo.IsDevelopment }} +<link rel="stylesheet" href="{{ .RelPermalink }}"> +{{- else }} +{{- with . | minify | fingerprint }} +<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous"> +{{- end }} +{{- end }} +{{- end }} +{{- with resources.Get "css/paper.css" }} +{{- if hugo.IsDevelopment }} +<link rel="stylesheet" href="{{ .RelPermalink }}"> +{{- else }} +{{- with . | minify | fingerprint }} +<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous"> +{{- end }} +{{- end }} +{{- end }} +{{- with resources.Get "css/main.css" }} +{{- if hugo.IsDevelopment }} +<link rel="stylesheet" href="{{ .RelPermalink }}"> +{{- else }} +{{- with . | minify | fingerprint }} +<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous"> +{{- end }} +{{- end }} +{{- end }}
\ No newline at end of file diff --git a/themes/neonxp/layouts/_partials/head/js.html b/themes/neonxp/layouts/_partials/head/js.html new file mode 100644 index 0000000..1b7c18e --- /dev/null +++ b/themes/neonxp/layouts/_partials/head/js.html @@ -0,0 +1,16 @@ +<!-- {{- with resources.Get "js/main.js" }} + {{- $opts := dict + "minify" (not hugo.IsDevelopment) + "sourceMap" (cond hugo.IsDevelopment "external" "") + "targetPath" "js/main.js" + }} + {{- with . | js.Build $opts }} + {{- if hugo.IsDevelopment }} + <script src="{{ .RelPermalink }}"></script> + {{- else }} + {{- with . | fingerprint }} + <script src="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous"></script> + {{- end }} + {{- end }} + {{- end }} +{{- end }} -->
\ No newline at end of file diff --git a/themes/neonxp/layouts/_partials/header.html b/themes/neonxp/layouts/_partials/header.html new file mode 100644 index 0000000..7980a00 --- /dev/null +++ b/themes/neonxp/layouts/_partials/header.html @@ -0,0 +1,2 @@ +<h1>{{ site.Title }}</h1> +{{ partial "menu.html" (dict "menuID" "main" "page" .) }} diff --git a/themes/neonxp/layouts/_partials/menu.html b/themes/neonxp/layouts/_partials/menu.html new file mode 100644 index 0000000..ebad53f --- /dev/null +++ b/themes/neonxp/layouts/_partials/menu.html @@ -0,0 +1,50 @@ +{{- /* +Renders a menu for the given menu ID. + +@context {page} page The current page. +@context {string} menuID The menu ID. + +@example: {{ partial "menu.html" (dict "menuID" "main" "page" .) }} +*/}} + +{{- $page := .page }} +{{- $menuID := .menuID }} + +{{- with index site.Menus $menuID }} +<nav> + <ul> + {{- partial "inline/menu/walk.html" (dict "page" $page "menuEntries" .) }} + </ul> +</nav> +{{- end }} + +{{- define "_partials/inline/menu/walk.html" }} +{{- $page := .page }} +{{- range .menuEntries }} +{{- $attrs := dict "href" .URL }} +{{- if $page.IsMenuCurrent .Menu . }} +{{- $attrs = merge $attrs (dict "class" "active" "aria-current" "page") }} +{{- else if $page.HasMenuCurrent .Menu .}} +{{- $attrs = merge $attrs (dict "class" "ancestor" "aria-current" "true") }} +{{- end }} +{{- $name := .Name }} +{{- with .Identifier }} +{{- with T . }} +{{- $name = . }} +{{- end }} +{{- end }} +<li> + <a {{- range $k, $v :=$attrs }} {{- with $v }} {{- printf " %s=%q" $k $v | safeHTMLAttr }} {{- end }} {{- end -}}> + {{if .Pre}} + {{.Pre}} + {{end}} + {{ $name }} + </a> + {{- with .Children }} + <ul> + {{- partial "inline/menu/walk.html" (dict "page" $page "menuEntries" .) }} + </ul> + {{- end }} +</li> +{{- end }} +{{- end }}
\ No newline at end of file diff --git a/themes/neonxp/layouts/_partials/terms.html b/themes/neonxp/layouts/_partials/terms.html new file mode 100644 index 0000000..f826abb --- /dev/null +++ b/themes/neonxp/layouts/_partials/terms.html @@ -0,0 +1,27 @@ +{{- /* +For a given taxonomy, renders a list of terms assigned to the page. + +@context {page} page The current page. +@context {string} taxonomy The taxonomy. + +@example: {{ partial "terms.html" (dict "taxonomy" "tags" "page" .) }} +*/}} + +{{- $page := .page }} +{{- $taxonomy := .taxonomy }} + +{{- with $page.GetTerms $taxonomy }} +{{- if . }} +<article> + {{- $label := (index . 0).Parent.LinkTitle }} + <div> + <h3>{{ $label }}:</h3> + <ul class="taxonomy"> + {{- range . }} + <li><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></li> + {{- end }} + </ul> + </div> + {{- end }} +</article> +{{- end }}
\ No newline at end of file diff --git a/themes/neonxp/layouts/baseof.html b/themes/neonxp/layouts/baseof.html new file mode 100644 index 0000000..39dcbec --- /dev/null +++ b/themes/neonxp/layouts/baseof.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="{{ site.Language.LanguageCode }}" dir="{{ or site.Language.LanguageDirection `ltr` }}"> +<head> + {{ partial "head.html" . }} +</head> +<body> + <header> + {{ partial "header.html" . }} + </header> + <main> + {{ block "main" . }}{{ end }} + </main> + <footer> + {{ partial "footer.html" . }} + </footer> +</body> +</html> diff --git a/themes/neonxp/layouts/home.html b/themes/neonxp/layouts/home.html new file mode 100644 index 0000000..1f26b97 --- /dev/null +++ b/themes/neonxp/layouts/home.html @@ -0,0 +1,21 @@ +{{ define "main" }} +<article> + <h1>{{ .Title }}</h1> + {{ .Content }} +</article> +<div class="h-feed"> + {{ range .Pages }} + <article class="h-entry"> + <h2 class="p-name"><a class="u-url" href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2> + {{if .Date }} + {{ $dateMachine := .Date | time.Format "2006-01-02 15:04:05-07:00" }} + {{ $dateHuman := .Date | time.Format ":date_long" }} + <time class="dt-published" datetime="{{ $dateMachine }}">{{ $dateHuman }}</time> + {{end}} + <div class="p-summary"> + {{ .Summary }} + </div> + </article> + {{ end }} +</div> +{{ end }}
\ No newline at end of file diff --git a/themes/neonxp/layouts/page.html b/themes/neonxp/layouts/page.html new file mode 100644 index 0000000..506b56a --- /dev/null +++ b/themes/neonxp/layouts/page.html @@ -0,0 +1,37 @@ +{{ define "main" }} +<div class="h-entry"> + <article> + <h1 class="p-name">{{ .Title }}</h1> + {{if .Date }} + {{ $dateMachine := .Date | time.Format "2006-01-02T15:04:05-07:00" }} + {{ $dateHuman := .Date | time.Format ":date_long" }} + <time class="dt-published" datetime="{{ $dateMachine }}">{{ $dateHuman }}</time> + {{end}} + </article> + <article class="e-content"> + {{ .Content }} + </article> + {{ partial "terms.html" (dict "taxonomy" "tags" "page" .) }} + {{if .Param "comments"}} + <h2>Комментарии</h2> + {{ range $key, $comment := where .Site.Data.comments "url" "eq" .Page.Path }} + <article> + <b>{{$comment.from}}:</b> + <p>{{$comment.comment}}</p> + </article> + {{ else }} + <article>Комментариев пока нет.</article> + {{ end }} + + <article> + {{ $comment := (print "mailto:blog@neonxp.ru?subject=" .Page.Permalink | safeHTML) }} + Для отправки комментария достаточно отправить e-mail со своим комментарием + на адрес + <a href={{$comment}}>blog@neonxp.ru</a>, в теме нужно указать ссылку на + пост.<br /> + Или просто нажать кнопку ниже. Всё очень просто :)<br /> + <a class="btn-primary" href={{$comment}}>Написать комментарий</a> + </article> + {{end}} +</div> +{{ end }}
\ No newline at end of file diff --git a/themes/neonxp/layouts/section.html b/themes/neonxp/layouts/section.html new file mode 100644 index 0000000..a330f20 --- /dev/null +++ b/themes/neonxp/layouts/section.html @@ -0,0 +1,26 @@ +{{ define "main" }} +<article> + <h1>{{ .Title }}</h1> + {{ .Content }} +</article> +<div class="h-feed"> + + {{ range .Paginator.Pages }} + <article class="h-entry"> + <h2 class="p-name"><a class="u-url" href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2> + {{if .Date }} + {{ $dateMachine := .Date | time.Format "2006-01-02 15:04:05-07:00" }} + {{ $dateHuman := .Date | time.Format ":date_long" }} + <time class="dt-published" datetime="{{ $dateMachine }}">{{ $dateHuman }}</time> + {{end}} + <div class="p-summary"> + {{ .Summary }} + </div> + <a class="btn-primary" href="{{ .RelPermalink }}">Читать дальше...</a> + </article> + {{ end }} + <article> + {{ partial "pagination.html" . }} + </article> +</div> +{{ end }}
\ No newline at end of file diff --git a/themes/neonxp/layouts/taxonomy.html b/themes/neonxp/layouts/taxonomy.html new file mode 100644 index 0000000..ff8e552 --- /dev/null +++ b/themes/neonxp/layouts/taxonomy.html @@ -0,0 +1,11 @@ +{{ define "main" }} +<article> + <h1>{{ .Title }}</h1> + {{ .Content }} +</article> +<article> + {{ range .Pages }} + <h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2> + {{ end }} +</article> +{{ end }}
\ No newline at end of file diff --git a/themes/neonxp/layouts/term.html b/themes/neonxp/layouts/term.html new file mode 100644 index 0000000..c2e7875 --- /dev/null +++ b/themes/neonxp/layouts/term.html @@ -0,0 +1,7 @@ +{{ define "main" }} + <h1>{{ .Title }}</h1> + {{ .Content }} + {{ range .Pages }} + <h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2> + {{ end }} +{{ end }} diff --git a/themes/neonxp/static/favicon.ico b/themes/neonxp/static/favicon.ico Binary files differnew file mode 100644 index 0000000..da16cb8 --- /dev/null +++ b/themes/neonxp/static/favicon.ico |
