Представленный ниже шаблон сервера на Golang был подготовлен для передачи знаний внутри нашей команды. Основная цель шаблона, кроме обучения — это снизить время на прототипирование небольших серверных задач на Go. Шаблон включает: Вторая часть посвящена прототипированию REST API. Третья часть посвящена развертыванию шаблона в Docker, Docker Compose, Kubernetes (kustomize). Пятая часть посвящена оптимизации Worker pool и особенностям его работы в составе микросервиса, развернутого в Kubernetes. Ссылка на репозиторий проекта.  В шаблон включены HTTP обработчики:  В ходе внедрения 1С:ERP, появилась интересная задача — интеграция 1С с шиной IBM MQ.
Ключевыми требованиями в части взаимодействия с IBM MQ были: Дополнительно были выдвинуты требования к обработке XML сообщений: Стандартного адаптера в 1C к IBM MQ не было. Существующий REST API к IBM MQ не подходил под требования. Адаптер 1C к IBM MQ был успешно разработан на Go с использованием официальной библиотеки IBM MQ. Библиотека отличается неплохой стабильностью, что и не удивительно, так как она написана в виде "обертки" над стандартной C библиотекой. За полгода работы с ней было зафиксировано всего 2 бага с обработкой слайсов []. Представленный в статье шаблон backend сервера, является обобщением полученного опыта. Архитектура адаптера 1С к IBM MQ укрупнено показана на следующем рисунке.   Все чувствительные, с точки зрения безопасности, параметры сервера передаются через командную строку. Для этого используется библиотека github.com/urfave/cli. Список основных параметров: Для обработки конфигурационного файла используется библиотека github.com/sasbury/mini. Список типовых параметров, включенных в шаблон: На следующем рисунке показана упрощенная UML диаграмма последовательности запуска и остановки сервера.  Для координации создания, запуска и остановки сервера используется daemon. В его задачи входит:  В общем случае, сервисы создаются и настраиваются при создании daemon.
Если есть ошибки при создании отдельных сервисах, то daemon не создается.
Если сервис предназначен для работы в фоне, то в daemon для него создается отдельный канал ошибок: При создание сервиса, ему передаются параметры: Примеры задач при создании сервисов: Запуск daemon заключается в скоординированном запуске сервисов.
Так как все сервисы уже были созданы ранее, то запуск сервиса — это, обычно, включение листенера или установление флага, разрешающего начать обработку.
Для запуска сервисов в фоне, используется анонимная функция с возвратом в канал ошибок. Пример, запуска HTTP сервера После запуска сервисов, daemon подписывается на основные системные прерывания и переходит в режим ожидания сигналов или возврата в каналы ошибок от сервисов.
Получении daemon ошибки от сервиса, означает, что какой-то из сервисов не может продолжить работу. Здесь логика обработки может существенно отличаться, от полной остановки (как в примере ниже) до перезапуска сбойного сервиса.
Например, если основной сервер IBM MQ становится недоступен, то daemon пробует пересоздать сервис на резервном сервере IBM MQ и продолжить обработку. В запуск сервисов, работающих в фоне, добавляется анонимная функция восстановления после паники (пример ниже). При обработке паники, ошибка возвращается в канал ошибок для уведомления daemon. Остановка daemon заключается в скоординированной остановке сервисов и последующем закрытии корневого контекста. Остановка сервисов, работающих в фоне, осуществляется по следующему сценарию: Для корректной остановки сервисов при закрытии контекста, используется такой подход: Для остановки HTTP сервера использовался несколько другой подход: Один из наиболее удачных пакетов для обработки ошибок github.com/pkg/errors.
Первоначально использовал его, но со временем стало не хватать структурности ошибки, поэтому перешел на простой кастомный пакет. Структура для хранения ошибки: Мне удобно работать с типизированными ошибками, поэтому код ошибки выделен отдельным атрибутом. Например, в адаптере 1С к IBM MQ, использовался простой 4 символьный числовой код. Например, ошибки начинающиеся с "8ххх" относились к HTTP, с "7ххх" — к IBM MQ. Caller — файл, строка и наименование метода в месте регистрации ошибки. Удобно использовать, если нет необходимости выводить полный стек. Пример вывода: Caller вычисляется функцией Args — отдельная строка аргументов, которые можно добавить к сообщению при регистрации ошибки. Используется для целей отладки. CauseErr и CauseMsg — исходная ошибка и сообщение. Используется, если оборачиваем чужую ошибку в свою структуру. Trace — стандартный трейс стека. Для его получения использовал несколько своеобразный подход. При регистрации ошибки создавал дополнительно пустую ошибку из пакета github.com/pkg/errors и печатал ее с ключом '%+v'. В этом режиме она выводит стек. Для соответствия интерфейсу Error, используется вывод в сокращенном формате. Пример вывода в сокращенном формате Для расширенного форматированного вывода используются ключи Пример вывода с ключом '%+v' Предложенный формат вывода не полностью соответствует подходу структурированного логирования. Это сделано специально для более удобного чтения лога в ходе отладки.
Если нужен боле строгий формат — достаточно поправить в одном месте метод Format. Используются два метода для регистрации ошибок: Дополнительные аргументы можно либо встроить в сообщение ошибки, либо передать дополнительными параметрами в args ...interface{}. Все ошибки от сторонних и стандартных пакетов оборачиваются в кастомную ошибку в месте возникновения. Исходная ошибка вкладывается внутрь кастомной, например: Использовался следующий подход: Пример логирования при ошибке чтения тела HTTP запроса: Методом processError дополнительно ошибка может логироваться в заголовок HTTP ответа и/или тело ответа.
Необходимость такого логирования настраивается в конфигурационном файле сервера — параметр HTTPErrLog, или динамически вызовом POST на /httperrlog. При записи многострочного текста в HTTP header нужно не забывать исключать все управляющие символы Для управления уровнями логирования использовался пакет github.com/hashicorp/logutils.
Из рекомендованного списка уровней логирования RFC 5424 — The Syslog Protocol, в шаблоне оставил только: Кастомный пакет логирования получился крайне простым — всего 100 строк Ссылка на полезную статью от Dave Cheney Let’s talk about logging.
Если предложенный вариант логирования покажется слишком простым — то рекомендую посмотреть в сторону A simple logging interface for Go. Все логирование идет через стандартный пакет "log". Но для него подменяется вывод на кастомный логер github.com/hashicorp/logutils. При необходимости, можно параллельно логировать в файл, для этого на уровне main создается лог файл и устанавливается MultiWriter Логирование в режиме INFO и DEBUG сделано немного более дружественным чем логирование ошибок. Пример ниже В режиме ERROR используется формат вывода из раздела выше про обработку ошибок. Для каждого уровня логирования сделаны отдельные функции: Для меня это удобнее, чем в каждой точке логирования указывать необходимый уровень.
Еще один плюс — легко централизовано отключить вывод DEBUG сообщений при переходе в продуктив. Чтобы снизить затраты на логирование INFO и DEBUG, лучше отказаться от форматирования строк в точке вызова. Вместо лучше использовать Тесты показали, что при использовании такого подхода, накладные расходы на вызовы логирования DEBUG в режиме INFO не превысили 1%.
В этом есть существенный положительный момент — можно динамически изменять уровень с INFO на DEBUG без остановки сервера вызовом POST на /loglevel. Для анализа сложных ситуаций, полезно иметь возможность логирования HTTP трафика проходящего через сервер.
В пакете httplog собраны методы для логирования HTTP header и body: Настройка типа логирования и лог файла может задаваться в конфигурационном файле — параметры HttpLog и HTTPLogType, или динамически вызовом POST на /httplog. В шаблон включены два базовых способа аутентификации: HTTP Basic Authentication или MS AD Authentication.
Для MS AD Authentication используется библиотека gopkg.in/korylprince/go-ad-auth.v2.
На уровне конфигурационного файла, сервера можно задать тип аутентификации и параметры подключения к серверу MS AD. Пользователь и пароль для проверки HTTP Basic Authentication передается через командую строку при старте сервера. Использование JSON Web Token (JWT) задается на уровне конфигурационного файла сервера. Время жизни токена задается параметром JWTExpiresAt (JWTExpiresAt=0 — время жизни не ограничено). Секретный ключ для генерации JWT передается через командую строку при старте сервера. Для работы с JWT используется библиотека github.com/dgrijalva/jwt-go.
Вся обработка JWT: создание, проверка, формирование cookie собрано в небольшой кастомный пакет jwt Логика использования JWT следующая: Для организации библиотек перешел с golang/dep/cmd/dep на go mod, но для хранения библиотек по прежнему используется папка vendor в корне проекта. Это позволяет хранить "правильные" версии библиотек в своем репозитории проекта. Обновление библиотек выполняется командой. При переходе на go mod столкнулся с проблемой, что часть сторонних библиотек не поддерживают корректно работу с модулями и загружается неправильная версия. Для таких библиотек нужно указывать в конце номер нужного commit, например: Для того, чтобы компилятор брал библиотеки из каталога vendor, нужно указать опцию Для сборки используется простой make файл с несколькими режимами (взят где-то на посторах интернета):  При сборе в исполняемый файл внедряется версия, дата сборки и commit. Эта информация выводится в лог файл — весьма полезно для разбора ошибок.
Для реализации такого внедрения, в main добавляем переменные При сборе в make файле, запрашиваем git о commit При сборке привязываем эти значения к ранее объявленным переменным (опция -ldflags "-X"), заодно указываем операционку, под которую собираем и место хранения бинарника В main.main в переменную записываю итоговую информацию о версии, commit и дате сборки IT Архитектор