Skip to content

Techniques

TECHNIQUE 1 GNU/UNIX-style command-line arguments

Встроенный пакет flag несомненно полезен и обеспечивает самые необходимые возможности, но он не поддерживает предоставления флагов, которое ожидает найти большинство пользователей. Разница в стилях взаимодействия с пользователями в Plan 9 и Linux/BSD достаточно значительна, чтобы вызвать непонимание. В основном это связано с появлением проблем при попытке совместить короткие флаги, как в команде ls -la.

Проблема

Те, кто работает в системах, отличных от Windows, обычно используют один из вариантов UNIX и ожидают обработки флагов в стиле UNIX. Как написать на языке Go инструмент командной строки, соответствующий ожиданиям пользователей? Идеальным решением станет написание одноразовой специализированной обработки флагов.

Решение

Эту часто встречающуюся проблему можно решить несколькими способами. Некоторые приложения, такие как система управления контейнерами программного обеспечения Docker, имеют субпакеты, содержащие код для обработки флагов в стиле Linux. В некоторых случаях применяются разновидности пакета flag языка Go с дополнительными возможностями. Но реализация извлечения аргументов, отдельная для каждого проекта, требует достаточно большого дублирования усилий для многократного решения одной и той же проблемы.

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

TECHNIQUE 2 Avoiding CLI boilerplate code

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

Проблема

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

Решение

Если вам требуется удобный и полный набор возможностей для создания консольного приложения, обратите внимание на фреймворки, которые в дополнение к обработке флагов обеспечивают маршрутизацию команд, вывод справочного текста, поддержку подкоманд и функцию автозавершения в командной строке. Одним из самых популярных фреймворков для создания консольных приложений является cli. Он используется в таких проектах с открытым исходным кодом, как Cloud Foundry, система управления контейнерами Docker, система непрерывной интеграции и развертывания Drone.

TECHNIQUE 3 Using configuration files

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

Проблема

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

Решение

Одним из самых популярных форматов конфигурационных файлов в настоящее время является формат JavaScript Object Notation (JSON). Стандартная библиотека Go включает средства анализа и обработки данных в этом формате. И это неудивительно, потому что файлы в формате JSON используются очень широко.

Решение

Рекурсивный акроним YAML расшифровывается как YAML Ain’t Markup Language (YAML – не язык разметки) и обозначает удобочитаемый формат сериализации данных. Формат YAML легко читается, может содержать комментарии, и с ним достаточно просто работать. Формат YAML широко используется для хранения конфигураций приложений, и мы, как авторы, рекомендуем его. В языке Go отсутствует встроенный YAML-процессор, но существует несколько доступных сторонних библиотек, и одна из них рассматривается далее.

Решение

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

TECHNIQUE 4 Configuration via environment variables

Конфигурационные файлы, безусловно, представляют собой отличное транспортное средство для передачи конфигурационных данных в программы. Но некоторые новые окружения не поддерживают предположений, которые мы делаем, когда используем традиционные конфигурационные файлы. Иногда приложение не имеет доступа к файловой системе на требуемом уровне. Некоторые системы полагают, что файлы конфигурации являются частью исходного кода (и считают их статической частью выполняемого файла). Это ограничивает возможности использования конфигурационных файлов.

Четче всего эта тенденция проявляется в новых системах PaaS облачных служб. Развертывание в этих системах обычно осуществляется путем помещения пакета исходного кода на управляющий сервер (подобно сохранению в Git). И единственным способом настройки приложений на таких серверах является получение данных из переменных окружения. Рассмотрим приемы работы с такими переменными.

Проблема

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

Решение

12-факторные приложения, развертываемые в Heroku, Cloud Foundry, Deis и других платформах PaaS или системах управления контейнерами кластеров (рассматриваются в главе 11), становятся все более обычным явлением. И одним из двенадцати факторов является хранение конфигурационных данных в переменных окружения. Это дает возможность определять отдельные конфигурации для каждого окружения, в котором выполняется приложение.

TECHNIQUE 5 Graceful shutdowns using manners

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

Проблема

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

Решение

Для решения этой задачи необходимо реализовать собственную логику или использовать сторонний пакет, например https://github.com/braintree/manners. Начиная с версии Go 1.18, пакет http поддерживает Graceful shutdown из коробки.

TECHNIQUE 6 Matching paths to content

Веб-приложения и серверы, поддерживающие интерфейс REST, обычно выполняют разные функции для разных путей. Ниже показан фрагмент URL-адреса, представляющий путь. Пример «Hello World», использует одну функцию для обработки всех возможных путей. Для простого приложения в стиле «Hello World» этого вполне достаточно. Но одна функция не может нормально обрабатывать несколько путей и не обеспечивает масштабирования в реальных приложениях. В этом разделе рассматривается несколько методов обработки отличающихся путей, а в некоторых случаях и отличающихся HTTP-методов (иногда называемых глаголами).

Проблема

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

.Фрагмент пути в URL-адресе, используемой для маршрутизации запросов

https://example.com/foo#bar?baz=quo

Решение: несколько обработчиков

Данное решение использует функции-обработчики для каждого из путей. Оно было представлено в руководстве «Writing Web Applications») и реализует шаблон, подходящий для веб-приложений с несколькими простыми путями. Такой подход имеет определенные ограничения (о них вы узнаете чуть ниже), которые могут вынудить вас использовать другой прием, описываемый вслед за этим.

TECHNIQUE 7 Handling complex paths with wildcards

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

Проблема

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

Решение

В стандартной библиотеке Go имеется пакет path для обработки путей, где символы slash используются как разделители. Этот пакет не имеет прямого отношения к путям URL и прекрасно поддерживает любые другие пути, но его на удивление удобно использовать в паре с HTTP-обработчиком.

TECHNIQUE 8 URL pattern matching

Для большинства приложений в стиле REST простого сопоставления с регулярными выражениями более чем достаточно. Но как поступить, если требуется пойти дальше и предусмотреть какую-то особенную обработку URL-адресов? Пакет path для этого не подойдет, поскольку поддерживает только простое сопоставление в стиле POSIX.

Проблема

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

Решение

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

TECHNIQUE 9 Faster routing (without the work)

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

Проблема

Встроенный пакет http недостаточно гибок, и его применение оправдано далеко не всегда.

Решение

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

Ниже приводится список наиболее популярных решений:

  • https://github.com/julienschmidt/httprouter – быстрый пакет маршрутизации с акцентом на использование минимального объема памяти и как можно меньшего времени на обработку маршрутов. Поддерживает сопоставление путей без учета регистра символов,исключение фрагментов /../ из путей и обрабатывает необязательные завершающие символы /;
  • https://github.com/gorilla/mux – часть комплекта веб-инструментов Gorilla. Это бесплатная коллекция пакетов компонентов, которые можно использовать в приложении. Пакет mux реализует универсальный набор критериев для сопоставления, включая хост, схемы, HTTP-заголовки и многое другое;
  • https://github.com/bmizerany/pat – маршрутизатор, действующий подобно механизму маршрутизации из библиотеки Sinatra. Зарегистрированные пути удобно просматривать, и они могут содержать именованные параметры, например /user/:name. Имеются и другие пакеты с такими же возможностями, такие как https://github.com/gorilla/pat.

TECHNIQUE 10 Using goroutine closures

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

Проблема

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

Решение

Используем замыкание функции и дадим планировщику шанс сделать все остальное.

TECHNIQUE 11 Waiting for goroutines

Иногда требуется вызвать несколько сопрограмм и дождаться, когда все они завершат работу. Проще всего достичь нужного результата позволяет формирование wait groups (группы ожидания).

Проблема

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

Решение

Запустим отдельные задания как сопрограммы. Используем функцию sync.WaitGroup, чтобы известить внешний процесс, что все сопрограммы завершились и можно продолжить выполнение. На рис. 3.1 приведена обобщенная схема: запуск нескольких рабочих процессов и передача им заданий. Один из процессов передает задания рабочим процессам и ждет, пока они закончатся.

TECHNIQUE 12 Locking with a mutex

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

Проблема

Нескольким сопрограммам необходим доступ на чтение и изменение одного и того же фрагмента данных.

Решение

Один из самых простых способов избежать состояния гонки – дать каждой сопрограмме возможность lock (заблокировать) ресурс перед использованием и разблокировать после выполнения операций. Все прочие сопрограммы, столкнувшиеся с блокировкой, будут вынуждены ждать снятия блокировки, перед тем как самим заблокировать ресурс. Для блокировки и разблокировки объекта используется функция sync.Mutex.

TECHNIQUE 13 Using multiple channels

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

Проблема

Использовать каналы для передачи данных между сопрограммами и получить возможность прерывать этот процесс при выходе.

Решение

Используем оператор select и несколько каналов. В языке Go каналы часто используются для передачи сигнала, когда завершилось выполнение какого-то задания или все готово к закрытию.

TECHNIQUE 14 Closing channels

Разработчики на языке Go перекладывают обязанность по уборке мусора на диспетчера памяти. Когда переменная выходит из области видимости, связанная с ней память автоматически освобождается. Но при работе с сопрограммами и каналами следует проявлять повышенную осторожность. Что произойдет, если отправителем и получателем данных являются сопрограммы и отправитель завершит отправку данных? Будут ли автоматически освобождены получатель и канал? Нет. Диспетчер памяти освобождает только значения, которые гарантированно не будут больше использоваться, к открытым каналам и сопрограммам это не относится.

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

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

Проблема

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

Решение

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

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

TECHNIQUE 15 Locking with buffered channels

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

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

Проблема

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

Решение

Используем канал с размером буфера 1 и обеспечим совместное использование этого канала сопрограммами, которые требуется синхронизировать.

TECHNIQUE 16 Minimize the nils

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

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

Проблема

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

Решение

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

TECHNIQUE 17 Custom error types

Тип данных error в языке Go – это интерфейс, определение которого приводится в следующем листинге.

type error interface {
    Error() string
}

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

Проблема

Функция возвращает ошибку. Детальные сведения о ней играют важную роль и определяют порядок дальнейшей обработки.

Решение

Создадим тип данных, реализующий интерфейс error и предоставляющий дополнительные возможности.

TECHNIQUE 18 Error variables

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

Проблема

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

Решение

В языке Go (в отличие от других языков программирования) хорошей практикой считается создавать переменные для хранения ошибок на уровне пакетов, которые можно возвращать при возникновении определенных ошибок. Отличным примером может служить пакет io из стандартной библиотеки языка Go, содержащий такие ошибки, как io.EOF и io.ErrNoProgress.

TECHNIQUE 19 Issuing panics

Определение функции, возбуждающей panic, можно выразить так: panic(interface{}). В вызов функции panic можно передать практически любой аргумент. При желании ее можно вызвать с аргументом nil.

.Example for output

panic: nil
goroutine 1 [running]:
main.main()
    /Users/mbutcher/Code/go-in-practice/chapter4/proper_panic.go:4 +0x32

Данная ошибка не несет никакой полезной нагрузки. Аварию можно вызвать со строкой: panic("Oops,Ididitagain.").

.Example for output

panic: Oops, I did it again.
goroutine 1 [running]:
main.main()
    /Users/mbutcher/Code/go-in-practice/chapter4/proper_panic.go:4 +0x64

Уже лучше. По крайней мере, имеется хоть какая-то полезная информация. Но правильно ли так делать?

Проблема

Что следует передавать функции panic в качестве аргумента? Существуют ли полезные или идиоматически общепринятые способы возбуждения аварий?

Решение

Лучше всего в функцию panic передавать ошибки. Используйте тот тип ошибок, который упростит восстановление (если предусмотрено).

TECHNIQUE 20 Recovering from panics

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

Проблема

Panic в одной из функций приводит к краху всей программы.

Решение

Используем отложенную функцию и вызовем функцию recover, чтобы выяснить причину и обработать аварию.

TECHNIQUE 21 Trapping panics on goroutines

Когда возникает panic, среда выполнения Go раскручивает стек функции, пока не столкнется с вызовом recover. Но если обход завершится на вершине стека и функция recover нигде не будет вызвана, программа прервет работу.

Проблема

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

Решение

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