Русский
Русский
English
Статистика
Реклама

Golang

Разрабы становятся админами, а админы разрабами. Интервью с инженером Uber, где разделение исчезло совсем

11.02.2021 20:09:20 | Автор: admin

Данила Мигалин (@miga) живет в Вильнюсе и работает инженером в Uber.

Давным-давно контора, которая занималась русификацией игр, не взяла его работать переводчиком. На следующий день он устроился админом, потому что в школе увлекался программированием. Русское IT это большая деревня, одни и те же люди переходят из компании в компанию. До Uber я успел поработать в Яндексе и Майкрософте, говорит Данила.

С 2006 года он занимался ip-телефонией и админской работой. В свободное время писал на Перле, затем на Питоне, делал свои пет-проекты. Некоторые из них даже пошли в продакшн и в Яндексе, и в Майкрософте. Писать по-серьезному в продакшн он начал, только когда пришел в Uber. Место, которое мне предложили в компании, предполагало знание Golang. Меня не смутило то, что я иду админом на позицию разработчика. Я думал: отлично, наконец-то можно будет завязать с админским делом и спокойно писать код.

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


Трудно ли админу стать разрабом, а разрабу админом

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

Сейчас мы все называемся Software Engineer и пишем код каждый день. В Майкрософте мы назывались Service Engineer (уже не Operations, но еще не Software Engineer) и занимались девопской работой. Писали разработчикам разные автоматизации для деплоя.

Сейчас в моем окружении девопсов уже практически не осталось. В Uber у нас нет ни Opsов, ни Devов. Все инженеры пишут, деплоят и онколят свой продукт от и до. Команды ответственны за полный жизненный цикл продукта. Больше нет злых дяденек админов или более добрых дяденек девопсов, которые что-то за тебя сделают, задеплоят, замониторят. И мне это нравится.

Такая система хороша тем, что нет размывания ответственности. Есть продукт, его пишет команда. Эта же команда ответственна, чтобы он работал. Если что-то сломалось, ты знаешь, что виноват либо твой коллега, либо ты сам. Сломался продукт, которым ты пользуешься? Ты можешь пойти к ребятам и все прояснить. Они знают, как он написан, они могут его починить. Раньше придешь к админу, который онколит, со своей проблемой он пожмет плечами, скажет про ошибку в логах, переадресует к ребятам, которые будут только утром.

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

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

Компьютеры и сервисы становятся сложнее. Требования к разработчикам естественным образом повышаются. Я считаю это нормальным. Когда Яндекс переходил от классической модели админ и разработчик к девопсу, и программистов начинали ставить в онкол, многие люди ушли. Им это очень не понравилось! Они нанимались писать код, а все остальное гори синим пламенем.

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

Круто, что в Uber дежурства, что называется, follow the sun. Когда у нас ночь дежурят пацаны по другую сторону океана. Нам остается день. До этого мы дежурили круглосуточно. У нас были проекты, которые мы делали в Вильнюсе, и были проекты из Сан-Франциско. Мы за свои проекты онколили 24/7 и ребята из Сан-Франциско онколили за свои проекты 24/7. Потом провели ряд вебинаров, рассказали что к чему друг другу и начали онколить днем за свое и за американское, а они своим днем за свое и за Вильнюсское.

Многие хотят работать девопсами, но не все готовы к ночным дежурствам.

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

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

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

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

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

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

Самое главное, надо приучать себя к ответственности. Твоя программа это не код, который ты написал и забыл, это код, который где-то бежит надежно и красиво. Понимание этого упростило бы очень многое в индустрии.

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

Так было всегда.

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

Сейчас вакансию бэкендера без докера уже не найти. Ладно, а в обратную сторону это работает? Ты же не занимался продуктовой разработкой в конвейерном смысле. Сложно было?

Я не испытывал особых трудностей. Не скажу ничего насчет знания алгоритмов структур данных и прочего computer science, но уметь выразить свои мысли на языке программирования и уметь найти алгоритм это знание необходимо. И у меня оно было.

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

Запомнился один собес с админом большой российской технологической компании. Я задал ему простую задачку, скорее всего, сделать рейт-лимитер, и заметил, что у парня ступор. Он просто не знал, с какой стороны подойти. А по системным знаниям отвечал хорошо. Кое-как задачу допинали. Я потом его защищал, предлагал взять его хотя бы мидлом. За 10 лет практики человек каким-то мистическим образом умудрился избежать программирования. Это тренируется легко, с учетом того, что у тебя будут задачи, рядом будут люди, которые смогут тебя менторить. Не такая это великая наука по сравнению с тем огромным пластом знаний, который он получил за время своего админства.

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

Мне кажется, нет особой проблемы в том, чтобы изучить +1 язык программирования. Я бы не испугался Java.


Почему Golang стал стандартом в сфере Devops

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

При переходе с Питона на Golang я не испытал страха. Golang в тысячу, в миллиард раз проще, чем Питон. Ты буквально можешь открыть http://tur.golang.org/, пройти его за день и на утро писать в продакшн. Я не шучу, это действительно так.

Но есть одно но. Golang очень простой и поэтому не выразительный. Лично меня это раздражает.

Знаю ребят, которые с Java переходят на Scala, потом на Хаскель, потом на Идрис, и все равно им не хватает выразительности.

Да, я постоянно страдаю. Пишу код и думаю, блин, занимаюсь ерундой! Будь это Erlang, я бы воспользовался паттерн-матчингом, все было бы изящненько, красивенько. С Golang никакого эстетического удовольствия. Он не про изящность. Это в Питоне ты можешь написать какой-нибудь comprehension и, откинувшись на спинку кресла, любоваться им 5 минут: как ловко получилось. Мало возможностей проявить хоть какую-то творческую жилку. Уверен, что топорность Go была одной из целей создания языка, чтобы гугловские ребята, вся эта армия программистов, не занималась творчеством, а просто писала понятный и надежный код.

Вы и разработку, и автоматизацию девопс тоже на Golang делаете?

Да, конкретно в нашей архитектуре нет такого разделения. Это все сервис. Просто один обслуживает юзерский трафик, другой оркеструет контейнеры.

Моя команда занимается автоматизацией баз данных. Мы предоставляем базы данных как сервис внутри Uber. У нас для этого есть свой оркестратор. Как кубернетес, только убернетес! Мы пишем этот оркестратор каждый день. Пишем его на Golang.

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

И дальше следует предложение, которое мне очень нравится: Если вам необходимо читать эту документацию дальше, то вы слишком умный! Вы делаете что-то слишком умное. Так и написано по-английски.

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

Ты хорошо заметил, Golang очень opinionated. Он сделан с такой жесткой визией. Ты называешь переменные, выравниваешь текст, организовываешь взаимодействие между потоками и все. Про остальное можно забыть. Это хорошо снижает когнитивную нагрузку.

Мне кажется, что он поэтому у девопсов и зашел. Программисты любят решать такие проблемы: придумывать, как им форматировать код. А если человеку нужен скрипт, который автоматизирует простую вещь, он не хочет думать о том, как ему собирать, запаковывать, форматировать и так далее. Если тебя человек не из индустрии спросит о твоей профессии, что ты ответишь? Разработчик, девопс или админ?

Думаю, человек не из индустрии понятия не имеет, кто такой девопс. Я скажу программист. Я так и говорю. Это же всем понятно.


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

Подробнее..

Перевод SQLite с использованием Go и Python

10.02.2021 20:14:57 | Автор: admin

Для будущих студентов курса "Golang Developer. Professional" и всех интересующихся подготовили перевод интересного материала.

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


Введение

В основном я предпочитаю использовать реляционные базы данных (SQL), поскольку они предоставляют несколько возможностей, которые весьма полезны при работе с данными. SQLite - отличный выбор, так как база данных там представляет собой единый файл, что упрощает обмен данными. Несмотря на то, что это единый файл, SQLite может обрабатывать до 281 терабайта данных. SQLite также поставляется с клиентом командной строки sqlite3, который отлично подходит для быстрого прототипирования.

Примечание: В других базах данных есть транзакции, языки запросов и схемы. Однако базы данных на основе SQL имеют тенденцию быть более развитыми испытанными временем, а сам SQL стандартизирован.

Определения

Начнем с нескольких определений, которые помогут нам внести ясность.

SQLite: это встроенная база данных SQL. SQLite небольшая, быстрая и самая часто используемая база данных. Это также, безусловно, мой излюбленный способ обмена большими объемами данных между двумя программами.

Транзакции: вы вводите данные в базу данных SQL внутри транзакции. Это означает, что либо поступают сразу все данные, либо не поступают никакие. Транзакции на порядки упрощают код повторного выполнения операций в конвейерах данных.

Схема: данные в реляционных базах данных имеют схему, что означает, что их легче проверить на достоверность.

SQL: Structured Query Language (язык структурированных запросов) это язык для выборки и изменения данных. Вам не нужно изобретать еще один способ выборки интересующих вас частей данных. SQL это устоявшийся формат, вокруг которого накоплено много знаний и инструментов.

Проект

Мы напишем HTTP-сервер на Go, который будет получать уведомления о сделках (trades) и хранить их в базе данных SQLite. Затем мы напишем программу на Python, которая будет обрабатывать эти данные.

В Go мы будем использовать github.com/mattn/go-sqlite3, который является оболочкой для библиотеки C SQLite.

Примечание: Поскольку go-sqlite использует cgo, изначальное время сборки будет больше обычного. Использование cgo означает, что результирующий исполняемый файл зависит от некоторых разделяемых библиотек из ОС, что немного усложняет распространение.

В Python мы будем использовать встроенный модуль sqlite3 и функцию read_sql Pandas, чтобы загружать данные. Исходный код из этой статьи доступен по ссылке.

Код на Go

Код, который я собираюсь вам показать, можно найти в файле trades.go.

Листинг 1: Структура Trade

37 // Trade - это сделка о покупке/продаже по символу ценной бумаги.38 type Trade struct {39     Time   time.Time40     Symbol string41     Price  float6442     IsBuy  bool43 }

В Листинге 1 показана структура данных Trade. У нее есть поле Time, отражающее время сделки, поле Symbol, хранящее биржевое сокращение (символ акции, например, AAPL), поле Price, содержащее цену, и логический флаг, который сообщает, это сделка покупки или продажи.

Листинг 2: Схема базы данных

24     schemaSQL = `25 CREATE TABLE IF NOT EXISTS trades (26     time TIMESTAMP,27     symbol VARCHAR(32),28     price FLOAT,29     buy BOOLEAN30 );31 32 CREATE INDEX IF NOT EXISTS trades_time ON trades(time);33 CREATE INDEX IF NOT EXISTS trades_symbol ON trades(symbol);34 `

В Листинге 2 объявляется схема базы данных, соответствующая структуре Trade. В строке 25 мы создаем таблицу под названием trades. В строках 26-29 мы определяем столбцы таблицы, которые соответствуют полям структуры Trade. В строках 32-33 мы создаем индексы для таблицы, чтобы обеспечить быстрое запрашивание по time и symbol.

Листинг 3: Внесение записи

16     insertSQL = `17 INSERT INTO trades (18     time, symbol, price, buy19 ) VALUES (20     ?, ?, ?, ?21 )22 `

Листинг 3 определяет SQL-запрос для внесения записи в базу данных. В строке 20 мы используем символ-заполнитель ? для параметров этого запроса. Никогда не используйте fmt.Sprintf для создания SQL-запроса вы рискуете создать SQL-инъекцию.

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

Листинг 4: DB

45 // DB - это база данных биржевых торгов.46 type DB struct {47     sql    *sql.DB48     stmt   *sql.Stmt49     buffer []Trade50 }

В Листинге 4 описана структура DB. В строке 47 мы устанавливаем соединение с базой данных. В строке 48 мы храним подготовленный (предварительно скомпилированный) оператор для вставки, а в строке 49 находится наш буфер ожидающих транзакций в памяти.

Листинг 5: NewDB

53 // NewDB создает Trades для управления торговлей акциями в базе данных SQLite. Этот API не является потокобезопасным.54 func NewDB(dbFile string) (*DB, error) {55     sqlDB, err := sql.Open("sqlite3", dbFile)56     if err != nil {57         return nil, err58     }59 60     if _, err = sqlDB.Exec(schemaSQL); err != nil {61         return nil, err62     }63 64     stmt, err := sqlDB.Prepare(insertSQL)65     if err != nil {66         return nil, err67     }68 69     db := DB{70         sql:    sqlDB,71         stmt:   stmt,72         buffer: make([]Trade, 0, 1024),73     }74     return &db, nil75 }

Листинг 5 демонстрирует создание готовой к использованию базы данных DB. В строке 55 мы подключаемся к базе данных с помощью драйвера sqlite3. В строке 60 мы применяем SQL-схему, чтобы создать таблицу trades, если она еще не существует. В строке 64 мы предварительно компилируем инструкцию InsertSQL. В строке 72 мы создаем внутренний буфер с длиной 0 и емкостью 1024.

Примечание: Чтобы не усложнять код, предоставляемый мной API DB не горутино-безопасен (в отличие от sql.DB). Если несколько горутин вызывают API одновременно, вы столкнетесь с состоянием гонки. Я оставлю это вам в качестве упражнения сделайте этот код горутино-безопасным.

Листинг 6: Add

77 // Add сохраняет сделку в буфер. Как только буфер заполняется, сделки вносятся в базу данных.79 func (db *DB) Add(trade Trade) error {80     if len(db.buffer) == cap(db.buffer) {81         return errors.New("trades buffer is full")82     }83 84     db.buffer = append(db.buffer, trade)85     if len(db.buffer) == cap(db.buffer) {86         if err := db.Flush(); err != nil {87             return fmt.Errorf("unable to flush trades: %w", err)88         }89     }90 91     return nil92 }

В Листинге 6 приведен метод Add. В строке 84 мы добавляем сделку (trade) в буфер в памяти. В строке 85 мы проверяем, заполнен ли буфер, и если да, то мы вызываем метод Flush в строке 86, который вносит записи из буфера в базу данных.

Листинг 7: Flush

94  // Flush вносит ждущие обработки сделки в базу данных.95  func (db *DB) Flush() error {96      tx, err := db.sql.Begin()97      if err != nil {98          return err99      }100 101     for _, trade := range db.buffer {102         _, err := tx.Stmt(db.stmt).Exec(trade.Time, trade.Symbol, trade.Price, trade.IsBuy)103         if err != nil {104             tx.Rollback()105             return err106         }107     }108 109     db.buffer = db.buffer[:0]110     return tx.Commit()111 }

В Листинге 7 приведен метод Flush. В строке 96 мы начинаем транзакцию. В строке 101 мы итерируем по внутреннему буферу, а в строке 102 вносим каждую сделку. Если при внесении произошла ошибка, мы выполняем rollback в строке 104. В строке 109 мы сбрасываем буфер сделок в памяти. И наконец, в строке 110 мы выполняем commit.

Листинг 8: Close

113 // Close вносит (посредством Flush) все сделки в базу данных и предотвращает любую торговлю в будущем.114 func (db *DB) Close() error {115     defer func() {116         db.stmt.Close()117         db.sql.Close()118     }()119 120     if err := db.Flush(); err != nil {121         return err122     }123 124     return nil125 }

В Листинге 8 приведен метод Close. В строке 120 мы вызываем Flush, чтобы внести все оставшиеся сделки в базу данных. В строках 116 и 117 мы закрываем (close) инструкцию и базу данных. Функции, создающие DB, должны иметь функцию defer db.Close(), чтобы убедиться, что связь с базой данных закончена.

Листинг 9: Импорты

5 // Ваши пакеты main и test требуют эти импорты, чтобы пакет sql был правильно инициализирован.6 // _ "github.com/mattn/go-sqlite3"7 8 import (9     "database/sql"10     "errors"11     "fmt"12     "time"13 )

В Листинге 9 приведен импорт для файла. В строке 5 мы импортируем database/sql, которая определяет API для работы с базами данных SQL. database/sql не содержит какого-либо конкретного драйвера базы данных.

Как говорится в комментарии к оператору импорта, чтобы использовать пакет trades, вам необходимо импортировать пакет, который реализует драйвер базы данных sqlite3 (например, github.com/mattn/go-sqlite3). Поскольку вы импортируете пакет, реализующий драйвер только для небольшого изменения регистрации протокола sqlite3, мы используем перед импортом, сообщая компилятору Go, что то, что мы не используем этот пакет в коде это нормально.

Листинг 10: Пример использования

Код этих примеров можно найти в файле tradestest.go.

66 func ExampleDB() {67     dbFile := "/tmp/db-test" + time.Now().Format(time.RFC3339)68     db, err := trades.NewDB(dbFile)69     if err != nil {70         fmt.Println("ERROR: create -", err)71         return72     }73     defer db.Close()74 75     const count = 10_00076     for i := 0; i < count; i++ {77         trade := trades.Trade{78             Time:   time.Now(),79             Symbol: "AAPL",80             Price:  rand.Float64() * 200,81             IsBuy:  i%2 == 0,82         }83         if err := db.Add(trade); err != nil {84             fmt.Println("ERROR: insert - ", err)85             return86         }87     }88 89     fmt.Printf("inserted %d records\n", count)90     // Вывод:91     // inserted 10000 records92 }

В Листинге 10 показан пример использования (в виде тестируемого примера). В строке 67 мы создаем новую базу данных, а в строке 73 мы закрываем ее с помощью оператора defer. В строке 76 мы запускаем цикл для вставки сделок, а в строке 83 мы собственно и вставляем сделку в базу данных.

Код на Python

Примеры кода на Python можно найти в файле analysis_trades.py.

Листинг 11: Импорты

02 import sqlite303 from contextlib import closing04 from datetime import datetime05 06 import pandas as pd

В Листинге 11 показаны библиотеки, которые мы используем в нашем Python-коде. В строке 2 мы импортируем встроенный модуль sqlite3, а в строке 6 библиотеку pandas.

Листинг 12: Select SQL

08 select_sql = """09 SELECT * FROM trades10 WHERE time >= ? AND time <= ?11 """

В Листинге 12 показан SQL-запрос для получения данных. В строке 10 мы выбираем все столбцы из таблицы trades. В строке 10 мы добавляем элемент WHERE для выбора временного диапазона. Как и в Go-коде мы используем заполнители аргументов ? и не пишем SQL-запросы вручную.

Листинг 13: Загрузка сделок

14 def load_trades(db_file, start_time, end_time):15     """Загружаем сделки из db_file за заданный временной диапазон."""16     conn = sqlite3.connect(db_file)17     with closing(conn) as db:18         df = pd.read_sql(select_sql, db, params=(start_time, end_time))19 20     # Мы не можем использовать здесь detect_types=sqlite3.PARSE_DECLTYPES, поскольку Go вставляет часовой пояс, а sqlite3 Python не умеет обрабатывать его.22     # Смотрите https://bugs.python.org/issue29099# См. Https://bugs.python.org/issue2909923     df["time"] = pd.to_datetime(df["time"])24     return df

В Листинге 13 показан код для загрузки сделок из заданного временного диапазона. В строке 16 мы подключаемся к базе данных. В строке 17 мы используем менеджер контекста, что-то вроде defer в Go, чтобы убедиться, что база данных закрыта. В строке 18 мы используем функцию pandas read_sql для загрузки данных из SQL-запроса в DataFrame. Python имеет API для подключения к базам данных (например, database/sql), а Pandas может использовать любой совместимый драйвер. В строке 23 мы конвертируем столбец time в Timestamp pandas. Это особенность SQLite, в котором нет встроенной поддержки TIMESTAMP типов.

Листинг 14: Средняя цена

27 def average_price(df):28     """Возвращает среднюю цену в df, сгруппированную по (stock, buy)"""29     return df.groupby(["symbol", "buy"], as_index=False)["price"].mean()

В Листинге 14 показано, как вычислить среднюю цену на symbol и buy. В строке 29 мы используем DataFrame groupby для группировки по symbol и buy. Мы используем as_index=False, чтобы получить symbol и buy в виде столбцов в итоговом фрейме данных. Затем мы берем столбец price и вычисляем среднее значение для каждой группы.

Листинг 15: Вывод

symbol,buy,priceAAPL,0,250.82925665004535AAPL,1,248.28277375538832GOOG,0,250.11537993385295GOOG,1,252.4726772487683MSFT,0,250.9214212695317MSFT,1,248.60187022941685NVDA,0,250.3844763417279NVDA,1,249.3578146208962

В Листинге 15 показан результат выполнения кода Python на тестовых данных.

Заключение

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

Я максимально упростил код, чтобы показать наиболее интересные моменты. Есть несколько мест, где вы можете попробовать улучшить его:

  • Добавить код повторного выполнения в Flush

  • Выполнить дополнительную проверку ошибок в Close

  • Сделать DB горутино-безопасным

  • Реализовать больше аналитики в Python-части

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

Если вы не хотите напрямую работать с SQL, есть несколько альтернатив, в мире Go есть такие пакеты, как sqlx, gorm, а в мире Python есть SQLAlchemy, который может использовать Pandas.

Приятного вам кодинга. Дайте мне знать, как вы используете SQL в целом и SQLite в частности.


Узнать подробнее о курсе "Golang Developer. Professional".

Смотреть открытый вебинар по теме Go-каналы снаружи и внутри.

Подробнее..

Пишем Slack бота для Scrum покера на Go. Часть 1

04.03.2021 00:14:15 | Автор: admin

Здравствуйте! Сегодня мы напишем Slack бота для Scrum покера на языке Go. Писать будем по возможности без фреймворков и внешних библиотек, так как наша цель разобраться с языком программирования Go и проверить, насколько этот язык удобен для разработки подобных проектов.

Дисклеймер

Я только познаю Go и многих вещей еще не знаю. Мой основной язык разработки Python. Поэтому часто буду отсылать к нему в тех местах, где по моему мнению в Python что-то сделано удобнее или проще. Цель этих отсылок в том, чтобы породить дискуссию, ведь вполне вероятно, что эти "удобные вещи" также присутствуют в Go, просто я их не нашел.

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

Хватит прелюдий, вперед в бой!

Итоговый результат

Анимация работы будущего бота

Для тех, кому читать код интересней, чем статью прошу сюда.

Структура приложения

Разобьем нашу программу на следующие слои. У нас предполагается слой взаимодействия (web), слой для рисования интерфейса средствами Slack UI Block Kit (ui), слой для сохранения / получения результатов (storage), а также место для хранения настроек (config). Давайте создадим следующие папки в проекте:

config/storage/ui/web/-- clients/-- server/main.go

Сервер

Для сервера будем использовать стандартный сервер из пакета http. Создадим структуру Server следующего вида в web -> server:

server.go
package serverimport ("context""log""net/http""os""os/signal""sync/atomic""time")type Server struct {  // Здесь мы будем определять все необходимые нам зависимости и передавать их на старте приложения в main.gohealthy        int32logger         *log.Logger}func NewServer(logger *log.Logger) *Server {return &Server{logger: logger,}}

Эта структура будет выступать хранилищем зависимостей для наших хэндлеров. Есть несколько подходов для организации работы с хэндлерами и их зависимостями. Например, можно объявлять и запускать все в main.go, там же где мы создаем экземпляры наших структур и интерфейсов. Но это плохой путь. Еще есть вариант использовать глобальные переменные и просто их импортировать. Но в таком случае становится сложно покрывать проект тестами. Дальше мы увидим плюсы выбранного мной подхода. Итак, нам нужно запустить наш сервер. Напишем метод:

server.go
func (s *Server) setupRouter() http.Handler {  // TODOrouter := http.NewServeMux()  return router}func (s *Server) Serve(address string) {server := &http.Server{Addr:         address,    Handler:      s.setupRouter(),ErrorLog:     s.logger, // Наш логгерReadTimeout:  5 * time.Second,WriteTimeout: 10 * time.Second,IdleTimeout:  15 * time.Second,}  // Создаем каналы для корректного завершения процессаdone := make(chan bool)quit := make(chan os.Signal, 1)  // Настраиваем сигнал для корректного завершения процессаsignal.Notify(quit, os.Interrupt)go func() {<-quits.logger.Println("Server is shutting down...")    // Эта переменная пригодится для healthcheck'а напримерatomic.StoreInt32(&s.healthy, 0)    // Даем клиентам 30 секунд для завершения всех операций, прежде чем сервер будет остановленctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)defer cancel()    // Информируем сервер о том, что не нужно держать существующие коннектыserver.SetKeepAlivesEnabled(false)    // Выключаем серверif err := server.Shutdown(ctx); err != nil {s.logger.Fatalf("Could not gracefully shutdown the server: %v\n", err)}close(done)}()s.logger.Println("Server is ready to handle requests at", address)  // Переменная для проверки того, что сервер запустился и все хорошоatomic.StoreInt32(&s.healthy, 1)  // Запускаем серверif err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {s.logger.Fatalf("Could not listen on %s: %v\n", address, err)}  // Когда сервер остановлен и все хорошо, снова получаем управление и логируем результат<-dones.logger.Println("Server stopped")}

Теперь давайте создадим первый хэндлер. Создадим папку в web -> server -> handlers:

healthcheck.go
package handlersimport ("net/http")func Healthcheck() http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {w.Write("OK")})}

Добавим наш хэндлер в роутер:

server.go
// Наш код вышеfunc (s *Server) setupRouter() http.Handler {router := http.NewServeMux()router.Handle("/healthcheck",handlers.Healthcheck(),)  return router}// Наш код ниже

Идем в main.go и пробуем запустить наш сервер:

package mainimport ("log"  "os"  "go-scrum-poker-bot/web/server")func main() {  // Создаем логгер со стандартными флагами и префиксом "INFO:".   // Писать он будет только в stdoutlogger := log.New(os.Stdout, "INFO: ", log.LstdFlags)app := server.NewServer(logger)app.Serve(":8000")}

Пробуем запустить проект:

go run main.go

Если все хорошо, то сервер запустится на :8000 порту. Наш текущий подход к созданию хэндлеров позволяет передавать в них любые зависимости. Это нам еще пригодится, когда мы будем писать тесты. ;) Прежде чем идти дальше, нам нужно немного настроить нашу локальную среду, чтобы Slack смог с нами взаимодействовать.

NGROK

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

ngrok http 8000

Если все хорошо, то вы увидите что-то вроде этого:

ngrok by @inconshreveable                                                                                                            (Ctrl+C to quit)                                                                                                                                                     Session Status                online                                                                                                                 Account                       Sayakhov Ilya (Plan: Free)                                                                                             Version                       2.3.35                                                                                                                 Region                        United States (us)                                                                                                     Web Interface                 http://127.0.0.1:4040                                                                                                  Forwarding                    http://ffd3cfcc460c.ngrok.io -> http://localhost:8000                                                                  Forwarding                    https://ffd3cfcc460c.ngrok.io -> http://localhost:8000                                                                                                                                                                                                                      Connections                   ttl     opn     rt1     rt5     p50     p90                                                                                                          0       0       0.00    0.00    0.00    0.00     

Нас интересует строчка https://ffd3cfcc460c.ngrok.io. Она нам понадобится дальше.

Slash commands

Создадим наше приложение в Slack. Для этого нужно перейти сюда -> Create New App. Далее указываем имя GoScrumPokerBot и добавляем его в свой Workspace. Далее, нам нужно дать нашему боту права. Для этого идем в OAuth & Permissions -> Scopes и добавляем следующие права: chat:write, commands. Первый набор прав нужен, чтобы бот мог писать в каналы, а второй для slash команд. И наконец нажимаем на Reinstall to Workspace. Готово! Теперь идем в раздел Slash commands и добавляем нашу команду /poker .

В Request URL нужно вписать адрес из пункта выше + путь. Пусть будет так: https://ffd3cfcc460c.ngrok.io/play-poker.

Slash command handler

Теперь создадим хэндлер для обработки событий на только созданную команду. Идем в web -> server -> handlers и создаем файл play_poker.go:

func PlayPokerCommand() http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {    w.Header().Set("Content-Type", "application/json")    w.Write([]byte(`{"response_type": "ephemeral", "text": "Hello world!"}`))})}

Добавляем наш хэндлер в роутер:

server.go
func (s *Server) setupRouter() http.Handler {router := http.NewServeMux()router.Handle("/healthcheck",handlers.Healthcheck(),)router.Handle("/play-poker",handlers.PlayPokerCommand(),)  return router}

Идем в Slack и пробуем выполнить эту команду: /poker. В ответ вы должны получить что-то вроде этого:

Но это не единственный вариант взаимодействия со Slack. Мы также можем слать сообщения в канал. Этот вариант мне понравился больше и плюс у него больше возможностей в сравнении с ответом на команду. Например вы можете послать сообщение в фоне (если оно требует долгих вычислений). Давайте напишем наш http клиента. Идем в web -> clients. Создаем файл client.go:

client.go
package clients// Создадим новый тип для наших хэндлеровtype Handler func(request *Request) *Response// Создадим новый тип для middleware (о них чуть позже)type Middleware func(handler Handler, request *Request) Handler// Создадим интерфейс http клиентаtype Client interface {Make(request *Request) *Response}// Наша реализация клиентаtype BasicClient struct {client     *http.Clientmiddleware []Middleware}func NewBasicClient(client *http.Client, middleware []Middleware) Client {return &BasicClient{client: client, middleware: middleware}}// Приватный метод для всей грязной работыfunc (c *BasicClient) makeRequest(request *Request) *Response {payload, err := request.ToBytes() // TODOif err != nil {return &Response{Error: err}}  // Создаем новый request, передаем в него данныеreq, err := http.NewRequest(request.Method, request.URL, bytes.NewBuffer(payload))if err != nil {return &Response{Error: err}}  // Применяем заголовкиfor name, value := range request.Headers {req.Header.Add(name, value)}  // Выполняем запросresp, err := c.client.Do(req)if err != nil {return &Response{Error: err}}defer resp.Body.Close()  // Читаем тело ответаbody, err := ioutil.ReadAll(resp.Body)if err != nil {return &Response{Error: err}}err = nil  // Если вернулось что-то отличное выше или ниже 20x, то ошибкаif resp.StatusCode > http.StatusIMUsed || resp.StatusCode < http.StatusOK {err = fmt.Errorf("Bad response. Status: %d, Body: %s", resp.StatusCode, string(body))}return &Response{Status:  resp.StatusCode,Body:    body,Headers: resp.Header,Error:   err,}}// Наш публичный метод для запросовfunc (c *BasicClient) Make(request *Request) *Response {if request.Headers == nil {request.Headers = make(map[string]string)}    // Применяем middlewarehandler := c.makeRequestfor _, middleware := range c.middleware {handler = middleware(handler, request)}return handler(request)}

Теперь создадим файл web -> clients:

request.go
package clientsimport "encoding/json"type Request struct {URL     stringMethod  stringHeaders map[string]stringJson    interface{}}func (r *Request) ToBytes() ([]byte, error) {if r.Json != nil {result, err := json.Marshal(r.Json)if err != nil {return []byte{}, err}return result, nil}return []byte{}, nil}

Сразу напишем тесты к методу ToBytes(). Для тестов я взял testify/assert, так как без нее была бы куча if'ов, а меня они напрягают :) . К тому же, я привык к pytest и его assert, да и как-то глазу приятнее:

request_test.go
package clients_testimport ("encoding/json""go-scrum-poker-bot/web/clients""reflect""testing""github.com/stretchr/testify/assert")func TestRequestToBytes(t *testing.T) {  // Здесь мы делаем что-то вроде pytest.parametrize (жаль, что в Go нет сахара для декораторов, это было бы удобнее)testCases := []struct {json interface{}data []byteerr  error}{{map[string]string{"test_key": "test_value"}, []byte("{\"test_key\":\"test_value\"}"), nil},{nil, []byte{}, nil},{make(chan int), []byte{}, &json.UnsupportedTypeError{Type: reflect.TypeOf(make(chan int))}},}  // Проходимся по нашим тест кейсамfor _, testCase := range testCases {request := clients.Request{URL:     "https://example.com",Method:  "GET",Headers: nil,Json:    testCase.json,}actual, err := request.ToBytes()    // Проверяем результатыassert.Equal(t, testCase.err, err)assert.Equal(t, testCase.data, actual)}}

И нам нужен web -> clients:

response.go
package clientsimport "encoding/json"type Response struct {Status  intHeaders map[string][]stringBody    []byteError   error}// Я намеренно сделал универсальный метод, чтобы можно было привезти любой ответ к нужному и не писать каждый раз эти богомерзкие if err != nilfunc (r *Response) Json(to interface{}) error {if r.Error != nil {return r.Error}return json.Unmarshal(r.Body, to)}

И также, напишем тесты для метода Json(to interface{}):

response_test.go
package clients_testimport ("errors""go-scrum-poker-bot/web/clients""testing""github.com/stretchr/testify/assert")// Один тест на позитивный кейсfunc TestResponseJson(t *testing.T) {to := struct {TestKey string `json:"test_key"`}{}response := clients.Response{Status:  200,Headers: nil,Body:    []byte(`{"test_key": "test_value"}`),Error:   nil,}err := response.Json(&to)assert.Equal(t, nil, err)assert.Equal(t, "test_value", to.TestKey)}// Один тест на ошибкуfunc TestResponseJsonError(t *testing.T) {expectedErr := errors.New("Error!")response := clients.Response{Status:  200,Headers: nil,Body:    nil,Error:   expectedErr,}err := response.Json(map[string]string{})assert.Equal(t, expectedErr, err)}

Теперь, когда у нас есть все необходимое, нам нужно написать тесты для клиента. Есть несколько вариантов написания тестов для http клиента. Я выбрал вариант с подменой http транспорта. Однако есть и другие варианты, но этот мне показался удобнее:

client_test.go
package clients_testimport ("bytes""go-scrum-poker-bot/web/clients""io/ioutil""net/http""testing""github.com/stretchr/testify/assert")// Для удобства объявим новый типtype RoundTripFunc func(request *http.Request) *http.Responsefunc (f RoundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {return f(request), nil}// Создание mock тестового клиентаfunc NewTestClient(fn RoundTripFunc) *http.Client {return &http.Client{Transport: RoundTripFunc(fn),}}// Валидный тестfunc TestMakeRequest(t *testing.T) {url := "https://example.com/ok"  // Создаем mock клиента и пишем нужный нам ответhttpClient := NewTestClient(func(req *http.Request) *http.Response {assert.Equal(t, req.URL.String(), url)return &http.Response{StatusCode: http.StatusOK,Body:       ioutil.NopCloser(bytes.NewBufferString("OK")),Header:     make(http.Header),}})  // Создаем нашего http клиента с замоканным http клиентомwebClient := clients.NewBasicClient(httpClient, nil)response := webClient.Make(&clients.Request{URL:     url,Method:  "GET",Headers: map[string]string{"Content-Type": "application/json"},Json:    nil,})assert.Equal(t, http.StatusOK, response.Status)}// Тест на ошибочный responsefunc TestMakeRequestError(t *testing.T) {url := "https://example.com/error"httpClient := NewTestClient(func(req *http.Request) *http.Response {assert.Equal(t, req.URL.String(), url)return &http.Response{StatusCode: http.StatusBadGateway,Body:       ioutil.NopCloser(bytes.NewBufferString("Bad gateway")),Header:     make(http.Header),}})webClient := clients.NewBasicClient(httpClient, nil)response := webClient.Make(&clients.Request{URL:     url,Method:  "GET",Headers: map[string]string{"Content-Type": "application/json"},Json:    nil,})assert.Equal(t, http.StatusBadGateway, response.Status)}

Отлично! Теперь давайте напишем middleware. Я привык для каждой, даже самой маленькой задачи, писать отдельную маленькую middleware. Так можно легко переиспользовать такой код в разных проектах / для разных API с разными требованиями к заголовкам / авторизации и так далее. Slack требует при отправке сообщений в канал указывать Authorization заголовок с токеном, который вы сможете найти в разделе OAuth & Permissions. Создаем в web -> clients -> middleware:

auth.go
package middlewareimport ("fmt""go-scrum-poker-bot/web/clients")// Токен будем передавать при определении middleware на этапе инициализации клиентаfunc Auth(token string) clients.Middleware {return func(handler clients.Handler, request *clients.Request) clients.Handler {return func(request *clients.Request) *clients.Response {request.Headers["Authorization"] = fmt.Sprintf("Bearer %s", token)return handler(request)}}}

И напишем тест к ней:

auth_test.go
package middleware_testimport ("fmt""go-scrum-poker-bot/web/clients""go-scrum-poker-bot/web/clients/middleware""testing""github.com/stretchr/testify/assert")func TestAuthMiddleware(t *testing.T) {token := "test"request := &clients.Request{Headers: map[string]string{},}handler := middleware.Auth(token)(func(request *clients.Request) *clients.Response {return &clients.Response{}},request,)handler(request)assert.Equal(t, map[string]string{"Authorization": fmt.Sprintf("Bearer %s", token)}, request.Headers)}

Также в репозитории вы сможете найти middleware для логирования и установки Content-Type: application/json. Здесь я не буду приводить этот код в целях экономии времени и места :).

Давайте перепишем наш PlayPoker хэндлер:

play_poker.go
package handlersimport ("errors""go-scrum-poker-bot/ui""go-scrum-poker-bot/web/clients""go-scrum-poker-bot/web/server/models""net/http""github.com/google/uuid")func PlayPokerCommand(webClient clients.Client, uiBuilder *ui.Builder) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {    // Добавим проверку, что нам пришли данные из POST Form с текстом и ID каналаif r.PostFormValue("channel_id") == "" || r.PostFormValue("text") == "" {w.Write(models.ResponseError(errors.New("Please write correct subject"))) // TODOreturn}resp := webClient.Make(&clients.Request{URL:    "https://slack.com/api/chat.postMessage",Method: "POST",      Json: uiBuilder.Build( // TODO: Напишем builder позжеr.PostFormValue("channel_id"),uuid.New().String(),r.PostFormValue("text"),nil,false,),})if resp.Error != nil {w.Write(models.ResponseError(resp.Error)) // TODOreturn}})}

И создадим в web -> server -> models . Файл errors.go для быстрого формирования ошибок:

errors.go
package modelsimport ("encoding/json""fmt")type SlackError struct {ResponseType string `json:"response_type"`Text         string `json:"text"`}func ResponseError(err error) []byte {resp, err := json.Marshal(SlackError{ResponseType: "ephemeral",Text:         fmt.Sprintf("Sorry, there is some error happened. Error: %s", err.Error()),},)if err != nil {return []byte("Sorry. Some error happened")}return resp}

Напишем тесты для хэндлера:

play_poker_test.go
package handlers_testimport ("errors""go-scrum-poker-bot/config""go-scrum-poker-bot/ui""go-scrum-poker-bot/web/server/handlers""go-scrum-poker-bot/web/server/models""net/http""net/http/httptest""net/url""strings""testing""github.com/stretchr/testify/assert")func TestPlayPokerHandler(t *testing.T) {config := config.NewConfig() // TODOmockClient := &MockClient{}uiBuilder := ui.NewBuilder(config) // TODOresponseRec := httptest.NewRecorder()router := http.NewServeMux()router.Handle("/play-poker", handlers.PlayPokerCommand(mockClient, uiBuilder))payload := url.Values{"channel_id": {"test"}, "text": {"test"}}.Encode()request, err := http.NewRequest("POST", "/play-poker", strings.NewReader(payload))request.Header.Set("Content-Type", "application/x-www-form-urlencoded")router.ServeHTTP(responseRec, request)assert.Nil(t, err)assert.Equal(t, http.StatusOK, responseRec.Code)assert.Empty(t, responseRec.Body.String())assert.Equal(t, true, mockClient.Called)}func TestPlayPokerHandlerEmptyBodyError(t *testing.T) {config := config.NewConfig()mockClient := &MockClient{}uiBuilder := ui.NewBuilder(config)responseRec := httptest.NewRecorder()router := http.NewServeMux()router.Handle("/play-poker", handlers.PlayPokerCommand(mockClient, uiBuilder))payload := url.Values{}.Encode()request, _ := http.NewRequest("POST", "/play-poker", strings.NewReader(payload))request.Header.Set("Content-Type", "application/x-www-form-urlencoded")router.ServeHTTP(responseRec, request)expected := string(models.ResponseError(errors.New("Please write correct subject")))assert.Equal(t, http.StatusOK, responseRec.Code)assert.Equal(t, expected, responseRec.Body.String())assert.Equal(t, false, mockClient.Called)}func TestPlayPokerHandlerRequestError(t *testing.T) {errMsg := "Error msg"config := config.NewConfig() // TODOmockClient := &MockClient{Error: errMsg}uiBuilder := ui.NewBuilder(config) // TODOresponseRec := httptest.NewRecorder()router := http.NewServeMux()router.Handle("/play-poker", handlers.PlayPokerCommand(mockClient, uiBuilder))payload := url.Values{"channel_id": {"test"}, "text": {"test"}}.Encode()request, _ := http.NewRequest("POST", "/play-poker", strings.NewReader(payload))request.Header.Set("Content-Type", "application/x-www-form-urlencoded")router.ServeHTTP(responseRec, request)expected := string(models.ResponseError(errors.New(errMsg)))assert.Equal(t, http.StatusOK, responseRec.Code)assert.Equal(t, expected, responseRec.Body.String())assert.Equal(t, true, mockClient.Called)}

Теперь нам нужно написать mock для нашего http клиента:

common_test.go
package handlers_testimport ("errors""go-scrum-poker-bot/web/clients")type MockClient struct {Called boolError  string}func (c *MockClient) Make(request *clients.Request) *clients.Response {c.Called = truevar err error = nilif c.Error != "" {err = errors.New(c.Error)}return &clients.Response{Error: err}}

Как видите, код хэндлера PlayPoker аккуратный и его просто покрывать тестами и не страшно в случае чего изменять.

Теперь можно приступить к написанию UI строителя интерфейсов для Slack UI Block Kit. Там все довольно просто, но много однотипного кода. Отмечу лишь, что Slack API мне не очень понравился и было тяжело с ним работать. Сам UI Builder можно глянуть в папке ui здесь. А здесь, в целях экономии времени, я не буду на нем заострять внимания. Отмечу лишь, что в качестве якоря для понимания того, событие от какого сообщения пришло и какой был текст для голосования (его мы не будем сохранять у себя, а будем брать непосредственно из события) будем использовать block_id. А для определения типа события будем смотреть на action_id.

Давайте создадим конфиг для нашего приложения. Идем в config и создаем:

config.go
package configtype Config struct {App   *AppSlack *SlackRedis *Redis}func NewConfig() *Config {return &Config{App: &App{ServerAddress: getStrEnv("WEB_SERVER_ADDRESS", ":8000"),PokerRanks:    getListStrEnv("POKER_RANKS", "?,0,0.5,1,2,3,5,8,13,20,40,100"),},Slack: &Slack{Token: getStrEnv("SLACK_TOKEN", "FILL_ME"),},    // Скоро понадобитсяRedis: &Redis{Host: getStrEnv("REDIS_HOST", "0.0.0.0"),Port: getIntEnv("REDIS_PORT", "6379"),DB:   getIntEnv("REDIS_DB", "0"),},}}// Получаем значение из env или выставляем defaultfunc getStrEnv(key string, defaultValue string) string {if value, ok := os.LookupEnv(key); ok {return value}return defaultValue}// Получаем int значение из env или выставляем defaultfunc getIntEnv(key string, defaultValue string) int {value, err := strconv.Atoi(getStrEnv(key, defaultValue))if err != nil {panic(fmt.Sprintf("Incorrect env value for %s", key))}return value}// Получаем список (e.g. 0,1,2,3,4,5) из env или выставляем defaultfunc getListStrEnv(key string, defaultValue string) []string {value := []string{}for _, item := range strings.Split(getStrEnv(key, defaultValue), ",") {value = append(value, strings.TrimSpace(item))}return value}

И напишем тесты к нему. Будем тестировать только публичные методы:

config_test.go
package config_testimport (    "go-scrum-poker-bot/config"    "os"    "testing"    "github.com/stretchr/testify/assert")func TestNewConfig(t *testing.T) {    c := config.NewConfig()    assert.Equal(t, "0.0.0.0", c.Redis.Host)    assert.Equal(t, 6379, c.Redis.Port)    assert.Equal(t, 0, c.Redis.DB)    assert.Equal(t, []string{"?", "0", "0.5", "1", "2", "3", "5", "8", "13", "20", "40", "100"}, c.App.PokerRanks)}func TestNewConfigIncorrectIntFromEnv(t *testing.T) {    os.Setenv("REDIS_PORT", "-")    assert.Panics(t, func() { config.NewConfig() })}

Я намеренно сделал обязательность выставления значений по умолчанию, хотя это не самый правильный путь. Изменим main.go:

main.go
package mainimport ("fmt""go-scrum-poker-bot/config""go-scrum-poker-bot/ui""go-scrum-poker-bot/web/clients"clients_middleware "go-scrum-poker-bot/web/clients/middleware""go-scrum-poker-bot/web/server"  "log""net/http""os""time")func main() {logger := log.New(os.Stdout, "INFO: ", log.LstdFlags)config := config.NewConfig()builder := ui.NewBuilder(config)webClient := clients.NewBasicClient(&http.Client{Timeout: 5 * time.Second,},[]clients.Middleware{ // Наши middlewareclients_middleware.Auth(config.Slack.Token),clients_middleware.JsonContentType,clients_middleware.Log(logger),},)app := server.NewServer(logger,webClient,builder,)app.Serve(config.App.ServerAddress)}

Теперь при запуске команды /poker мы в ответ получим наш симпатичный минималистичный интерфейс.

Slack Interactivity

Давайте научимся реагировать на события при взаимодействии пользователя с ним. Зайдем Your apps -> Наш бот -> Interactivity & Shortcuts. В Request URL введем:

https://ffd3cfcc460c.ngrok.io/interactivity

Создадим еще один хэндлер InteractionCallback в web -> server -> handlers:

interaction_callback.go
package handlersimport ("go-scrum-poker-bot/storage""go-scrum-poker-bot/ui""go-scrum-poker-bot/ui/blocks""go-scrum-poker-bot/web/clients""go-scrum-poker-bot/web/server/models""net/http")func InteractionCallback(userStorage storage.UserStorage,sessionStorage storage.SessionStorage,uiBuilder *ui.Builder,webClient clients.Client,) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {var callback models.Callback    // Об этом нижеdata, err := callback.SerializedData([]byte(r.PostFormValue("payload")))if err != nil {http.Error(w, err.Error(), http.StatusBadRequest)return}    // TODO: Скоро доберемся до нихusers := userStorage.All(data.SessionID)visible := sessionStorage.GetVisibility(data.SessionID)err = nil    // Определяем какое событие к нам поступило и реализуем немного логики исходя из негоswitch data.Action.ActionID {case ui.VOTE_ACTION_ID:users[callback.User.Username] = data.Action.SelectedOption.Valueerr = userStorage.Save(data.SessionID, callback.User.Username, data.Action.SelectedOption.Value)case ui.RESULTS_VISIBILITY_ACTION_ID:visible = !visibleerr = sessionStorage.SetVisibility(data.SessionID, visible)}if err != nil {http.Error(w, err.Error(), http.StatusInternalServerError)return}    // Шлем ответ перерисовывая интерфейс сообщения через response URL. Для пользователя все пройдет незаметноresp := webClient.Make(&clients.Request{URL:    callback.ResponseURL,Method: "POST",Json: &blocks.Interactive{ReplaceOriginal: true,Blocks:          uiBuilder.BuildBlocks(data.Subject, users, data.SessionID, visible),LinkNames:       true,},})if resp.Error != nil {http.Error(w, resp.Error.Error(), http.StatusInternalServerError)return}})}

Мы пока не определили наше хранилище. Давайте определим их интерфейсы и напишем тест на этот хэндлер. Идем в storage:

storage.go
package storagetype UserStorage interface {All(sessionID string) map[string]stringSave(sessionID string, username string, value string) error}type SessionStorage interface {GetVisibility(sessionID string) boolSetVisibility(sessionID string, state bool) error}

Я намеренно разбил логику на два хранилища, поскольку так удобнее тестировать и если будет нужно, то легко можно будет перевести например хранение голосов пользователей в базу данных, а настройки сессии оставить в Redis (как пример).

Теперь нужно создать модель Callback. Идем в web -> server -> models:

callback.go
package modelsimport ("encoding/json""errors""go-scrum-poker-bot/ui")type User struct {Username string `json:"username"`}type Text struct {Type string `json:"type"`Text string `json:"text"`}type Block struct {Type    string `json:"type"`BlockID string `json:"block_id"`Text    *Text  `json:"text,omitempty"`}type Message struct {Blocks []*Block `json:"blocks,omitempty"`}type SelectedOption struct {Value string `json:"value"`}type Action struct {BlockID        string          `json:"block_id"`ActionID       string          `json:"action_id"`Value          string          `json:"value,omitempty"`SelectedOption *SelectedOption `json:"selected_option,omitempty"`}type SerializedData struct {SessionID stringSubject   stringAction    *Action}type Callback struct {ResponseURL string    `json:"response_url"`User        *User     `json:"user"`Actions     []*Action `json:"actions"`Message     *Message  `json:"message,omitempty"`}// Грязно достаем ID сессии, но другого способа я не смог придуматьfunc (c *Callback) getSessionID() (string, error) {for _, action := range c.Actions {if action.BlockID != "" {return action.BlockID, nil}}return "", errors.New("Invalid session ID")}// Текст для голосованияfunc (c *Callback) getSubject() (string, error) {for _, block := range c.Message.Blocks {if block.BlockID == ui.SUBJECT_BLOCK_ID && block.Text != nil {return block.Text.Text, nil}}return "", errors.New("Invalid subject")}// Какое событие к нам пришлоfunc (c *Callback) getAction() (*Action, error) {for _, action := range c.Actions {if action.ActionID == ui.VOTE_ACTION_ID || action.ActionID == ui.RESULTS_VISIBILITY_ACTION_ID {return action, nil}}return nil, errors.New("Invalid action")}func (c *Callback) SerializedData(data []byte) (*SerializedData, error) {err := json.Unmarshal(data, c)if err != nil {return nil, err}sessionID, err := c.getSessionID()if err != nil {return nil, err}subject, err := c.getSubject()if err != nil {return nil, err}action, err := c.getAction()if err != nil {return nil, err}return &SerializedData{SessionID: sessionID,Subject:   subject,Action:    action,}, nil}

Давайте напишем тест на наш хэндлер:

interaction_callback_test.go
package handlers_testimport ("encoding/json""go-scrum-poker-bot/config""go-scrum-poker-bot/ui""go-scrum-poker-bot/web/server/handlers""go-scrum-poker-bot/web/server/models""net/http""net/http/httptest""net/url""strings""testing""github.com/stretchr/testify/assert")func TestInteractionCallbackHandlerActions(t *testing.T) {config := config.NewConfig()mockClient := &MockClient{}mockUserStorage := &MockUserStorage{}mockSessionStorage := &MockSessionStorage{}uiBuilder := ui.NewBuilder(config)router := http.NewServeMux()router.Handle("/interactivity",handlers.InteractionCallback(mockUserStorage, mockSessionStorage, uiBuilder, mockClient),)actions := []*models.Action{{BlockID:        "test",ActionID:       ui.RESULTS_VISIBILITY_ACTION_ID,Value:          "test",SelectedOption: nil,},{BlockID:        "test",ActionID:       ui.VOTE_ACTION_ID,Value:          "test",SelectedOption: &models.SelectedOption{Value: "1"},},}  // Проверяем на двух разных типах событийfor _, action := range actions {responseRec := httptest.NewRecorder()data, _ := json.Marshal(models.Callback{ResponseURL: "test",User:        &models.User{Username: "test"},Actions:     []*models.Action{action},Message: &models.Message{Blocks: []*models.Block{{Type:    "test",BlockID: ui.SUBJECT_BLOCK_ID,Text:    &models.Text{Type: "test", Text: "test"},},},},})payload := url.Values{"payload": {string(data)}}.Encode()request, err := http.NewRequest("POST", "/interactivity", strings.NewReader(payload))request.Header.Set("Content-Type", "application/x-www-form-urlencoded")router.ServeHTTP(responseRec, request)assert.Nil(t, err)assert.Equal(t, http.StatusOK, responseRec.Code)assert.Empty(t, responseRec.Body.String())assert.Equal(t, true, mockClient.Called)}}

Осталось определить mock для наших хранилищ. Обновим файл common_test.go:

common_test.go
// Существующий кодtype MockUserStorage struct{}func (s *MockUserStorage) All(sessionID string) map[string]string {return map[string]string{"user": "1"}}func (s *MockUserStorage) Save(sessionID string, username string, value string) error {return nil}type MockSessionStorage struct{}func (s *MockSessionStorage) GetVisibility(sessionID string) bool {return true}func (s *MockSessionStorage) SetVisibility(sessionID string, state bool) error {return nil}

Добавив в роутер новый хэндлер:

server.go
// Существующий кодfunc (s *Server) setupRouter() http.Handler {router := http.NewServeMux()router.Handle("/healthcheck",handlers.Healthcheck(),)router.Handle("/play-poker",handlers.PlayPokerCommand(s.webClient, s.uiBuilder),)router.Handle("/interactivity",handlers.InteractionCallback(s.userStorage, s.sessionStorage, s.uiBuilder, s.webClient),)return router}// Существующий код

Все хорошо, но наш сервер никак не уведомляет нас о том, что к нему поступил запрос + если мы где-то поймаем панику, то сервер может упасть. Давайте это исправим через middleware. Создаем папку web -> server -> middleware:

log.go
package middlewareimport ("log""net/http")func Log(logger *log.Logger) func(http.Handler) http.Handler {return func(next http.Handler) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {defer func() {logger.Printf("Handle request: [%s]: %s - %s - %s",r.Method, r.URL.Path, r.RemoteAddr, r.UserAgent(),)}()next.ServeHTTP(w, r)})}}

И напишем для нее тест:

log_test.go
package middleware_testimport ("bytes""go-scrum-poker-bot/web/server/middleware""log""net/http""net/http/httptest""os""strings""testing""github.com/stretchr/testify/assert")type logHandler struct{}func (h *logHandler) ServeHTTP(http.ResponseWriter, *http.Request) {}func TestLogMiddleware(t *testing.T) {var buf bytes.Bufferlogger := log.New(os.Stdout, "INFO: ", log.LstdFlags)  // Выставляем для логгера output наш буффер, чтобы все писалось в негоlogger.SetOutput(&buf)handler := &logHandler{}  // Берем mock recorder из стандартной библиотеки GoresponseRec := httptest.NewRecorder()router := http.NewServeMux()router.Handle("/test", middleware.Log(logger)(handler))request, err := http.NewRequest("GET", "/test", strings.NewReader(""))router.ServeHTTP(responseRec, request)assert.Nil(t, err)assert.Equal(t, http.StatusOK, responseRec.Code)  // Проверяем, что в буффер что-то пришло. Этого нам достаточно, чтобы понять, что middleware успешно отработалаassert.NotEmpty(t, buf.String())}

Остальные middleware можете найти здесь.

Ну и наконец слой хранения данных. Я решил взять Redis, так как это проще, да и не нужно для такого рода задач что-то большее, как мне кажется. Воспользуемся библиотекой go-redis и там же возьмем redismock для тестов.

Для начала научимся сохранять и получать всех пользователей переданной Scrum Poker сессии. Идем в storage:

users.go
package storageimport ("context""fmt""github.com/go-redis/redis/v8")// Шаблоны ключейconst SESSION_USERS_TPL = "SESSION:%s:USERS"const USER_VOTE_TPL = "SESSION:%s:USERNAME:%s:VOTE"type UserRedisStorage struct {redis   *redis.Clientcontext context.Context}func NewUserRedisStorage(redisClient *redis.Client) *UserRedisStorage {return &UserRedisStorage{redis:   redisClient,context: context.Background(),}}func (s *UserRedisStorage) All(sessionID string) map[string]string {users := make(map[string]string)  // Пользователей будем хранить в set, так как сортировка для нас не принципиальна.   // Заодно избавимся от необходимости искать дубликатыfor _, username := range s.redis.SMembers(s.context, fmt.Sprintf(SESSION_USERS_TPL, sessionID)).Val() {users[username] = s.redis.Get(s.context, fmt.Sprintf(USER_VOTE_TPL, sessionID, username)).Val()}return users}func (s *UserRedisStorage) Save(sessionID string, username string, value string) error {err := s.redis.SAdd(s.context, fmt.Sprintf(SESSION_USERS_TPL, sessionID), username).Err()if err != nil {return err}  // Голоса пользователей будем хранить в обычных ключах.   // Я сделал вечное хранение, но это легко можно поменять, изменив -1 на нужное значениеerr = s.redis.Set(s.context, fmt.Sprintf(USER_VOTE_TPL, sessionID, username), value, -1).Err()if err != nil {return err}return nil}

Напишем тесты:

users_test.go
package storage_testimport ("errors""fmt""go-scrum-poker-bot/storage""testing""github.com/go-redis/redismock/v8""github.com/stretchr/testify/assert")func TestAll(t *testing.T) {sessionID, username, value := "test", "user", "1"redisClient, mock := redismock.NewClientMock()usersStorage := storage.NewUserRedisStorage(redisClient)  // Redis mock требует обязательного указания всех ожидаемых команд и результаты их выполненияmock.ExpectSMembers(fmt.Sprintf(storage.SESSION_USERS_TPL, sessionID),).SetVal([]string{username})mock.ExpectGet(fmt.Sprintf(storage.USER_VOTE_TPL, sessionID, username),).SetVal(value)assert.Equal(t, map[string]string{username: value}, usersStorage.All(sessionID))}func TestSave(t *testing.T) {sessionID, username, value := "test", "user", "1"redisClient, mock := redismock.NewClientMock()usersStorage := storage.NewUserRedisStorage(redisClient)mock.ExpectSAdd(fmt.Sprintf(storage.SESSION_USERS_TPL, sessionID),username,).SetVal(1)mock.ExpectSet(fmt.Sprintf(storage.USER_VOTE_TPL, sessionID, username),value,-1,).SetVal(value)assert.Equal(t, nil, usersStorage.Save(sessionID, username, value))}func TestSaveSAddErr(t *testing.T) {sessionID, username, value, err := "test", "user", "1", errors.New("ERROR")redisClient, mock := redismock.NewClientMock()usersStorage := storage.NewUserRedisStorage(redisClient)mock.ExpectSAdd(fmt.Sprintf(storage.SESSION_USERS_TPL, sessionID),username,).SetErr(err)assert.Equal(t, err, usersStorage.Save(sessionID, username, value))}func TestSaveSetErr(t *testing.T) {sessionID, username, value, err := "test", "user", "1", errors.New("ERROR")redisClient, mock := redismock.NewClientMock()usersStorage := storage.NewUserRedisStorage(redisClient)mock.ExpectSAdd(fmt.Sprintf(storage.SESSION_USERS_TPL, sessionID),username,).SetVal(1)mock.ExpectSet(fmt.Sprintf(storage.USER_VOTE_TPL, sessionID, username),value,-1,).SetErr(err)assert.Equal(t, err, usersStorage.Save(sessionID, username, value))}

Теперь определим хранилище для "покерной" сессии. Пока там будет лежать статус видимости голосов:

sessions.go
package storageimport ("context""fmt""strconv""github.com/go-redis/redis/v8")// Шаблон для ключейconst SESSION_VOTES_HIDDEN_TPL = "SESSION:%s:VOTES_HIDDEN"type SessionRedisStorage struct {redis   *redis.Clientcontext context.Context}func NewSessionRedisStorage(redisClient *redis.Client) *SessionRedisStorage {return &SessionRedisStorage{redis:   redisClient,context: context.Background(),}}func (s *SessionRedisStorage) GetVisibility(sessionID string) bool {value, _ := strconv.ParseBool(s.redis.Get(s.context, fmt.Sprintf(SESSION_VOTES_HIDDEN_TPL, sessionID)).Val(),)return value}func (s *SessionRedisStorage) SetVisibility(sessionID string, state bool) error {return s.redis.Set(s.context,fmt.Sprintf(SESSION_VOTES_HIDDEN_TPL, sessionID),strconv.FormatBool(state),-1,).Err()}

И сразу напишем тесты для только что созданных методов:

sessions_test.go
package storage_testimport ("errors""fmt""go-scrum-poker-bot/storage""strconv""testing""github.com/go-redis/redismock/v8""github.com/stretchr/testify/assert")func TestGetVisibility(t *testing.T) {sessionID, state := "test", trueredisClient, mock := redismock.NewClientMock()mock.ExpectGet(fmt.Sprintf(storage.SESSION_VOTES_HIDDEN_TPL, sessionID),).SetVal(strconv.FormatBool(state))sessionStorage := storage.NewSessionRedisStorage(redisClient)assert.Equal(t, state, sessionStorage.GetVisibility(sessionID))}func TestSetVisibility(t *testing.T) {sessionID, state := "test", trueredisClient, mock := redismock.NewClientMock()mock.ExpectSet(fmt.Sprintf(storage.SESSION_VOTES_HIDDEN_TPL, sessionID),strconv.FormatBool(state),-1,).SetVal("1")sessionStorage := storage.NewSessionRedisStorage(redisClient)assert.Equal(t, nil, sessionStorage.SetVisibility(sessionID, state))}func TestSetVisibilityErr(t *testing.T) {sessionID, state, err := "test", true, errors.New("ERROR")redisClient, mock := redismock.NewClientMock()mock.ExpectSet(fmt.Sprintf(storage.SESSION_VOTES_HIDDEN_TPL, sessionID),strconv.FormatBool(state),-1,).SetErr(err)sessionStorage := storage.NewSessionRedisStorage(redisClient)assert.Equal(t, err, sessionStorage.SetVisibility(sessionID, state))}

Отлично! Осталось изменить main.go и server.go:

server.go
package serverimport ("context""go-scrum-poker-bot/storage""go-scrum-poker-bot/ui""go-scrum-poker-bot/web/clients""go-scrum-poker-bot/web/server/handlers""log""net/http""os""os/signal""sync/atomic""time")// Новый тип для middlewaretype Middleware func(next http.Handler) http.Handler// Все зависимости здесьtype Server struct {healthy        int32middleware     []Middlewarelogger         *log.LoggerwebClient      clients.ClientuiBuilder      *ui.BuilderuserStorage    storage.UserStoragesessionStorage storage.SessionStorage}// Добавляем их при инициализации сервераfunc NewServer(logger *log.Logger,webClient clients.Client,uiBuilder *ui.Builder,userStorage storage.UserStorage,sessionStorage storage.SessionStorage,middleware []Middleware,) *Server {return &Server{logger:         logger,webClient:      webClient,uiBuilder:      uiBuilder,userStorage:    userStorage,sessionStorage: sessionStorage,middleware:     middleware,}}func (s *Server) setupRouter() http.Handler {router := http.NewServeMux()router.Handle("/healthcheck",handlers.Healthcheck(),)router.Handle("/play-poker",handlers.PlayPokerCommand(s.webClient, s.uiBuilder),)router.Handle("/interactivity",handlers.InteractionCallback(s.userStorage, s.sessionStorage, s.uiBuilder, s.webClient),)return router}func (s *Server) setupMiddleware(router http.Handler) http.Handler {handler := routerfor _, middleware := range s.middleware {handler = middleware(handler)}return handler}func (s *Server) Serve(address string) {server := &http.Server{Addr:         address,Handler:      s.setupMiddleware(s.setupRouter()),ErrorLog:     s.logger,ReadTimeout:  5 * time.Second,WriteTimeout: 10 * time.Second,IdleTimeout:  15 * time.Second,}done := make(chan bool)quit := make(chan os.Signal, 1)signal.Notify(quit, os.Interrupt)go func() {<-quits.logger.Println("Server is shutting down...")atomic.StoreInt32(&s.healthy, 0)ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)defer cancel()server.SetKeepAlivesEnabled(false)if err := server.Shutdown(ctx); err != nil {s.logger.Fatalf("Could not gracefully shutdown the server: %v\n", err)}close(done)}()s.logger.Println("Server is ready to handle requests at", address)atomic.StoreInt32(&s.healthy, 1)if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {s.logger.Fatalf("Could not listen on %s: %v\n", address, err)}<-dones.logger.Println("Server stopped")}
main.go
package mainimport ("fmt""go-scrum-poker-bot/config""go-scrum-poker-bot/storage""go-scrum-poker-bot/ui""go-scrum-poker-bot/web/clients"clients_middleware "go-scrum-poker-bot/web/clients/middleware""go-scrum-poker-bot/web/server"server_middleware "go-scrum-poker-bot/web/server/middleware""log""net/http""os""time""github.com/go-redis/redis/v8")func main() {logger := log.New(os.Stdout, "INFO: ", log.LstdFlags)config := config.NewConfig()  // Объявляем Redis клиентredisCLI := redis.NewClient(&redis.Options{Addr: fmt.Sprintf("%s:%d", config.Redis.Host, config.Redis.Port),DB:   config.Redis.DB,})  // Наш users storageuserStorage := storage.NewUserRedisStorage(redisCLI)  // Наш sessions storagesessionStorage := storage.NewSessionRedisStorage(redisCLI)builder := ui.NewBuilder(config)webClient := clients.NewBasicClient(&http.Client{Timeout: 5 * time.Second,},[]clients.Middleware{clients_middleware.Auth(config.Slack.Token),clients_middleware.JsonContentType,clients_middleware.Log(logger),},)  // В Server теперь есть middlewareapp := server.NewServer(logger,webClient,builder,userStorage,sessionStorage,[]server.Middleware{server_middleware.Recover(logger), server_middleware.Log(logger), server_middleware.Json},)app.Serve(config.App.ServerAddress)}

Запустим тесты:

go test ./... -race -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic

Результат:

go tool cover -func coverage.txt
$ go tool cover -func coverage.txtgo-scrum-poker-bot/config/config.go:9:                                  NewConfig               100.0%go-scrum-poker-bot/config/helpers.go:10:                                getStrEnv               100.0%go-scrum-poker-bot/config/helpers.go:17:                                getIntEnv               100.0%go-scrum-poker-bot/config/helpers.go:26:                                getListStrEnv           100.0%go-scrum-poker-bot/main.go:22:                                          main                    0.0%go-scrum-poker-bot/storage/sessions.go:18:                              NewSessionRedisStorage  100.0%go-scrum-poker-bot/storage/sessions.go:25:                              GetVisibility           100.0%go-scrum-poker-bot/storage/sessions.go:33:                              SetVisibility           100.0%go-scrum-poker-bot/storage/users.go:18:                                 NewUserRedisStorage     100.0%go-scrum-poker-bot/storage/users.go:25:                                 All                     100.0%go-scrum-poker-bot/storage/users.go:34:                                 Save                    100.0%go-scrum-poker-bot/ui/blocks/action.go:9:                               BlockType               100.0%go-scrum-poker-bot/ui/blocks/button.go:11:                              BlockType               100.0%go-scrum-poker-bot/ui/blocks/context.go:9:                              BlockType               100.0%go-scrum-poker-bot/ui/blocks/section.go:9:                              BlockType               100.0%go-scrum-poker-bot/ui/blocks/select.go:10:                              BlockType               100.0%go-scrum-poker-bot/ui/builder.go:14:                                    NewBuilder              100.0%go-scrum-poker-bot/ui/builder.go:18:                                    getGetResultsText       100.0%go-scrum-poker-bot/ui/builder.go:26:                                    getResults              100.0%go-scrum-poker-bot/ui/builder.go:41:                                    getOptions              100.0%go-scrum-poker-bot/ui/builder.go:50:                                    BuildBlocks             100.0%go-scrum-poker-bot/ui/builder.go:100:                                   Build                   100.0%go-scrum-poker-bot/web/clients/client.go:22:                            NewBasicClient          100.0%go-scrum-poker-bot/web/clients/client.go:26:                            makeRequest             78.9%go-scrum-poker-bot/web/clients/client.go:65:                            Make                    66.7%go-scrum-poker-bot/web/clients/middleware/auth.go:8:                    Auth                    100.0%go-scrum-poker-bot/web/clients/middleware/json.go:5:                    JsonContentType         100.0%go-scrum-poker-bot/web/clients/middleware/log.go:8:                     Log                     87.5%go-scrum-poker-bot/web/clients/request.go:12:                           ToBytes                 100.0%go-scrum-poker-bot/web/clients/response.go:12:                          Json                    100.0%go-scrum-poker-bot/web/server/handlers/healthcheck.go:10:               Healthcheck             66.7%go-scrum-poker-bot/web/server/handlers/interaction_callback.go:12:      InteractionCallback     71.4%go-scrum-poker-bot/web/server/handlers/play_poker.go:13:                PlayPokerCommand        100.0%go-scrum-poker-bot/web/server/middleware/json.go:5:                     Json                    100.0%go-scrum-poker-bot/web/server/middleware/log.go:8:                      Log                     100.0%go-scrum-poker-bot/web/server/middleware/recover.go:9:                  Recover                 100.0%go-scrum-poker-bot/web/server/models/callback.go:52:                    getSessionID            100.0%go-scrum-poker-bot/web/server/models/callback.go:62:                    getSubject              100.0%go-scrum-poker-bot/web/server/models/callback.go:72:                    getAction               100.0%go-scrum-poker-bot/web/server/models/callback.go:82:                    SerializedData          92.3%go-scrum-poker-bot/web/server/models/errors.go:13:                      ResponseError           75.0%go-scrum-poker-bot/web/server/server.go:31:                             NewServer               0.0%go-scrum-poker-bot/web/server/server.go:49:                             setupRouter             0.0%go-scrum-poker-bot/web/server/server.go:67:                             setupMiddleware         0.0%go-scrum-poker-bot/web/server/server.go:76:                             Serve                   0.0%total:                                                                  (statements)            75.1%

Неплохо, но нам не нужно учитывать в coverage main.go (мое мнение) и server.go (здесь можно поспорить), поэтому есть хак :). Нужно добавить в начало файлов, которые мы хотим исключить из оценки следующую строчку с тегами:

//+build !test

Перезапустим с тегом:

go test ./... -race -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic -tags=test

Результат:

go tool cover -func coverage.txt
$ go tool cover -func coverage.txtgo-scrum-poker-bot/config/config.go:9:                                  NewConfig               100.0%go-scrum-poker-bot/config/helpers.go:10:                                getStrEnv               100.0%go-scrum-poker-bot/config/helpers.go:17:                                getIntEnv               100.0%go-scrum-poker-bot/config/helpers.go:26:                                getListStrEnv           100.0%go-scrum-poker-bot/storage/sessions.go:18:                              NewSessionRedisStorage  100.0%go-scrum-poker-bot/storage/sessions.go:25:                              GetVisibility           100.0%go-scrum-poker-bot/storage/sessions.go:33:                              SetVisibility           100.0%go-scrum-poker-bot/storage/users.go:18:                                 NewUserRedisStorage     100.0%go-scrum-poker-bot/storage/users.go:25:                                 All                     100.0%go-scrum-poker-bot/storage/users.go:34:                                 Save                    100.0%go-scrum-poker-bot/ui/blocks/action.go:9:                               BlockType               100.0%go-scrum-poker-bot/ui/blocks/button.go:11:                              BlockType               100.0%go-scrum-poker-bot/ui/blocks/context.go:9:                              BlockType               100.0%go-scrum-poker-bot/ui/blocks/section.go:9:                              BlockType               100.0%go-scrum-poker-bot/ui/blocks/select.go:10:                              BlockType               100.0%go-scrum-poker-bot/ui/builder.go:14:                                    NewBuilder              100.0%go-scrum-poker-bot/ui/builder.go:18:                                    getGetResultsText       100.0%go-scrum-poker-bot/ui/builder.go:26:                                    getResults              100.0%go-scrum-poker-bot/ui/builder.go:41:                                    getOptions              100.0%go-scrum-poker-bot/ui/builder.go:50:                                    BuildBlocks             100.0%go-scrum-poker-bot/ui/builder.go:100:                                   Build                   100.0%go-scrum-poker-bot/web/clients/client.go:22:                            NewBasicClient          100.0%go-scrum-poker-bot/web/clients/client.go:26:                            makeRequest             78.9%go-scrum-poker-bot/web/clients/client.go:65:                            Make                    66.7%go-scrum-poker-bot/web/clients/middleware/auth.go:8:                    Auth                    100.0%go-scrum-poker-bot/web/clients/middleware/json.go:5:                    JsonContentType         100.0%go-scrum-poker-bot/web/clients/middleware/log.go:8:                     Log                     87.5%go-scrum-poker-bot/web/clients/request.go:12:                           ToBytes                 100.0%go-scrum-poker-bot/web/clients/response.go:12:                          Json                    100.0%go-scrum-poker-bot/web/server/handlers/healthcheck.go:10:               Healthcheck             66.7%go-scrum-poker-bot/web/server/handlers/interaction_callback.go:12:      InteractionCallback     71.4%go-scrum-poker-bot/web/server/handlers/play_poker.go:13:                PlayPokerCommand        100.0%go-scrum-poker-bot/web/server/middleware/json.go:5:                     Json                    100.0%go-scrum-poker-bot/web/server/middleware/log.go:8:                      Log                     100.0%go-scrum-poker-bot/web/server/middleware/recover.go:9:                  Recover                 100.0%go-scrum-poker-bot/web/server/models/callback.go:52:                    getSessionID            100.0%go-scrum-poker-bot/web/server/models/callback.go:62:                    getSubject              100.0%go-scrum-poker-bot/web/server/models/callback.go:72:                    getAction               100.0%go-scrum-poker-bot/web/server/models/callback.go:82:                    SerializedData          92.3%go-scrum-poker-bot/web/server/models/errors.go:13:                      ResponseError           75.0%total:                                                                  (statements)            90.9%

Такой результат мне нравится больше :)

На этом пожалуй остановлюсь. Весь код можете найти здесь. Спасибо за внимание!

Подробнее..

Strategy Design Pattern

13.04.2021 22:15:19 | Автор: admin

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

Сегодня хочу с вами поговорить про шаблон проектирования Стратегия (Strategy). Постараюсь донести до вас принципы и суть шаблона без воды, и покажу как его применять на практике.

В чем суть?

Design patter Strategy или шаблон проектирования Стратегия относится к поведенческим шаблонам проектирования. Его задача - выделить схожие алгоритмы, решающие конкретную задачу. Реализация алгоритмов выносится в отдельные классы и предоставляется возможность выбирать алгоритмы во время выполнения программы.

Шаблон дает возможность в процессе выполнения выбрать стратегию (алгоритм, инструмент, подход) решения задачи.

В чем проблема?

Рассмотрим задачи, при решении которых можно применять такой подход.

Представьте, что перед вами стоит задача написать веб-портал по поиску недвижимости. MVP (Minimum ViableProduct) или минимально работающий продукт был спроектирован и приоритизирован вашей командой Product Managerов и на портале должен появиться функционал для покупателей квартир. То есть целевые пользователи вашего продукта в первую очередь - это те, кто ищет себе новое жилье для покупки. Одной из самых востребованных функций должна быть возможность:

  • Выбрать область на карте, где покупатель желает приобрести жилье

  • И указать ценовой диапазон цен на квартиры для фильтрации.

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

Но тут приходят к вам Product Manager'ы и говорят, что нужно добавить возможность искать и отображать недвижимость, которая сдается в аренду. У нас появляется еще один тип пользователя - арендаторы. Для арендаторов не так важно показывать фильтры по цене, им важно состояние квартиры, поэтому нужно отображать фотографии арендуемых квартир.

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

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

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

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

  • Основной алгоритм поиска квартир был реализован в одном супер-классе

  • Алгоритм выбора и отображения элементов интерфейса был реализован в одном супер-классе

  • Изменения в этих классах, сделанные разными программистами, приводили к конфликтам и необходимости регрессивного тестирования

  • Релизы продукта затягивались, время на разработку нового функционала увеличилась в несколько раз

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

Супер-класс с единым методом реализации алгоритма.Супер-класс с единым методом реализации алгоритма.

Какое решение?

В данном примере мы имеем несколько алгоритмов для одной функции:

  • Поиск квартир с продажей

  • Поиск квартир в аренду

  • Отображение или нет различных наборов фильтров

  • Отображение различных элементов интерфейса - фотографии, кнопки бронирования, кнопки обратной связи и т.д.

Шаблон проектирования Стратегия - решает такую задачу. Онпредлагает выделить семейство похожих алгоритмов, вынести их в отдельные классы. Это позволит без проблем изменять нужный алгоритм, расширять его, сводя к минимум конфликты разработки, зависимости от других классов и функционала. Вместо того, чтобы реализовывать алгоритм в едином классе, наш класс будет работать с объектами классов-стратегиями через объект-контекста и в нужным момент делегировать работу нужному объекту. Для смены алгоритма достаточно в нужным момент подставить в контекст нужный объект-стратегию.

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

Диаграмма классов шаблона StrategyДиаграмма классов шаблона Strategy

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

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

Несмотря на то, что каждый класс-стратегия реализует поиск по своему алгоритму, с применением необходимых фильтров (продажа, аренда, загородный дом, жилая, не жилая и т.д.), не исключено, кстати, что они будут работать с разными базами - все это для веб-интерфейса, который отображает список найденных объектов, не имеет никакого значения. После того, как пользователь выбрал интересующий его тип недвижимости в фильтрах на сайте, будет происходить запрос в контроллер на backend, с экшеном получения данных по входящим фильтрам и типам пользователя.

Задача контроллера определить класс-стратегию и запросить у класса-контекста данные для отображения, передав ему известный набор фильтров. Класс-контекст в этой схеме - это класс, которые реализует метод поиска квартир по заданным фильтрам. На диаграмме классов выше мы видим, что класс контекста определяет метод getData, и принимает аргументы filters. У него должен быть конструктор, принимающий активный в данный момент объект-стратегии и сеттерsetStrategy, устанавливающий активную стратегию. Такой метод пригодится для случая, когда пользователь меняет тип искомого объекта, например, он ищет недвижимость на продажу и хочет снять квартиру.

Пример реализации

Ниже рассмотрим пример, как решается описанная задача на языке GOlang. Первое что сделаем - определим интерфейс с методом doSearch:

Strategy.goStrategy.go

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

Для реализации конкретных алгоритмов создаем два файла. В каждом файле определяется свой определяемый тип с базовым типом struct, реализующие интерфейс Strategy. Соответственно, в методы, определяемые интерфейсом для каждого алгоритма, будут передаваться пользовательские фильтры. Реализации выглядит следующим образом:

FirstAlgorithm.goFirstAlgorithm.goSecondAlgorithm.goSecondAlgorithm.go

Посмотрим на нашу диаграмму классов. Нам осталось реализовать класс-контекста и клиентский код вызова конкретных алгоритмов в нужным момент. Как это сделать? Для создания слоя класса-контекста реализуем исходник, реализующий:

  • определяемый тип в базовым типом struct

  • функцию initStrategy, инициализирующий стратегию по-умолчанию и пользовательские фильтры

  • метод типа struct setStrategy, устанавливающий активную стратегию

  • и функция getData, вызывающий конкретную стратегию и возвращаемый данные для показа пользователю.

Context.goContext.go

Последний шаг - посмотрим на клиентский код. Сначала инициализируется стратегия по-умолчанию, получаем данные из этой стратегии. Затем выставляем новую активную стратегию и повторно вызываем функцию получения данных getData. Как видим, каждый раз вызывается одна и та же функция файла-контекста (или класса-контекста в диаграмме классов) для получения данных рендеринга, отображаемых в дальнейшем пользователю. Для простоты я вывожу отладочную информацию в консоль, чтобы убедиться, что работают разные алгоритмы при вызовах. Вот код:

Client.goClient.go

Вот вывод такого подхода:

First implements strategy map[role:1]

Second implements strategy map[role:2]

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

Объектно-ориентированный подход можно посмотреть. например, в этом курсе. Там показан пример на PHP.

Когда применять?

Напоследок поговорим когда применяется шаблон Strategy?

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

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

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

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

Подведем итог

Друзья, мы познакомились с поведенческим шаблоном проектирования Strategy. Шаблон используется для выделения схожих алгоритмов, решающих конкретную задачу. Посмотрели с вами реализацию на языке GOlang, ознакомились в возможностями подхода и разобрали когда его лучше применять.

Рад был с вами пообщаться, Alex Versus. Успехов!

Подробнее..

GoLand 2021.1 Удаленная разработка на Docker, SSH и WSL 2, поддержка Go 1.16, улучшенная работа с JSON

20.04.2021 20:11:13 | Автор: admin

Привет, Хабр!

Вышло первое крупное обновление GoLand 2021.1. Традиционно расскажем о новом в релизе и покажем, как пользоваться новой функциональностью.

Если совсем коротко, теперь вы можете:

  • собирать и запускать программы на удаленном окружении: Docker, SSH и WSL 2;

  • использовать новую функциональность Go 1.16 (например, //go:embed);

  • мгновенно генерировать код из JSON;

  • с легкостью обрабатывать ошибки с помощью новых быстрых исправлений;

  • обсуждать код и обмениваться новыми знаниями с помощью сервиса для совместной разработки и парного программирования Code With Me.

Но, это конечно, не все!

Кстати, если вы работаете с базами данных, а также с JavaScript и TypeScript, то последние обновления DataGrip и WebStorm уже включены в этот релизный билд (впрочем, как и всегда). Да, GoLand это больше, чем просто IDE для Go.

И последнее, новую функциональность можно попробовать в интерактивной форме. Урок What's New in GoLand 2021.1 уже ждет вас на начальном экране.

Run Targets

В этом релизе команда работала над одним из самых ожидаемых обновлений возможностью собирать и запускать программу на удаленном окружении Docker, SSH и WSL2 прямо из IDE.

Немного теории: добавить нужное окружение можно через меню Run | Edit Configurations. В открывшемся диалоге выбираете нужную конфигурацию, а в выпадающем списке Run on указываете окружение, на котором хотите запустить свой код.

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

А пока будем рады видеть вас в нашей документации.

Go 1.16

Поддержка //go:embed

Новости из мира Go 1.16: теперь можно встраивать файлы и папки на этапе компиляции программы в виде string (строки), []byte (слайса байтов), или с помощью embed.FS встроить сразу несколько директорий.

GoLand предоставляет подсветку кода, навигацию от директивы embed к файлам и папкам в проекте, рефакторинг Rename, Find usages (поиск использований), автодополнение кода и несколько инспекций. Например, GoLand предупредит вас, если вы попытаетесь вставить файл в переменную неправильного типа.

Обнаружение некорректного использования (t/b).Fatal

GoLand предупреждает о недопустимых вызовах testing.T метода Fatal внутри горутины, созданной во время теста.

Быстрое исправление перепишет код с вызовом t.Fatal (или аналогичного метода) так, чтобы он предупреждал о сбое теста с помощью t.Error и досрочно выходил из горутины с помощью оператора возврата (return).

Обнаружение некорректного использования asn1.Unmarshal

В Go 1.16 go vet находит неправильное использование asn1.Unmarshal и проверяет, что переменная, в которую выполняется демаршалинг (слайса байтов), не равна nil.

В GoLand 2021.1 есть аналогичная проверка с быстрым исправлением для решения этой проблемы.

Работа с JSON

Чтобы создать структуру с полями и тегами из JSON, в GoLand 2021.1 нужно просто скопировать данные в редактор кода. Это все :)

Если вы хотите отредактировать JSON перед созданием структуры, нажмите Alt+Enter, чтобы вызвать действие Generate type from JSON. GoLand откроет окно Generate Go type from the JSON с уже скопированным туда JSON. Там можно изменить данные по своему усмотрению, затем нажать кнопку Generate и ваша структура появится в редакторе. Также можно вставить JSON в окно вручную.

Кроме того, можно преобразовать JSON внутри уже существующей пустой структуры. Для этого щелкните на эту пустую структуру и вызовите действие Generate struct fields from JSON, нажав Alt+Enter или используя меню Generate.

Если вы вставили невалидный JSON-код в окно Generate Go type from JSON, GoLand вам об этом скажет.

Теперь гораздо проще создавать теги JSON и XML для всех экспортируемых полей в структуре.

Когда вы используете Alt+Enter для вызова Add key to tags в структуре или на ее поле, GoLand добавит тег ко всем полям с именами. Если вы хотите изменить стиль имени поля для всех тегов, нажмите Alt+Enter еще раз на любом ключе и выберите Change field name style in tags. Поддерживаются следующие стили: fieldName, FieldName, field-name и field_name.

Вы можете обновлять все значения тегов одновременно с помощью действия Update key value in tags. Нажмите Alt+Enter на любом значении тега в структуре, и GoLand добавит курсоры в конце значений тега каждому полю.

Кроме того, у IDE есть автодополнение для токенов JSON, XML и ASN.1 внутри значений тегов.

GoLand теперь поддерживает формат JSON Lines, в котором каждая запись находится на отдельной строке. Он используется для работы со структурированными данными и логами. IDE распознает типы файлов .jsonl, .jslines, .ldjson и .ndjson.

Файлы этих форматов содержат по несколько строк. Каждая из них является JSON-объектом и отделяется символом переноса строки. Запятые в начале и конце строки не обязательны, и вы можете не заключать содержимое файла в квадратные или фигурные скобки.

Обработка ошибок

Про обработку ошибок в Go можно найти множество статей с комментарием не так уж просто, как казалось. Мы верим в то, что наша IDE должна делать жизнь пользователей проще, поэтому мы добавили инспекцию Unhandled Error уже несколько лет назад. А чтобы жизнь была не только проще, но и веселей, теперь у этой инспекции появилось несколько быстрых исправлений:

  • Handle error присваивает результаты вызова переменным и создает проверку if error != nil.

  • Wrap error handling in a closure, для операторов defer и go, создает код, похожий на Handle error, но при этом оборачивает весь код в анонимную функцию (closure).

  • Ignore explicitly теперь сработает с defer и go. Быстрое исправление присваивает результаты вызова пустым переменным и оборачивает выражение в замыкание.

Новые быстрые исправления

Быстрое исправление несовместимости типов внутри составных литералов

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

Быстрое исправление Create Type

Create Type теперь предлагает не только создать тип, который уже используется, но еще не определен, но и создаст поля для него.

Одновременная генерация set и get методов

Создание методов get и set для каждого поля в длинной структуре не подходит под определение увлекательной задачи. Чтобы избавить вас от утомительной рутины, GoLand 2021.1 может cгенерировать все необходимые геттеры и сеттеры для выбранной структуры сразу.

Нажмите Alt+Enter на любом имени поля в структуре и выберите Generate getter and setters из выпадающего меню. GoLand откроет окно Select Fields, где вы можете выбрать методы get и set для всех полей или каких-то конкретных.

Мы понимаем, что генерация геттеров и сеттеров в большинстве случаев не нужна в Go, но если вдруг у вас будет такая задача, вы знаете как ее решить быстро :)

Рефакторинги

Extract Type

Extract Type позволяет извлечь тип из существующего или преобразовать анонимный тип структуры в именованный. Чтобы вызвать рефакторинг, нажмите Ctrl+Alt+Shift+T на Windows или Ctrl+T на macOS.

Rename модуля (go.mod)

Случается так, что иногда нужно переименовать имя модуля. Чтобы переименование происходило быстро и безболезненно, мы добавили поддержку рефакторинга Rename в go.mod файлах. Поддержка позволяет переименовать модуль без необходимости поиска по всему дереву проекта для замены имени.

Вызовите Rename на имени модуля с помощью Shift+F6 на Windows/Linux или F6 на macOS. GoLand откроет окно Rename, где можно изменить имя, настроить параметры переименования в комментариях и строках, и посмотреть предварительный результат рефакторинга. Из вкладки Refactoring Preview вы можете перейти ко всем использованиям этого имени модуля. Если результат рефакторинга вас не устроит, все изменения можно отменить.

Постфиксное автодополнение

Постфиксное автодополнение varCheckError

Введите .varCheckError после выражения, возвращающего ошибку. IDE добавит имена переменных, проверку if error != nil и вернет ошибку из функции.

Форматирование

Встроенный форматер теперь может группировать импорт точно так же, как это делает goimports -local.

Чтобы использовать эту функциональность, перейдите в Preferences/Settings | Editor | Code Style | Go | Imports и включите опцию группировки импортов. Теперь вы можете выбрать импорты, которые будут сгруппированы в отдельные блоки.

Инструмент для совместного редактирования кода

Сервис для совместной разработки и парного программирования Code With Me, который мы уже анонсировали в предыдущем релизе, поменял статус EAP на релизный и теперь работает в GoLand по умолчанию, без необходимости устанавливать плагин.

Что появилось в этой версии:

  • возможность гибко настраивать уровень доступа к проекту;

  • возможность общаться по видео и аудио, переписываться в мессенджере.

Кстати, чтобы присоединиться к сеансу и работать вместе с вами, даже не обязательно устанавливать GoLand.

Если у вашей компании повышенные требования к безопасности, можно установить локальную версию Code With Me и пользоваться сервисом в пределах внутренней сети.

Чуть больше о безопасности можно почитать в нашей документации.

На этой странице можно узнать, что именно входит в вашу лицензию.

Запуск и отладка с повышенными привилегиями

GoLand 2021.1 позволяет запускать расширенную sudo сессию. При первом запуске GoLand спросит, хотите ли вы, чтобы сессия продолжалась или прекращала свое выполнение немедленно. В Preferences/Settings | Appearance & Behavior | System Settings | Process Elevation всегда можно изменить свой ответ.

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

Автодополнение кода

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

Выберите Mark position changes in the completion popup в Preferences/Settings | Editor | General | Code Completion, чтобы опции автодополнения, ранжированные с помощью ML, были отмечены иконками стрелок вверх/вниз в выпадающем меню.

Улучшения пользовательского интерфейса

Меню быстрого доступа

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

Быстрый доступ к недавним проектам на Windows через меню 'Пуск'

Кликните правой кнопкой мыши на иконку GoLand в меню Пуск на Windows, чтобы открыть недавний проект.

Улучшенные всплывающие подсказки

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

Шрифты

Мы добавили больше вариантов шрифтов в Preferences/Settings | Editor | Font.

Другие изменения

Предварительный просмотр HTML-файлов

Мы добавили встроенное превью для HTML-файлов. Все изменения, которые вы вносите в HTML-файлы, а также в соответствующие CSS и JavaScript-файлы, мгновенно сохраняются, а превью обновляется в реальном времени в редакторе.

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

Раскрытие вкладок в режиме разделенного редактора

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

Плагин Makefile Language

Плагин Makefile Language для поддержки языка GNU Make теперь включен в IDE. Благодаря нашему коллеге, Виктору Кропп (который работал над плагином в свое свободное время (!!!)), в GoLand 2021.1 вы найдете все необходимое: подсветку кода, быстрый просмотр документации, Find Usages, навигацию и автодополнение.

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

По традиции напоминаю, что если вы уже используете IntelliJ IDEA Ultimate, то все возможности GoLand доступны прямо там, если установить этот плагин.

Спасибо за внимание!

Подробнее..

Factory Method Pattern

09.05.2021 20:20:27 | Автор: admin

Привет, друзья. С вами Alex Versus.

Ранее мы говорили про шаблоны проектирования Одиночка и Стратегия, про тонкости реализации на языке Golang.

Сегодня расскажу про Фабричный метод.

В чем суть?

Фабричный метод (Factory method) так же известный как Виртуальный конструктор(Virtual Constructor) - пораждающий шаблон проектирования, определяющий общий интерфес создания объектов в родительском классе и позволяющий изменять создаваемые объекты в дочерних классах.

Шаблон позволяет классу делегировать создание объектов подклассам. Используется, когда:

  1. Классу заранее неизвестно, объекты каких подклассов ему нужно создать.

  2. Обязанности делегируются подклассу, а знания о том, какой подкласс принимает эти обязанности, локализованы.

  3. Создаваемые объекты родительского класса специализируются подклассами.

Какую задачу решает?

Представьте, что вы создали программу управления доставкой еды. В программе в качестве единственного средства доставки используется электро-самокат. Ваши курьеры на электро-самокатах развозят еду из пункта А в пункт Б. Все просто.

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

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

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

И какое решение?

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

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

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

Посмотрим на диаграмму классов такого подхода.

Диаграмма классов Factory MethodДиаграмма классов Factory Method

Реализация на Golang

Пример реализации на PHP, можно изучить тут. Так как в Golang отсутствуют возможности ООП, такие как классы и наследование, то реализовать в классическом виде этот шаблон невозможно. Несмотря на это, мы можем реализовать базовую версию шаблона - Простая фабрика.

В нашем примере есть файл iTransport.go, который определяет методы создаваемых транспортных средств для доставки еды. Сущность транспорта будем хранить в структуре (struct), которая применяет интерфейс iTransport.

Так же реализуем файл Factory.go, который представляет фабрику создания нужных объектов. Клиентский код реализован в файле main.go. Вместо прямого создания конкретных объектов транспорта клиентский код будет использовать для этого метод фабрики getTransport(t string), передавая нужный тип объекта в виде аргумента функции.

Когда применять?

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

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

Какие преимущества?

  1. Избавляет слой создания объектов от конкретных классов продуктов. Выделяет код производства продуктов в одно место, упрощая поддержку кода.

  2. Упрощает добавление новых продуктов в программу.

  3. Реализует принцип открытости/закрытости (англ. openclosed principle, OCP) принцип ООП, устанавливающий следующее положение: программные сущности (классы, модули, функции и т. п.) должны быть открыты для расширения, но закрыты для изменения

Какие недостатки?

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

Итог

Используйте шаблон Фабричный метод в случае, когда вы хотите без проблем внедрять в вашу программу новые объекты с новыми конфигурациям для взаимодействия с основной бизнес логикой.

Рад был поделиться материалом, Alex Versus. Публикация на английском.
Всем удачи!

Подробнее..

Перевод Запускаем Golang на Jupyter Notebook

13.05.2021 16:14:30 | Автор: admin

Если вы знакомы с Python, то уже сталкивались с Jupyter Notebook или работали в нём по крайней мере один раз. Jupyter Notebook это удобный инструмент, позволяющий писать мини-код и отслеживать его выполнение. Он также помогает в документировании, ведении журнала и в том, чтобы поделиться своими работами с коллегами.

Неудивительно, что многие люди и крупные организации, такие как Netflix, для своих целей в разработке предпочитают Jupyter Notebook. Специально к старту нового потока курса по разработке на Go 26 мая мы решили поделиться переводом, автор которого рассказывает, как документировать проекты на Golang в Jupyter Notebook.


Если вы работаете на машине с Windows, потребуется установка Docker. Пожалуйста, следуйте этим инструкциям. Если вы работаете на Mac или Linux, вы можете либо использовать метод с docker выше, либо следовать процессам локальной установки, о которых я напишу ниже.

Содержание

  1. Установка.

  2. Запуск Jupyter Notebook.

  3. Написание простой программы.

Установка

Установка может показаться сложной, но я постараюсь сделать её как можно проще. Если при настройке вы столкнулись с какими-либо трудностями, пожалуйста, обратитесь к FAQ по устранению неполадок gophernote.

Я приведу три основных метода установки, хотя сам рекомендовал бы подход с *Docker*, поскольку он не зависит от операционной системы, то есть сможет работать на любой операционной системе.

1. Докер (рекомендация)

Вот основная команда:

$ docker run -it -p 8888:8888 -v /path/to/local/notebooks:/path/to/notebooks/in/docker gopherdata/gophernotes:latest-ds

Тег latest-ds указывает докеру, чтобы он извлёк версию пакета gophernotes, где уже установленные библиотеки Data Science, такие как GoNum, GoLearn и GoDa. Команда на вашей машине может выглядеть так:

$ docker run -it -p 8888:8888 -v /home/user/Documents/notebook:/notebook gopherdata/gophernotes:latest-ds

Затем вам будут предоставлены URL-адрес локального хоста подключённого блокнота и соответствующий ему токен. Скопируйте и вставьте его в свой браузер (например localhost:8888/?token=<your_given_token>).

Успешное монтирование Notebok в Docker (изображение от автора)Успешное монтирование Notebok в Docker (изображение от автора)

Кроме того, вы сможете увидеть папку notebook, которую указали при инициализации контейнера docker в браузере.

Папка блокнота, которую вы указали, когда инициализировали Docker (изображение от автора)Папка блокнота, которую вы указали, когда инициализировали Docker (изображение от автора)

Следуйте приведённым ниже инструкциям, если предпочитаете локальную установку. Однако они работают только для Linux и Mac. Машины с Windows в настоящее время не поддерживаются, и вы должны использовать вышеупомянутый метод Docker.

2. Linux

Вот команды локальной установки для Linux:

$ env GO111MODULE=on go get github.com/gopherdata/gophernotes$ mkdir -p ~/.local/share/jupyter/kernels/gophernotes$ cd ~/.local/share/jupyter/kernels/gophernotes$ cp "$(go env GOPATH)"/pkg/mod/github.com/gopherdata/gophernotes@v0.7.2/kernel/*  "."$ chmod +w ./kernel.json # in case copied kernel.json has no write permission$ sed "s|gophernotes|$(go env GOPATH)/bin/gophernotes|" < kernel.json.in > kernel.json

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

$ "$(go env GOPATH)"/bin/gophernotes

И вы сможете открыть блокнот этой командой:

$ jupyter --data-dir

3. Mac

Аналогично локальную установку для Mac можно выполнить, написав в терминале следующие команды:

$ env GO111MODULE=on go get github.com/gopherdata/gophernotes$ mkdir -p ~/Library/Jupyter/kernels/gophernotes$ cd ~/Library/Jupyter/kernels/gophernotes$ cp "$(go env GOPATH)"/pkg/mod/github.com/gopherdata/gophernotes@v0.7.2/kernel/*  "."$ chmod +w ./kernel.json # in case copied kernel.json has no write permission$ sed "s|gophernotes|$(go env GOPATH)/bin/gophernotes|" < kernel.json.in > kernel.json

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

$ "$(go env GOPATH)"/bin/gophernotes

Теперь можно открыть блокнот этой командой:

$ jupyter --data-dir

Фух, переварить такое довольно трудно. Переходим к частям веселее!

Запуск Jupyter Notebook

Теперь, когда вы настроили Gophernotes, перейдите в папку, где хотите хранить свои блокноты Golang, там мы создадим наш первый блокнот! В правом верхнем углу вы увидите новую кнопку. Нажмите на неё и выберите "Go" в качестве ядра блокнота.

Создание первого блокнота Go в Jupyter (изображение от автора)Создание первого блокнота Go в Jupyter (изображение от автора)

Как только вы сделаете это, вас встретит знакомый чистый блокнот Jupyter. Теперь первым делом нужно изменить название на My First Golang Notebook (или любое другое, как показано ниже):

Изменение названия блокнотаИзменение названия блокнота

Давайте напишем какую-нибудь простую программу в наш Golang Notebook.

Рекурсивный факториал

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

Импорт пакетов в блокнот GoИмпорт пакетов в блокнот Go

Теперь напишем рекурсивный факториал. Факториал числа n это произведение всех положительных целых чисел, меньших или равных n. Например 3!, то есть факториал числа 3, это 3 x 2 x 1 = 6. Записать функцию вычисления факториала можно в одну из ячеек Jupyter Notebook:

Рекурсивный факториал на GoРекурсивный факториал на Go

Осталось только запустить программу, которая распечатывает значение, вот так:

Вызов рекурсивной функции вычисления факториала и печать значенияВызов рекурсивной функции вычисления факториала и печать значения

Бонус

Мощь Jupyter Notebook в возможности аннотирования и комментирования без загромождения кодовой базы. Воспользоваться этими возможностями можно, изменив тип ячейки на markdown, то есть выделить ячейку, нажать ctrl+M и ввести соответствующие примечания.

Комментирование и аннотирование кодовой базы (изображение от автора)Комментирование и аннотирование кодовой базы (изображение от автора)

Заключение

Вот и всё. Теперь вы можете создать прототип приложения Golang и отслеживать его с помощью блокнота Jupyter. Вы можете поделиться этим блокнотом со своими друзьями или коллегами в целях документирования (или если вы хотите помочь новым разработчикам выполнить логический шаг, который выполняете, когда пишете код самостоятельно). Ссылка на репозиторий gophernotes.

А если вам не хочется ограничиваться столь простыми программами на Go, обратите внимание на специальный курс Backend-разработчик на Go. У всех поступивших на курс появляется поддержка в виде экспертов, готовых ответить на вопросы и пояснить нюансы языка. Хотите дополнить свой арсенал навыков умением кодить на GO добро пожаловать. А по ссылке можно ознакомиться с программой курса.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Перевод Go справляемся с конфликтами при блокировках с помощью пакета Atomic

13.05.2021 20:21:56 | Автор: admin

Перевод материала подготовлен в рамках курса "Golang Developer. Professional". Если вам интересно узнать подробнее о курсе, приглашаем на день открытых дверей онлайн.


Эта статья берет за основу Go 1.14.

Go предоставляет механизмы синхронизации памяти, такие как канал (channel) или мьютекс (mutex ), которые помогают решать различные проблемы. Касательно разделяемой памяти, мьютекс защищает память от гонки данных. Однако, несмотря на существование двух типов мьютексов, в целях повышения производительности Go также предоставляет атомарные примитивы памяти в пакете atomic. Но давайте сначала вернемся к гонкам данных, прежде чем углубляться в решения.

Гонка данных

Гонка данных (data race) может возникать, когда две или более горутины одновременно обращаются к одной и той же области памяти, и хотя бы одна из них выполняет запись. В то время как map имеет собственный механизм защиты от гонки данных, простые структуры их не имеют, что делает их уязвимыми к этой проблеме.

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

Выполнение этого кода ясно показывает, что результат недетерминирован из-за гонки данных:

[...]&{[79167 79170 79173 79176 79179 79181]}&{[79216 79219 79220 79221 79222 79223]}&{[79265 79268 79271 79274 79278 79281]}

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

WARNING: DATA RACERead at 0x00c0003aa028 by goroutine 9:  [...]  fmt.Printf()      /usr/local/go/src/fmt/print.go:213 +0xb5  main.main.func2()      main.go:30 +0x3bPrevious write at 0x00c0003aa028 by goroutine 7:  main.main.func1()      main.go:20 +0xfe

Защита чтения и записи от гонок данных может быть реализована с помощью мьютекса или (что является наиболее распространенным решением) пакетом atomic.

Mutex vs Atomic

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

Теперь программа выведет ожидаемый результат; числа увеличились как и должны были:

[...]&{[213 214 215 216 217 218]}&{[214 215 216 217 218 219]}&{[215 216 217 218 219 220]}

Второе решение может быть выполнено благодаря пакету atomic. Вот код:

Результат также является вполне ожидаемым:

[...]&{[32724 32725 32726 32727 32728 32729]}&{[32733 32734 32735 32736 32737 32738]}&{[32753 32754 32755 32756 32757 32758]}

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

Производительность

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

Выполнение теста десять раз бок о бок дает следующие результаты:

name                              time/opAtomicOneWriterMultipleReaders-4  72.2ns  2%AtomicMultipleReaders-4           65.8ns  2%MutexOneWriterMultipleReaders-4    717ns  3%MutexMultipleReaders-4             176ns  2%

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

Для получения дополнительной информации о пакете trace я предлагаю вам прочитать мою статью Go: Discovery of the Trace Package..

Вот профиль программы, использующей пакет atomic:

Горутины работают без перерывов и могут выполнять свои задачи. Что касается профиля программы с мьютексом, картина совсем другая:

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

Время блокировки составляет примерно треть всего времени. Это можно детализировать из профиля блокирующего:

Пакет atomic определенно дает преимущество в этом случае. Однако в некоторых случаях производительность может снизиться. Например, если вам нужно сохранить большую map, вам придется копировать ее каждый раз при обновлении map, что делает ее неэффективной.

Для получения дополнительной информации о мьютексах я предлагаю вам прочитать мою статью Go: Mutex and Starvation.


Узнать подробнее о курсе "Golang Developer. Professional"

Смотреть демо-урок Форматирование данных

Подробнее..

Перевод Go Управление обработкой множественных ошибок

20.05.2021 16:23:11 | Автор: admin

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

Одна горутина, несколько ошибок

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

Эта программа считывает и анализирует CSV-текст и отображает найденную ошибку. Было бы намного удобнее группировать ошибки, чтобы получить полный отчет. Чтобы объединить ошибки в одну, у нас есть выбор между двумя отличными пакетами:

  • Используя go-multierror от HashiCorp, несколько ошибок можно объединить в одну стандартную ошибку:

Затем можно вывести отчет:

Реализация здесь аналогична, вот результат:

Ошибки объединяются через точку с запятой без какого-либо другого форматирования.

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

name                    time/op         alloc/op        allocs/opHashiCorpMultiErrors-4  6.01s  1%     6.78kB  0%     77.0  0%UberMultiErrors-4       9.26s  1%     10.3kB  0%      126  0%

Реализация Uber немного медленнее и потребляет больше памяти. Однако этот пакет был разработан для группировки ошибок после их сбора, а не для итеративного добавления при каждом их возникновении. При группировании ошибок результаты близки, но код менее элегантен, поскольку требуется дополнительный этап. Вот обновленные результаты:

name                    time/op         alloc/op        allocs/opHashiCorpMultiErrors-4  6.01s  1%     6.78kB  0%     77.0  0%UberMultiErrors-4       6.02s  1%     7.06kB  0%     77.0  0%

Оба пакета используют интерфейс Go error со своей реализацией функции Error() string.

Одна ошибка, несколько горутин

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

Начнем с программы, которая использует несколько горутин для выполнения ряда действий; каждое из них длится одну секунду:

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

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

go run .  0.30s user 0.19s system 14% cpu 3.274 total

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

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

Теперь программы работают быстрее, поскольку они распространяют отмененный ошибкой контекст:

go run . 0.30s user 0.19s system 38% cpu 1.269 total

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


Перевод подготовлен в рамках набора студентов на курс "Golang Developer. Professional".

Всех желающих приглашаем на открытый вебинар Форматирование данных. На этом demo-занятии рассмотрим:
- кодировки quoted-printable и base64;
- текстовые форматы JSON, XML и YAML;
- использование структур и интерфейсов для парсинга данных;
- сравнение бинарных сериализаторов: gob, msgpack и protobuf.
После занятия вы сможете сериализовывать и десериализовывать данные различных форматов стандартными средствами языка и сторонними библиотеками. Присоединяйтесь!

Подробнее..

Prototype Design Pattern в Golang

24.05.2021 18:21:26 | Автор: admin

Привет друзья! С вами Алекс и я продолжаю серию статей, посвящённых применению шаблонов проектирования в языке Golang.

Интересно получать обратную связь от вас, понимать на сколько применима данная область знаний в мире языка Golang. Ранее уже рассмотрели шаблоны: Simple Factory, Singleton и Strategy. Сегодня хочу рассмотреть еще один шаблон проектирования - Prototype.

Для чего нужен?

Это порождающий шаблон проектирования, который позволяет копировать объекты, не вдаваясь в подробности их реализации.

Какую проблему решает?

Представьте, у вас есть объект, который необходимо скопировать. Как это сделать? Создать пустой объект такого же класса, затем поочерёдно скопировать значения всех полей из старого объекта в новый. Прекрасно, но есть нюанс! Не каждый объект удается скопировать таким образом, ведь часть его состояния может быть приватной, а значит - недоступной для остального кода программы.

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

Какое решение?

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

Реализация этого метода в разных классах очень схожа. Метод создаёт новый объект текущего класса и копирует в него значения всех полей собственного объекта. Так получится скопировать даже приватные поля, так как большинство языков программирования разрешает доступ к приватным полям любого объекта текущего класса. Объект, который копируют, называется прототипом, отсюда и название шаблона. Когда объекты программы содержат сотни полей и тысячи возможных конфигураций, прототипы могут служить своеобразной альтернативой созданию подклассов. Шаблон прототип должен копировать объекты любой сложности без привязки к их конкретным классам.

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

Диаграмма классов

Prototype Class DiagramPrototype Class Diagram

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

Как реализовать?

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

Рассмотрим пример реализации рубрикатора каталога продуктов в интернет-магазине с большим количеством категорий товаров. В рубрикаторе есть разделы верхнего уровня, в которых содержатся конечные рубрики и разделы второго уровня. Разделы второго уровня также могут содержать рубрик и разделы третьего уровня и т.д. То есть это древовидная структура объектов, у них есть стволы и листья - конечные элементы дерева. Каждая рубрика и раздел имеет свой набор свойств, относительно товаров раздела. Например, рубашки - это цвет, размер, бренд и т.д.

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

Каждую рубрика, как конечный элемент рубрикатора, может быть представлен интерфейсом prototype, который объявляет функцию clone. За основу конкретных прототипов рубрики и раздела мы берем тип struct, которые реализуют функции show и clone интерфейса prototype.

Итак, реализуем интерфейс прототипа. Далее мы реализуем конкретный прототип directory, который реализует интерфейс prototype представляет раздел рубрикатора. И конкретный прототип для рубрики. Обе структуру реализуют две функции show, которая отвечает за отображение конкретного контента ноды и clone для копирования текущего объекта. Функция clone в качестве единственного параметра принимает аргумент, ссылающийся на тип указателя на структуру конкретного прототипа - это либо рубрика, либо директория. И возвращает указатель на поле структуры, добавляя к наименованию поля _clone.

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

Open directory 2  Directory 2    Directory 1        category 1    category 2    category 3Clone and open directory 2  Directory 2_clone    Directory 1_clone        category 1_clone    category 2_clone    category 3_clone

Когда применять?

  1. У вас много объектов с множеством свойств и вам нужно создавать их клоны быстро и эффективно. Шаблон предлагает использовать набор прототипов, вместо создания подклассов для описания популярных конфигураций объектов. Вместо порождения объектов из подклассов, вы копируете существующие объекты-прототипы, в которых уже настроено состояние. Тем самым избегая роста количества классов в программе и уменьшая её сложность.

  2. Код не должен зависеть от классов копируемых объектов. Если ваш код работает с объектами, переданными через общий интерфейс - вы не можете привязаться к их классам, даже если бы хотели, поскольку их конкретные классы неизвестны. Прототип предоставляет клиенту общий интерфейс для работы со всеми прототипами. Клиенту не нужно зависеть от классов копируемых объектов, а только от интерфейса клонирования.

Итог

Друзья, шаблон Prototype предлагает:

  • Удобную концепцию для создания копий объектов.

  • Помогает избежать дополнительных усилий по созданию объекта стандартным путём, когда это непозволительно дорого для приложения.

  • В объектных языках позволяет избежать наследования создателя объекта в клиентском приложении, как это делает паттерн abstract factory, например.

Кстати, друзья, вот тут можно посмотреть результаты опроса читателей хабра. 63% опрошенных считают, что применение шаблонов проектирования в Golang - это зло. Связано, скорее всего, с тем, что язык Golang процедурный и ему чужды подходы объектно-ориентированных языков. Но рассматривать реализации и применение шаблонов стоит, так как это позволяет больше их понимать и периодически применять для решения тех или иных задач. Каждый подход требует, конечно, дискуссии и разумного применения.

Друзья, рад был поделиться темой, Алекс. На английском статью можно найти тут.
Удачи!

Подробнее..

Чиним проблемы нагрузок в Go с помощью настройки пула HTTP-соединений

09.06.2021 20:16:20 | Автор: admin

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

Проблема скрывалась внутри нашего API Gateway. Это сервис, который реализует паттерн Фасад и предоставляет единственное торчащее наружу окно к микросервисам.

В упрощенном виде его работу можно представить так:

  1. Проверить аутентификацию и авторизацию с помощью HTTP-запроса в сервис аутентификации

  2. Спроксировать запрос в нужный сервис, который определяем по пути и методу запроса пользователя

Иллюстрация работы API GatewayИллюстрация работы API Gateway

Конец декабря время роста нагрузок и числа ошибок

Настал конец декабря. Вместе с ним к нам в поддержку стали приходить вот такие обращения:

При работе в ЛК возникают постоянные ошибки системные по 10-20 раз на дню и больше. Просьба исправить и наладить работу площадки.

Ошибки возникали на стороне API Gateway. Мы полезли в логи за подробностями и увидели ошибки, похожие на таймауты обращения к сервису аутентификацию:

{err_type: context.deadlineExceededError, err: context deadline exceeded}{err_type: *errors.errorString, err: context canceled}

Трейсы в Jaeger показали ровно такую же картину мы не дожидались ответа от сервиса аутентификации за 2 секунды. Поэтому между нами и разработчиками сервиса аутентификации произошёл примерно такой диалог:

- Ребята, кажется, вы таймаутите. Вот трейс, на котором видно, что мы не дождались от вас ответа за 2 секунды.

- Ничего подобного, у нас все норм мы за 200 миллисекунд отвечаем в 99% запросов. А вот вы по какой-то причине часто преждевременно обрываете соединение.

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

Скриншот с множеством ошибок Cancelled by clientСкриншот с множеством ошибок Cancelled by client

Итого, мы имеем:

  1. Используемый нами сервис аутентификации стабильно отрабатывает за 200 миллисекунд.

  2. Многие наши обращения к этому сервису таймаутят за 2 секунды.

Причина проблемы: дефолтные настройки в Go

В это время один из авторов нашего API Gateway отметил, что очень давно заметил аномалию: сервис открывает неожиданно много соединений к удаленным портам. При запуске команды из-под контейнера видно:

$ ss -natp state time-wait | awk '{print $4}' | sort -nr | uniq -c | sort -nr | head1053 10.20.49.117:801030 10.20.49.92:801016 10.20.49.91:801014 10.20.54.129:801013 10.20.53.213:801008 10.20.53.173:80969 10.20.53.172:80

Эта команда показывает количество TCP-сокетов в состоянии TIME_WAIT до разных удалённых портов. Если коротко, то состояние TIME_WAIT это де-факто закрытое клиентом соединение. Linux по возможности предотвращает повторное использование этих пар на протяжении 60 секунд, чтобы защититься от того, что старые пакеты помешают вновь установленному TCP-соединению.

Но для нас важно другое. Само существование TCP-соединения означает, что соединение установилось и закрылось. Если такая ситуация происходит массово, то мы имеем дело с накладными расходами на DNS-резолвинг и установку соединения. В результате этого время HTTP-запроса может увеличиваться. Избежать эту проблему помогают пулы соединении. В Go для этои цели используется абстракция http.Transport.

Здесь мы вплотную приближаемся к истокам проблемы. Мы для всех клиентских запросов использовали http.DefaultTransport. Он обладает следующими параметрами:

var DefaultTransport RoundTripper = &Transport{    Proxy: ProxyFromEnvironment,    DialContext: (&net.Dialer{        Timeout:   30 * time.Second,        KeepAlive: 30 * time.Second,    }).DialContext,    ForceAttemptHTTP2:     true,    MaxIdleConns:          100,    IdleConnTimeout:       90 * time.Second,    TLSHandshakeTimeout:   10 * time.Second,    ExpectContinueTimeout: 1 * time.Second,}

Среди перечисленных выше параметров к настройке пула соединений имеют отношения два:

  • MaxIdleConns число соединений, которое разрешается иметь в состоянии Idle (т.е. открытых TCP-соединений, которые в данный момент не используются);

  • IdleConnTimeout время, через которое закрываются такие неактивные соединения.

Однако в DefaultTransport совершенно не указан другой ключевой параметр MaxIdleConnsPerHost. Он отвечает за то, сколько неактивных TCP-соединений допускается устанавливать на один хост.

При этом если MaxIdleConnsPerHost не указан, тогда используется значение по умолчанию:

const DefaultMaxIdleConnsPerHost = 2

Поскольку мы использовали именно http.DefaultTransport для всех запросов, мы получили следующую проблему.

Представим, что нам одновременно понадобилось установить 10 соединений до сервиса аутентификации. Тогда хотя бы для 8 из них будут открыты и вскоре тут же закрыты TCP-соединения, из-за ограничения MaxIdleConnsPerHost. Если такая ситуация будет повторяться часто, у нас будет больше накладных расходов на один HTTP-запрос, поскольку для него понадобится новое соединение. Из-за этого вероятность таймаутов возрастает.

Решение: отдельный транспорт с особенными настройками

Чтобы решить проблему, мы сделали следующее:

  • Выделили отдельный транспорт под сервисы аутентификации и авторизации, чтобы транспорт под сервис аутентификации не вытеснял остальные сервисы.

  • Утилизировали выделенный пул на полную сделали так, чтобы значение MaxIdleConnsPerHost соответствовало значению MaxIdleConns:

func createOneHostTransport() *http.Transport {    result := http.DefaultTransport.(*http.Transport).Clone()    result.MaxIdleConnsPerHost = result.MaxIdleConns    return result}
График response time обращения к сервису аутентификацииГрафик response time обращения к сервису аутентификации

Тут видно значительное уменьшение 0.99-квантиля по времени обращения графиков (голубой цвет) с 2-3 секунд до менее 300 миллисекунд. Должен признать, даже после этого мы изредка видели таймауты при обращении к сервису аутентификации. Но теперь мы хотя бы видели эти же таймауты на графиках другого сервиса.

Но почему в Go такие настройки по умолчанию?

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

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

Если бы приложения могли говорить...

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

Приложение: http.Transport, привет! Нам тут сотня пользователей пришла одновременно и им всем вдруг срочно понадобилось посмотреть инфу о своём профиле. Установи, пожалуйста, сотню соединений к user-service

http.Transport: Конечно, вот получи сотню установленных соединений под запросы! Но я их не буду ещё полторы минуты закрывать, вдруг пригодятся .

Приложение: Всё сработало, спасибо! Только вот теперь им всем вдруг захотелось посмотреть информацию о товаре. Пожалуйста, установи соединение с product-service.

http.Transport: Да, не вопрос держи. Но только я их у себя в пуле соединений хранить не буду, поскольку он уже полностью забит соединениями до user-service, которые тебе больше не нужны.

Приложение: (_)

Разбираемся с непонятными таймаутами в Go: чеклист

Если вдруг вы сталкиваетесь с непонятными таймаутами, попробуйте следующее:

  1. Проверьте метрики по скорости выполнения запросов к сторонним сервисам по HTTP. Если таких метрик нет, заведите пригодятся.

  2. Если видите расхождения в таймаутах клиента и сервера, проверьте количество соединений TIME_WAIT.

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

  4. Для хоста с неоправданно большим числом запросов к нему рассмотрите вариант заведения отдельного транспорта.

Что ещё почитать по теме

Подробнее..

Fintech на практике как Quadcode технологии для трейдинга и банкинга разрабатывает

01.06.2021 12:20:22 | Автор: admin

Привет, самое хардовое IT комьюнити Рунета! Я Саша, главный архитектор в компании Quadcode. Мы пришли на Хабр для того, чтобы показать кухню Fintech варимся мы во всем этом 8 лет, поэтому уже можем поделиться опытом. В своем блоге будем рассказывать об архитектурах, технологиях, инструментах и лайфхаках.

Этот пост первый в списке, его можно считать знакомством. Под катом я расскажу про структуру нашей команды, про продукты Quadcode это платформа для трейдинга, банкинг и внутренние разработки, а также про наши первые шаги на пути к IPO.

Наша команда

Команда Quadcode уже 8 лет работает в финтехе. Цель компании создавать удобные финтех-инструменты для B2B клиентов со всего мира.

В разработке мы руководствуемся Agile принципами, да и в принципе склонны к гибким методологиям. Именно они позволяют достигать баланса в скорости и качестве разработки продуктов, поэтому разработка представляет из себя набор Scrum команд.

Во главе каждой команды стоит Team Lead. Сами команды сгруппированы в отделы, работающие над определенными предметными областями. Например, есть отдел Finance Development, в котором команды разрабатывают финансовые сервисы для платформы. Есть ветка, где располагаются владельцы продукта (product owners), задача которых развивать и улучшать наши продукты. Сейчас у нас в разработке 230+ опытных (реально опытных, у каждого много лет практики) специалистов. Это порядка 24 команд и 6 Product Owners. Джуниоров мы берем редко. Но с каждым годом искать опытных специалистов становится все сложнее, так что все больше в эту сторону смотрим.

Задачи по разработке выстраиваются на основе продуктовых Roadmap. Это план развития продукта с целью получения определенных бизнес-показателей. Роадмап выстраивается для каждого продукта и может быть составлен на разные временные промежутки: полгода, год, три года и т.д. Из готовых продуктовых роадмапов выстраивается общий план: когда, какие фичи и для каких продуктов должны быть сделаны.

Роадмап в нашем понимании это связующее звено между бизнесом, продуктом и разработкой.

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

Работаем удаленно или в офисе в Санкт-Петербурге каждый выбирает самостоятельно. Я для себя выбрал работу из дома, но по офису тоже скучаю, он у нас классный с тренажерным залом, массажным кабинетом и прочими плюшками.

Технологический стек

Наши основные языки для разработки Golang и C++. Из дополнительных технологий на бэкенде PHP, Python, NodeJS, на фронте JavaScript (ReactJS), в аналитике Python, Scala, а в автотестах Java.

Инфраструктура в компании гибридная. Мы арендуем собственные сервера в датацентрах. Все stateless приложения стараемся эксплуатировать в Kubernetes, если для этого нет ограничений, хотя бывает и такое. Kubernetes-кластера также преимущественно работают на наших серверах. То, что требует гарантированных ресурсов, например нагруженные базы данных, мы эксплуатируем на железе. Конечно, используем и облака там, где это приносит пользу. Например в задачах, где требуется обработать большое количество данных, чтобы предоставить отчет заказчику. Для таких задач нужно временно получить ресурсы для анализа, но после получения результата они не нужны.

Для точечных целей применяем технологии, которые позволяют решить специфические задачи. Например, наше Desktop приложение под Windows, Mac и Web написано на С++ и имеет единую кодовую базу. В данном случае С++ дает нам кроссплатформенность и отличную производительность при рендере графики. Однако мы практически не используем С++ для Backend разработки, потому что это дорого. Основной язык разработки для Backend у нас Go. В то же время мы не используем его как инструмент для тестирования. Для этих целей применяем Java, так как это намного удобнее и является уже практически промышленным стандартом в индустрии.

Какие продукты создает команда Quadcode

Наш флагманский продукт платформа для трейдинга. За 7 лет развития количество пользователей платформы выросло с 950 тысяч до 88 миллионов в 170+ странах.

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

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

А теперь кратко о наших продуктах:

SaaS Trading Platform

Команда с нуля разработала платформу с аптаймом 99.5%, на базе которой более 7 лет успешно функционирует брокер.

Платформа предоставляет клиенты под Windows, MacOS, Anrdoid, iOS, а также WEB трейдрум.

На платформе можно торговать следующими инструментами:

  • Digital опционы

  • FX опционы

  • CFD

  • Forex

  • Crypto и др.

Основной язык для разработки платформы Golang. Платформа начала свое существование с монолитной архитектуры классического для своего времени стека: PHP+PostgreSQL+Redis+JS.

Через 3 года эксплуатации было решено перейти на микросервисную архитектуру, так как монолит уже не давал гибкости и не мог обеспечить необходимые темпы разработки. С миграцией на микросервисную архитектуру мы также ушли с PHP в сторону Go, о чем не жалеем.

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

С прошлого года наша платформа развивается как SaaS решение. На базе решения любой желающий может без больших усилий организовать своего собственного брокера, все есть в коробке под ключ: трейдинговый сервис, процедуры KYC, биллинг, support, crm. Словом, все, чтобы быстро стартануть бизнес. Любого нового брокера можно поднять за месяц. Чтобы обеспечить вариативность в функционале, мы разрабатываем гибкую систему модулей для SaaS-решения.

* Для того, чтобы наглядно объяснить, что такое SaaS, и показать, куда мы в итоге хотим прийти, приведем пример с пиццей. Это так называемая модель Pizza-as-a-service, вкусно и полезно.* Для того, чтобы наглядно объяснить, что такое SaaS, и показать, куда мы в итоге хотим прийти, приведем пример с пиццей. Это так называемая модель Pizza-as-a-service, вкусно и полезно.

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

Сейчас добиваемся того, чтобы в экосистеме платформы был максимально широкий спектр инструментов: Forex, СFD и инвестиционные продукты в удобной для пользователя форме. Идеальный вариант сделать платформу подходящей как для банков, так и для их клиентов. Мы собираем паззл продукта из мельчайших деталей. Процесс этот не такой быстрый, но пока все получается. Быстро и не получится ни в правовом плане, ни в плане технологий.

Примеры задач, которые стоят перед командой в этом году:

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

  2. Также один из крупных проектов это разработка собственного движка Margin Forex & MCFD.

  3. Проработка Prediction Churn. Фича основана на анализе данных и предсказывает момент, когда пользователь решит уйти. Сейчас результат Prediction Churn достоверен с вероятностью 82%. Когда система предсказывает, что пользователь готов уйти с платформы,в работу включаются менеджеры, чтобы создать удобные для трейдера условия работы на платформе. Это позволяет продлить срок работы с трейдером. Чем дальше, тем точнее будет работать Prediction Churn, и тем лучше мы сможем держать контакт с пользователем.

Banking

Это второй наш продукт. В основе направления находится собственный лицензированный провайдер финансовых услуг, который зарегистрирован в Великобритании. Продукт предоставляет следующие функции B2B и B2C клиентам:

  • Дистанционный онбординг для физических и юридических лиц.

  • Доступ к счету через мобильное приложение и онлайн-банкинг.

  • Мультивалютные счета в формате IBAN.

  • SEPA, TARGET2 и SWIFT переводы.

  • Выпуск пластиковых и виртуальных карт.

Технологический стек классический: ядро системы работает под управлением JAVA. А также применяется PHP+JS для реализации административных интерфейсов управления и web приложений.

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

Внутренние разработки

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

Из наиболее интересных можно выделить следующие:

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

  2. Sandbox. В сложных многокомпонентных, а в нашем случае системах с большим количеством сервисов, всегда возникает проблема с тестированием. Важно иметь возможность получать воспроизводимое окружение для тестирования, так называемые тестовые стенды. Еще в самом начале пути мы создали Sandbox систему, с помощью которой можно собирать копии платформы с различными конфигурациями. Это своего рода конструктор, куда можно зайти, выбрать какая функциональность нужна - будет создана сборка, запущены необходимые микросервисы и можно тестировать. Все это работает на базе Docker + Kubernetes.

  3. Central Information System. Всегда возникает необходимость в инструменте, который может объединить в себе все системы компании. Речь не только про разработку, но и про КДП, HR, Финансовый отдел. Такая система должна помогать находить ответы на различные вопросы. Например, что за команда такая A, какие у нее сотрудники, кто руководитель, какой у нее ФОТ, что она сделала за прошедший квартал. И плюс еще много всяких индивидуальных хотелок. Найти такой продукт, имеющий в себе все, достаточно проблематично, да и выглядят такие системы довольно монструозно. Хороший пример SAP. Мы же вкладываемся в собственную разработку такой системы, которая реализует все потребности различных отделов и интегрируется с другими системами: Gitlab, таск трекер, финансовые системы (1C).

Вместо заключения

За 2020 мы проделали большой путь по разработке SAAS решения и внедрения нового банкинг продукта, сейчас появилось еще несколько важных целей. Компания использует стратегическое планирование, мы движемся в сторону присутствия на рынках всех стран, удвоения показателя EBITDA и выхода на IPO.

В будущих статьях на Хабре мы расскажем более подробно о нашем подходе к разработке, планированию и работе с командами. Вместо рекламной паузы ссылка на наши вакансии. Если остались вопросы, то пишите в ТГ @wolverinoid.

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

Подробнее..

Вызов кода Go из Dart с использованием cgo и Dart FFI на простом примере

09.06.2021 16:12:52 | Автор: admin

Ключевой мотивацией для написания данной статьи является факт сильного недостатка информации (особенно в русскоязычном сообществе) по использованию cgo и Dart FFI для вызова Go кода из языка Dart.

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

В случае если можно можно избежать экспорта go кода в Dart (например экспортировать готовую c библиотеку), то лучше воспользоваться такой возможностью и не использовать cgo. Однако, могут возникать случаи, когда перегонка go в dart кода является оптимальным решением (например вы уже знакомы с Go и Dart, и не хотите писать код на C, в таком случае есть смысл задуматься об использованием cgo и Dart FFI).

В данной статье на простом примере будет показано как можно вызвать код Go из языка Dart (например в приложениях на Flutter).

Что должно быть установлено:

  • Go

  • Dart

  • Текстовый редактор/IDE (я буду использовать VSCode, так как это самая популярная среда среди Dart и Go сообщества, так же будут установлены специальные плагины для поддержки языков Go и Flutter)

Шаг 1 - Создаем пустое консольное приложение на Dart

Вызываем Command Palette клавишей F1 и создаем новый проект на Dart, выбираем опцию Console Application (данный формат использован для примера, далее код на cgo можно будет использовать в том числе из Flutter проектов или других форматов приложений на Dart).

Назвать приложение можно в целом как угодно, я выбрал название cgo_dartffi_helloworld, исключительно для тестового примера. (Нам потребуется именно директория с проектом на Dart, так как мы будем добавлять ffi в pubspec.yaml файл).

Прожигаем кнопку создать и переходим в директорию со новоиспеченным проектом.

Шаг 2 - Добавляем ffi в yaml файл

Далее нам необходимо добавить ffi в yaml файл для возможности использования go кода из dart.

name: cgo_dartffi_helloworlddescription: A sample command-line application.version: 1.0.0environment:  sdk: '>=2.12.0 <3.0.0'dependencies:  path: ^1.8.0  ffi: ^0.1.3dev_dependencies:  pedantic: ^1.10.0  test: ^1.16.0

Шаг 3 - Создаем .go файл содержащий экспортируемую функцию

Далее необходимо создать файл на go, (в например в руте директории с проектом, например lib.go) который будет содержать функцию для экспорта в Dart. В данном примере эта функция - HelloFromGo().

// filename: lib.gopackage mainimport "C"//export HelloFromGofunc HelloFromGo() *C.char {message := "Hello to dart lang from go"return C.CString(message)}func main() {}

Стоит быть крайне аккуратными при написании кода cgo так как большая часть инструментов, включая сборщик мусора перестают работать. В cgo комментарии имеют значение (да, это странно), именно с помощью комментариев можно обозначить функцию которую необходимо экспортировать (используя слово export). Более подробно данные нюансы описаны на официальной странице cgo https://golang.org/cmd/cgo/, ну а мы вернемся к практической стороне вопроса.

Шаг 4 - Собираем динамическую библиотеку из go файла

Далее необходимо открыть терминал и запустить там следующую команду:

go build -buildmode=c-shared -o lib.a lib.go

Данная команда создаст файл lib.a (который и представляет из себя динамическую c библиотеку). Даже для такого небольшого файлика время сборки заставляет ужаснуться (аж целых несколько секунд, в отличии от моментальных сборок на go, еще один из плюсов go, который теряется при использовании cgo).

Шаг 5 - Проверяем наличие необходимых файлов

На данном этапе наша директория должна выглядеть примерно следующим образом:

Она должна содержать следующие файлы:

  • Измененный pubspec.yaml файл

  • lib.h, lib.a файлы созданные из файла lib.go

  • директорию bin с дефолтным файлом библиотеки dart (туда мы сейчас и отправимся)

Шаг 6 - Прописываем биндинги на cgo функцию в Dart коде

Далее идёт самая сложная (и потенциально вызывающая ошибки) часть процедуры. Наличие ошибок возможно при несоответствии имён (так как нет возможности их проверить), по этому на данном этапе необходимо быть особенно аккуратными.

  • 6.1 - удаляем всё содержимое файла bin/cgo_dartffi_helloworld.dart и начинаем там писать с чистого листа

  • 6.2 - импортируем необходимые библиотеки (для нас это ffi и utf8 для передачи текста)

import 'dart:ffi' as ffi;import 'package:ffi/src/utf8.dart';
  • 6.3 - открываем динамическую библиотеку

final dylib = ffi.DynamicLibrary.open('lib.a');
  • 6.4 - привязываем нашу функции к функции в dart

typedef HelloFromGo = ffi.Pointer<Utf8> Function();typedef HelloFromGoFunc = ffi.Pointer<Utf8> Function();final HelloFromGo _finalFunction = dylib    .lookup<ffi.NativeFunction<HelloFromGoFunc>>('HelloFromGo')    .asFunction();
  • 6.5 - создаем метод который проверит вызов нашей функции (обратите внимание, метод .toDartString переводит стринг из формата C в формат Dart):

void main() {  print(_finalFunction().toDartString());}

Таким образом мы создали функцию на go, которая передает string в язык Dart.

Далее при написании своих функций следует учитывать, что форматы данных в языках Go, C и Dart могут отличаться (и зачастую так происходит), что приводит к необходимости использовать различные конвертации на стороне go/dart кода, более подробно можно ознакомиться по следующим ссылкам:

Полный код на Dart:

import 'dart:ffi' as ffi;import 'package:ffi/src/utf8.dart';final dylib = ffi.DynamicLibrary.open('lib.a');typedef HelloFromGo = ffi.Pointer<Utf8> Function();typedef HelloFromGoFunc = ffi.Pointer<Utf8> Function();final HelloFromGo _finalFunction = dylib    .lookup<ffi.NativeFunction<HelloFromGoFunc>>('HelloFromGo')    .asFunction();void main() {  print(_finalFunction().toDartString());}

При необходимости передавать параметры в вызываемую функцию можно использовать поинтеры и объявить их в вызываемой функции, например:

typedef GetHash = Pointer<Utf8> Function(Pointer<Utf8> str);typedef GetHashFunc = Pointer<Utf8> Function(Pointer<Utf8> str);final GetHash _getHashGoFunction =    _lib.lookup<NativeFunction<GetHashFunc>>('GetHash').asFunction();

Главное помнить, что необходимо проверять форматы передаваемых данных.

Подробнее..
Категории: C , Go , Dart , Flutter , Golang , Cgo , Ffi

Подсказки по написанию тестов в приложениях на Go

23.04.2021 14:18:12 | Автор: admin

В нашей компании в стеке разработки есть язык Go. И иногда,при написании unit-тестов к приложениям написанным на Go, у нас появляются сложности. В этой статье мы расскажем о некоторых моментах, которые мы учитываем при написании тестов. На примерах разберём как их можно использовать.

Используем интерфейсы при разработке

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

package yourpackage import (    "context"     "github.com/go-redis/redis/v8") func CheckLen(ctx context.Context, client *redis.Client, key string) bool {    val, err := client.Get(ctx, key).Result()    if err != nil {    return false    }    return len(val) < 10  }

Тесты для неё могут выглядеть примерно так:

package yourpackage import (    "context"    "testing"     "github.com/go-redis/redis/v8") func TestCheckLen(t *testing.T) {    ctx := context.Background()    rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})    err := rdb.Set(ctx, "some_key", "value", 0).Err()    if err != nil {    t.Fatalf("redis return error: %s", err)    }     got := CheckLen(ctx, rdb, "some_key")    if !got {    t.Errorf("CheckLen return %v; want true", got)    }}

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

Перепишем наш код с использованием интерфейсов:

package yourpackage import (    "context"     "github.com/go-redis/redis/v8") type Storage interface {    Set(ctx context.Context, key string, v interface{}) error    Get(ctx context.Context, key string) (string, error)} type RedisStorage struct {    Redis *redis.Client} func (rs *RedisStorage) Set(ctx context.Context, key string, v interface{}) error {    return rs.Redis.Set(ctx, key, v, 0).Err()} func (rs *RedisStorage) Get(ctx context.Context, key string) (string, error) {    return rs.Redis.Get(ctx, key).Result()} func CheckLen(ctx context.Context, storage Storage, key string) bool {    val, err := storage.Get(ctx, key)    if err != nil {    return false    }    return len(val) < 10}

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

package yourpackage import (    "context"    "testing") type testRedis struct{} func (t *testRedis) Get(ctx context.Context, key string) (string, error) {    return "value", nil}func (t *testRedis) Set(ctx context.Context, key string, v interface{}) error {    return nil} func TestCheckLen(t *testing.T) {   ctx := context.Background()    storage := &testRedis{}     got := CheckLen(ctx, storage, "some_key")    if !got {    t.Errorf("CheckLen return %v; want true", got)    }}

Используем генераторы моков

Понятное дело, что для каждого случая писать свой мок немного избыточно. Можно попробовать написать универсальный мок. А можно попробовать его сгенерировать на основе интерфейса. Существует множество генераторов моков. Нам нравится https://github.com/vektra/mockery.

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

mockery --recursive=true --inpackage --name=Storage

И дальше используем его в тестах следующим образом:

package yourpackageimport (    "context"    "testing"     mock "github.com/stretchr/testify/mock") func TestCheckLen(t *testing.T) {    ctx := context.Background()     storage := new(MockStorage)    storage.On("Get", mock.Anything, "some_key").Return("value", nil)     got := CheckLen(ctx, storage, "some_key")    if !got {    t.Errorf("CheckLen return %v; want true", got)    }

Перехватываем логирование

Допустим у нас есть код, который логирует свои действия с использованием какой-либо сторонней библиотеки, например, Logrus.

package yourpackage import (    log "github.com/sirupsen/logrus") func Minus(a, b int) int {    log.Infof("Minus(%v, %v)", a, b)    return a - b} func Plus(a, b int) int {    log.Infof("Plus(%v, %v)", a, b)    return a + b} func Mul(a, b int) int {    log.Infof("Mul(%v, %v)", a, b)    return a + b // тут ошибка}

И тесты к этому коду:

package yourpackage import "testing" func TestPlus(t *testing.T) {    a, b, expected := 3, 2, 5    got := Plus(a, b)    if got != expected {    t.Errorf("Plus(%v, %v) return %v; want %v", a, b, got, expected)    }} func TestMinus(t *testing.T) {    a, b, expected := 3, 2, 1    got := Minus(a, b)    if got != expected {    t.Errorf("Minus(%v, %v) return %v; want %v", a, b, got, expected)    }} func TestMul(t *testing.T) {    a, b, expected := 3, 2, 6    got := Mul(a, b)    if got != expected {    t.Errorf("Mul(%v, %v) return %v; want %v", a, b, got, expected)    }}

При запуске тестов мы видим, помимо ошибки, ещё логирование от других тестов:

time="2021-03-22T22:09:54+03:00" level=info msg="Plus(3, 2)"time="2021-03-22T22:09:54+03:00" level=info msg="Minus(3, 2)"time="2021-03-22T22:09:54+03:00" level=info msg="Mul(3, 2)"--- FAIL: TestMul (0.00s)yourpackage_test.go:55: Mul(3, 2) return 5; want 6FAILFAILgotest2/yourpackage 0.002sFAIL

Если кодовая база большая, то упавшие тесты потеряются среди лишнего логирования. Чтобы такого не было, можно сделать перехват логов в тестах. Для приведённого примера это может выглядеть вот так:

package yourpackage import (    "io"    "testing"     "github.com/sirupsen/logrus") type logCapturer struct {    *testing.T    origOut io.Writer} func (tl logCapturer) Write(p []byte) (n int, err error) {    tl.Logf((string)(p))    return len(p), nil} func (tl logCapturer) Release() {    logrus.SetOutput(tl.origOut)} func CaptureLog(t *testing.T) *logCapturer {    lc := logCapturer{T: t, origOut: logrus.StandardLogger().Out}    if !testing.Verbose() {    logrus.SetOutput(lc)    }    return &lc} func TestPlus(t *testing.T) {    defer CaptureLog(t).Release()    a, b, expected := 3, 2, 5    got := Plus(a, b)    if got != expected {    t.Errorf("Plus(%v, %v) return %v; want %v", a, b, got, expected)    }} func TestMinus(t *testing.T) {    defer CaptureLog(t).Release()    a, b, expected := 3, 2, 5    got := Minus(a, b)    if got != expected {    t.Errorf("Minus(%v, %v) return %v; want %v", a, b, got, expected)    }} func TestMul(t *testing.T) {    defer CaptureLog(t).Release()    a, b, expected := 3, 2, 5    got := Mul(a, b)    if got != expected {    t.Errorf("Mul(%v, %v) return %v; want %v", a, b, got, expected)    }}

И тогда вывод тестов будет лаконичнее, и сразу будет понятно, какое логирование и в каком тесте велось:

--- FAIL: TestMul (0.00s)yourpackage_test.go:16: time="2021-03-22T22:10:52+03:00" level=info msg="Mul(3, 2)"yourpackage_test.go:55: Mul(3, 2) return 5; want 6FAILFAILgotest2/yourpackage 0.002sFAIL

Здесь приведён пример для Logrus, но нечто похожее можно сделать с любой библиотекой логирования. Например, для библиотеки Zap есть отдельный модуль, который облегчает тестирование.

Считаем покрытие правильно

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

Допустим, наш проект состоит из трёх пакетов. И мы хотим для них посчитать покрытие. Обращаемся за помощью к утилите cover, которая скажет нам примерно следующее:

$ go tool cover -helpUsage of 'go tool cover':Given a coverage profile produced by 'go test':    go test -coverprofile=c.out...Display coverage percentages to stdout for each function:    go tool cover -func=c.out

Пробуем:

$ go test -coverprofile=c.out ./...ok  gotestcover/minus   0.001s  coverage: 100.0% of statements?   gotestcover/mul [no test files]ok  gotestcover/plus    0.001s  coverage: 100.0% of statements

Уже из этого вывода видно, что у нас два пакета покрыты на 100 % и для одного пакета нет тестовых файлов. Получим отчёт о покрытии:

$ go tool cover -func=c.outgotestcover/minus/minus.go:4:   Minus       100.0%gotestcover/plus/plus.go:4: Plus        100.0%total:                      (statements)100.0%

Но тут что-то не так. В отчёте говорится о полном покрытии тестами. Хотя мы знаем, что это не так. Это всё потому, что при подсчёте покрытия не учитывается пакет, в котором нет тестов. Его не будет и в HTML-отчёте. И кажется, что это не всегда может быть корректным, потому что зачастую мы хотим знать покрытие всего кода, а не только того, для которого мы написали тесты. Это можно исправить, указав специальный параметр при запуске тестов:

go test -coverpkg=./... -coverprofile=c.out ./

И теперь отчёт выдаёт ожидаемый процент покрытия тестами:

$ go tool cover -func=c.outgotestcover/minus/minus.go:4:   Minus       100.0%gotestcover/mul/mul.go:4:   Mul         0.0%gotestcover/plus/plus.go:4: Plus        100.0%total:                      (statements)66.7%

Считаем покрытие при тестировании приложения как черного ящика

Писать тесты на Go довольно-таки сложно. И если вы разрабатываете какой-нибудь веб-сервис, то иногда бывает проще написать тесты на другом языке, например, на Python, и тестировать приложение как чёрный ящик.

Но возникает вопрос, а можно ли посчитать покрытие для такого способа тестирования? Да, посчитать можно. В целом, идея довольно проста. Пишем подобный тест:

func TestRunMain(t *testing.T) {    main()}

Запускаем его, потом интеграционные тесты, и завершаем наш тест. Звучит просто, но есть несколько нюансов. Зачастую надо сделать так, чтобы этот тест не запускался со всеми остальными тестами. Он особый, и для него должна быть отдельная логика запуска. Ещё функция main не должна приводить к выходу с ненулевым кодом возврата. И надо реализовать способ выхода из main по сигналу, не завершая при этом сам тест. То есть в целом надо реализовать для нашего web-сервиса graceful shutdown, что несложно сделать, и это в целом полезно. Давайте на примере реализуем небольшой web-сервис, протестируем его с помощью curl, и посчитаем покрытие тестами.

Сервис наш будет выглядеть следующим образом (взято с https://gobyexample.com/http-servers):

package main import (    "context"    "fmt"    "net/http"    "os"    "os/signal"    "time") func hello(w http.ResponseWriter, req *http.Request) {    fmt.Fprintf(w, "hello\n")} func headers(w http.ResponseWriter, req *http.Request) {    for name, headers := range req.Header {    for _, h := range headers {    fmt.Fprintf(w, "%v: %v\n", name, h)    }    }} func main() {    http.HandleFunc("/hello", hello)    http.HandleFunc("/headers", headers)     // Приложим некоторые усилия, чтобы приложение завершилось с нулевым кодом выхода    // Это важно для тестов, и в целом приятно    server := &http.Server{Addr: ":8090", Handler: nil}    // Запускаем приложение в отдельной горутине    go func() {    server.ListenAndServe()    }()     // А в текущей ждём сигнала об остановке приложения    quit := make(chan os.Signal, 1)    signal.Notify(quit, os.Interrupt)    <-quit    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)    defer cancel()    server.Shutdown(ctx)}

И тест к нему:

// +build testrunmain package main import "testing" func TestRunMain(t *testing.T) {    main()}

Комментарий +build testrunmain говорит о том, что тест будет запускаться только в случае, если передан соответствующий tag. Запускаем наш тест:

$ go test -v -tags testrunmain -coverpkg=./... -coverprofile=c.out  ./...=== RUN   TestRunMain

Тестируем с помощью curl:

$ curl 127.0.0.1:8090/hellohello

И завершаем наше тестирование, нажав Ctrl+C:

$ go test -v -tags testrunmain -coverpkg=./... -coverprofile=c.out  ./...=== RUN   TestRunMain^C--- PASS: TestRunMain (100.92s)PASScoverage: 80.0% of statements in ./...ok  gobintest   100.926s    coverage: 80.0% of statements in ./

Можем посмотреть покрытие тестами и увидеть, что обработчик headers остался не протестированным:

$ go tool cover -func=c.outgobintest/main.go:12:   hello       100.0%gobintest/main.go:16:   headers     0.0%gobintest/main.go:24:   main        100.0%total:              (statements)80.0%

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

Хотите узнать больше о тестировании в Go? Вот ещё несколько интересных статей на хабре: один, два, три.

Подробнее..

Архитектура кода программного обеспечения декорируем стратегией. Рассказ в 10 эпизодах, основанный на реальных событиях

21.05.2021 08:10:50 | Автор: admin

Встречаются два эксперта-консультанта по конструированию программного обеспечения:
- Как написать сложное корпоративное приложение, поддерживать которое будет всегда легко и дешево.
- Могу рассказать...
- Рассказать и я могу! Написать-то как?..

Время чтения: 25 мин.

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

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

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

Введение в предметную область

"Красота" поддержки программного обеспечения во многом зависит от того, насколько много времени и сил было уделено самым первым этапам разработки (определение цели, выработка требований, разработка архитектуры и т.д.). Неверно сформулированные требования это тоже ошибка, такая же, как упустить переполнение переменных целочисленных типов данных в коде. Но цена ошибок первых этапов, выявленных на стадии поддержки приложения, непозволительна велика по сравнению с "багами", допущенными в коде при конструировании. Подробнее об этой математике цен ошибок на различных стадиях разработки можно почитать в "Совершенном коде" Стива Макконнелла.

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

В этой статье я хочу предложить технику написания программ, в основе которой лежит два паттерна проектирования ООП: декоратор и стратегия. Я уверен, что основная часть читающих статью наверняка не раз сталкивалась с этими паттернами (возможно, даже на практике). Но чтобы все чувствовали себя "в своей тарелке", обращусь к определениям из "Паттернов проектирования" Эриха Гаммы, Ричарда Хелма, Ральфа Джонсона и Джона Влиссидеса (Банда четырех, Gang of Four, GoF):

  • Декоратор (Decorator, Wrapper) паттерн проектирования, позволяющий динамически добавлять объекту новые обязанности. Является гибкой альтернативой порождению подклассов с целью расширения функциональности.

  • Стратегия (Strategy, Policy) паттерн проектирования, который определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Стратегия позволяет изменять алгоритмы независимо от клиентов, которые ими пользуются.

Подход, который я называю "Декорирование стратегией" и который мы с вами будем рассматривать дальше, предполагает использование этих паттернов совместно друг с другом. Соответственно он не имеет смысла при их использовании порознь.

Декорирование стратегией, на мой взгляд, даёт великую пользу при поддержке приложений на очень большом жизненном цикле программного продукта. Компоненты в коде, написанные с применением данного подхода, соответствуют всем принципам дизайна SOLID из "Чистой архитектуры" Роберта Мартина. Каждый компонент, который мы напишем далее, будет отвечать только за одно действие; после написания нового компонента мы ни разу не модифицируем логику его методов, а лишь будем расширять ее в декорирующих компонентах; в силу паттерна "Декоратор" все расширяемые и расширяющие компоненты соответствуют одному контракту, следовательно их можно заменять друг другом; интерфейсы компонентов не содержат зависимостей, которые не используются; компоненты бизнес-логики ни в коей мере не зависят от деталей.

Я не раз сталкивался в обсуждениях с опытными разработчиками, которые говорят: "А вот всё, что связано с применением принципов SOLID, паттернов ООП на практике это миф!". Любезно обращаясь к скептически настроенным к применению теории разработки в реальных больших корпоративных проектах, хочу сказать: "А вот посмотрим!"

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

Также я хочу выделить очень важные моменты, которые в реальном коде обязательно бы имели место, но так как код в статье имеет демонстрационное назначение, здесь эти моменты будут опускаться, дабы не "перетягивать на себя" ценное внимание читателя:

  • Должная обработка ошибок. В коде мы ограничимся оборачиванием ошибок дополнительным сообщением с помощью пакета "github.com/pkg/errors".

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

  • Комментарии и документирование кода.

  • Всё, что связано, с конкурентным выполнением задач и синхронизацией.

  • Структура файлов и директорий проекта.

  • Стили, линтеры и статический анализ.

  • Покрытие кода тестами.

  • Сквозь методы компонентов рекомендуется с первых этапов разработки "тянуть" context.Context, даже если он в тот момент не будет использоваться. Для упрощения повествования в примерах далее контекст также использоваться не будет.

Перейдём же наконец от скучной теории к занимательной практике!

Пролог. Закладываем фундамент

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

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

Первое, что нужно сделать определить интерфейс нашего первого компонента службы, которая будет представлять желаемый use-case SavePersonService. Но для этого нам нужно определить объекты нашей предметной области, а именно структуру данных, содержащую информацию о человеке PersonDetails. Создадим в корне проекта пакет app, далее создадим файл app/person.go, и оставим в нём нашу структуру:

// app/person.gotype PersonDetails struct {    Name string    Age  int}

Данный файл завершён, больше мы к нему в этой статье возвращаться не будем. Далее создаем файл app/save-person.go, и определяем в нём интерфейс нашего use-case:

// app/save-person.gotype SavePersonService interface {    SavePerson(id int, details PersonDetails) error}

Оставим сразу рядом с определением интерфейса его первую реализацию компонент noSavePersonService, который ничего не делает в теле интерфейсного метода:

// app/save-person.go// ... предыдущий код ...type noSavePersonService struct{}func (noSavePersonService) SavePerson(_ int, _ PersonDetails) error { return nil }

Поскольку объекты noSavePersonService не содержат состояния, можно гарантировать, что данный "класс" может иметь только один экземпляр. Напоминает паттерн проектирования Синглтон (Singleton ещё его называют Одиночка, но мне это название по ряду причин не нравится). Предоставим глобальную точку доступа к нему. В Golang легче всего это сделать, определив глобальную переменную:

/ app/save-person.go// ... предыдущий код ...var NoSavePersonService = noSavePersonService{}

Зачем мы написали ничего не делающий компонент? С первого взгляда он очень походит на заглушку. Это не совсем так. Далее поймём.

Эпизод 1. Будем знакомы, Декоратор Стратегией

Перейдём непосредственно к реализации бизнес-логики нашей задачи. Нам нужно в конечном счёте иметь хранилище, в котором содержатся данные о пользователях. С точки зрения выбора технологии мы сразу себе представляем, что будем использовать PostgreSQL, но правильно ли завязываться в коде нашей бизнес-логики на конкретную технологию. Вы правы конечно нет. Определить компонент нашего хранилища нам позволит паттерн Репозиторий (Repository). Создадим пакет с реализациями интерфейса нашего use-case save-person внутри app, и в нём создадим файл app/save-person/saving_into_repository.go реализации нашего use-case, которая обновляет данные в репозитории:

// app/save-person/saving_into_repository.gotype PersonRepository interface {    UpdatePerson(id int, details app.PersonDetails) error}type SavePersonIntoRepositoryService struct {    base app.SavePersonService    repo PersonRepository}func WithSavingPersonIntoRepository(base app.SavePersonService, repo PersonRepository) SavePersonIntoRepositoryService {    return SavePersonIntoRepositoryService{base: base, repo: repo}}func (s SavePersonIntoRepositoryService) SavePerson(id int, details app.PersonDetails) error {    err := s.base.SavePerson(id, details)    if err != nil {        return errors.Wrap(err, "save person in base in save person into repository service")    }    err = s.repo.UpdatePerson(id, details)    if err != nil {        return errors.Wrap(err, "update person in repo")    }    return nil}

В коде выше впервые появляется компонент, который выражает наш подход "Декорирование стратегией". Сам компонент представляет собой декоратор, реализующий интерфейс нашего use-case, который оборачивает любой компонент с таким же интерфейсом. В реализации метода изначально вызывается метод декорируемого объекта s.base; после этого происходит вызов стратегии обновления данных о человеке в хранилище s.repo. По сути, весь подход это конструирование компонентов-декораторов, которые содержат два объекта:

  1. Непосредственно декорируемый объект с таким же интерфейсом.

  2. Стратегия, логику которой мы добавляем в довесок к логике декорируемого объекта.

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

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

Напомню, что бизнес-логика не должна содержать ненужные зависимости, зависимости от деталей и т.п. Другими словами, бизнес-логика должна быть "чистая, как слеза". Где тогда должны находиться зависимости от конкретных реализаций, зависимости от используемых технологий? Ответ в файле main.go. Следуя замечаниям Роберта Мартина, можно сделать умозаключение, что код компонентов файла, содержащего точку входа в программу, является самым "грязным" с точки зрения зависимостей от всего. Обозначим в main.go метод, который нам возвращает клиент к базе данных PostgreSQL. И собственно сборку объекта службы нашего use-case и вызов его метода на условных входных данных:

// main.gofunc NewPostgreSQLDatabaseClient(dsn string) savePerson.PersonRepository {    _ = dsn // TODO implement    panic("not implemented")}func run() error {    userService := savePerson.WithSavingPersonIntoRepository(        app.NoSavePersonService,        NewPostgreSQLDatabaseClient("postgres://user:pass@127.0.0.1:5432/users?sslmode=disable"))    err := userService.SavePerson(5, app.PersonDetails{        Name: "Mary",        Age:  17,    })    if err != nil {        return errors.Wrap(err, "save user Mary")    }    return nil}

В коде выше мы можем заметить, что в качестве стратегии репозитория выступает обозначенный конкретный компонент клиента к PostgreSQL. В качестве же декорируемого объекта выступает наша "фиктивная" реализация use-case app.NoSavePersonService, которая по сути ничего не делает. Зачем она нужна? Она ничего полезного ведь не делает? Не легче ли просто вызвать метод клиента к базе данных? Спокойно, звёздный час этой реализации сейчас настанет.

Ссылка на полный код эпизода

Эпизод 2. Магия начинается!

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

// main.go// ... предыдущий код ...func NewMemoryCache() savePerson.PersonRepository {    // TODO implement    panic("not implemented")}// ... последующий код ...

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

// main.go// внутри run()userService := savePerson.WithSavingPersonIntoRepository(    savePerson.WithSavingPersonIntoRepository(        app.NoSavePersonService,        NewPostgreSQLDatabaseClient("postgres://user:pass@127.0.0.1:5432/users?sslmode=disable")),    NewMemoryCache(),)err := userService.SavePerson(5, app.PersonDetails{    Name: "Mary",    Age:  17,})if err != nil {    return errors.Wrap(err, "save user Mary")}

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

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 3. Рефакторинг для здоровья

В предыдущем листинге кода создание сервиса выглядит достаточно громоздко. Нетрудно догадаться, применяя наш подход, мы продолжим и далее всё больше и больше оборачивать компонент, добавляя к логике новые стратегии. Поэтому мы, как опытные разработчики, замечаем эту потенциальную трудность и производим небольшой рефакторинг когда. Нам поможет паттерн Билдер (Builder опять же мне не очень нравится ещё одно его название Строитель). Это будет отдельный компонент, зона ответственности которого предоставить возможность сборки объекта службы нашего use-case. Файл app/save-person/builder.go:

// app/save-person/builder.gotype Builder struct {    service app.SavePersonService}func BuildIdleService() *Builder {    return &Builder{        service: app.NoSavePersonService,    }}func (b Builder) SavePerson(id int, details app.PersonDetails) error {    return b.service.SavePerson(id, details)}

Компонент Builder должен обязательно реализовывать интерфейс службы нашего use-case, так как именно он будет использоваться в конечном счёте. Поэтому мы добавляем метод SavePerson, который вызывает одноименный метод объекта в приватном поле service. Конструктор данного компонента называется BuildIdleService, потому что создаёт объект, который ничего не будет делать при вызове SavePerson (нетрудно заметить инициализацию поля service объектом app.NoSavePersonService). Зачем нам нужен этот бесполезный компонент? Чтобы получить всю истинную пользу, необходимо обогатить его другими методами. Эти методы будут принимать в параметрах стратегию и декорировать ею объект службы в поле service. Но вначале сделаем конструктор WithSavingPersonIntoRepository в app/save-person/saving_into_repository.go приватным, так как для создания службы мы теперь будем использовать только Builder:

// app/save-person/saving_into_repository.go// ... предыдущий код ...func withSavingPersonIntoRepository(base app.SavePersonService, repo PersonRepository) SavePersonIntoRepositoryService {    return SavePersonIntoRepositoryService{base: base, repo: repo}}// ... последующий код ...

Добавляем соответствующий метод для Builder:

// app/save-person/builder.go// ... предыдущий код ...func (b *Builder) WithSavingPersonIntoRepository(repo PersonRepository) *Builder {    b.service = withSavingPersonIntoRepository(b.service, repo)    return b}

И наконец производим рефакторинг в main.go:

// main.go// ... предыдущий код ...userService := savePerson.BuildIdleService().        WithSavingPersonIntoRepository(NewPostgreSQLDatabaseClient("postgres://user:pass@127.0.0.1:5432/platform?sslmode=disable")).        WithSavingPersonIntoRepository(NewMemoryCache())// ... последующий код ...

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 4. Больше заказчиков!

Через несколько дней успешной работы нашего кода в продакшне, к нам приходит другой заказчик от бизнеса и просит реализовать функциональность обновления информации о налогоплательщиках в отдельном хранилище. По неким причинам, обсуждение которых находится за пределами данной статьи, мы понимаем, что эту информацию лучше хранить в MongoDB. Клиент к базе добавляется в main.go:

// main.go// ... предыдущий код ...func NewMongoDBClient(dsn string) savePerson.PersonRepository {    _ = dsn // TODO implement    panic("not implemented")}// ... последующий код ...

Воспользуемся нашим билдером и просто добавим новый код в main.go под имеющийся фрагмент с userService:

// main.go// ... предыдущий код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithSavingPersonIntoRepository(NewMemoryCache())err = taxpayerService.SavePerson(1326423, app.PersonDetails{    Name: "Jack",    Age:  37,})if err != nil {    return errors.Wrap(err, "save taxpayer Jack")}

Мы выполнили уже столько поставленных задач, имея небольшой фрагмент кода бизнес-логики. Заметьте, изменения преимущественно вносятся в файл main.go

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 5. Путь в никуда

Проходит ещё время. Заказчик 2 ставит нам такую задачу. Так как все налогоплательщики должны быть совершеннолетними, необходимо в бизнес-логику добавить функциональность проверки возраста человека перед сохранением в хранилище. С этого момента начинаются интересные вещи. Мы можем добавить эту валидацию в метод SavePersonIntoRepositoryService.SavePerson в файле app/save-person/saving_into_repository.go. Но тогда при нескольких декорированиях стратегией сохранения информации в репозиторий эта валидация будет вызываться столько раз, сколько производилось таких декораций. Хотя и все проверки помимо первой никак не влияют на результат напрямую, всё-таки не хочется лишний раз вызывать один и тот же метод.

Мы можем добавить валидацию в Builder.SavePerson. Но есть проблема: заказчику 1 не нужна проверка возраста при сохранении. Придётся добавить if и дополнительный флаг в параметры конструктора, который будет определять необходимость валидации:

// app/save-person/builder.gotype Builder struct {    service           app.SavePersonService    withAgeValidation bool}func BuildIdleService(withAgeValidation bool) *Builder {    return &Builder{        service:           app.NoSavePersonService,        withAgeValidation: withAgeValidation,    }}func (b Builder) SavePerson(id int, details app.PersonDetails) error {    if b.withAgeValidation && details.Age < 18 {        return errors.New("invalid age")    }    return b.service.SavePerson(id, details)}// ... последующий код ...

И тогда в main.go нужно вызывать конструкторы билдера с разными значениями флага withAgeValidation:

// main.go// ... предыдущий код ... userService := savePerson.BuildIdleService(false).// ... код ...taxpayerService := savePerson.BuildIdleService(true).// ... последующий код ...

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

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 6. Путь истины

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

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

// app/save-person/validating.gotype PersonValidator interface {    ValidatePerson(details app.PersonDetails) error}type PreValidatePersonService struct {    base      app.SavePersonService    validator PersonValidator}func withPreValidatingPerson(base app.SavePersonService, validator PersonValidator) PreValidatePersonService {    return PreValidatePersonService{base: base, validator: validator}}func (s PreValidatePersonService) SavePerson(id int, details app.PersonDetails) error {    err := s.validator.ValidatePerson(details)    if err != nil {        return errors.Wrap(err, "validate person")    }    err = s.base.SavePerson(id, details)    if err != nil {        return errors.Wrap(err, "save person in base in pre validate person service")    }    return nil}

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

Добавим соответствующий метод в Builder:

// app/save-person/builder.go// ... предыдущий код ...func (b *Builder) WithPreValidatingPerson(validator PersonValidator) *Builder {    b.service = withPreValidatingPerson(b.service, validator)    return b}

Добавление каждого нового декоратора стратегией требует добавление нового метода в наш билдер.

Добавим реализацию валидатора, проверяющую возраст человека:

// main.go// ... предыдущий код ...type personAgeValidator struct{}func (personAgeValidator) ValidatePerson(details app.PersonDetails) error {    if details.Age < 18 {        return errors.New("invalid age")    }    return nil}var PersonAgeValidator = personAgeValidator{}// ... последующий код ...

Так как personAgeValidator не имеет состояния, можем сделать для компонента единую точку доступа PersonAgeValidator. Далее просто вызываем новый метод в main.go только для taxpayerService:

// main.go// ... предыдущий код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithSavingPersonIntoRepository(NewMemoryCache()).    WithPreValidatingPerson(PersonAgeValidator)// ... последующий код ...

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 7. А ну-ка закрепим

Уверен, к данному эпизоду вы поняли смысл подхода "Декорирование стратегией". Чтобы закрепить, давайте добавим ещё один такой компонент. Представим, технический руководитель требует от нас покрыть метриками время выполнения сохранения данных в хранилище. Мы могли бы замерить это время, просто добавив пару строчек кода в SavePersonIntoRepositoryService. Но как бы не так! Мы же не изменяем уже работающий в продакшне код, а можем его только расширить. Давайте же так и сделаем. Добавим новый декоратор стратегией отправки метрики времени:

// app/save-person/sending_metric.gotype MetricSender interface {    SendDurationMetric(metricName string, d time.Duration)}type SendMetricService struct {    base         app.SavePersonService    metricSender MetricSender    metricName   string}func withMetricSending(base app.SavePersonService, metricSender MetricSender, metricName string) SendMetricService {    return SendMetricService{base: base, metricSender: metricSender, metricName: metricName}}func (s SendMetricService) SavePerson(id int, details app.PersonDetails) error {    startTime := time.Now()    err := s.base.SavePerson(id, details)    s.metricSender.SendDurationMetric(s.metricName, time.Since(startTime))    if err != nil {        return errors.Wrap(err, "save person in base in sending metric service")    }    return nil}

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

// app/save-person/builder.go// ... предыдущий код ...func (b *Builder) WithMetricSending(metricSender MetricSender, metricName string) *Builder {    b.service = withMetricSending(b.service, metricSender, metricName)    return b}

И наконец обозначаем в main.go функцию, возвращающую savePerson.MetricSender и добавляем вызов нового метода Builder в сборку наших сервисов:

// main.go// ... предыдущий код ...func MetricSender() savePerson.MetricSender {    // TODO implement    panic("not implemented")}// ... код ...userService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewPostgreSQLDatabaseClient("postgres://user:pass@127.0.0.1:5432/platform?sslmode=disable")).    WithMetricSending(MetricSender(), "save-into-postgresql-duration").    WithSavingPersonIntoRepository(NewMemoryCache())// ... код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithMetricSending(MetricSender(), "save-into-mongodb-duration").    WithSavingPersonIntoRepository(NewMemoryCache()).    WithPreValidatingPerson(PersonAgeValidator)// ... последующий код ...

Обратите внимание, что новые методы мы ставим в цепочку вызовов там, где мы хотим производить замер.

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 8. Результаты ясновидения

Проходит время. Заказчик 2 ставит новую задачу. Он желает знать, как долго выполняется сохранение данных о налогоплательщике, но с небольшой оговоркой: учитывать нужно всё, кроме валидации. Похоже на замер времени, который мы недавно реализовали для своих целей, не правда ли? Чтобы решить задачу, всё что нам требуется это добавить вызов метода для новой метрики в main.go:

// main.go// ... предыдущий код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithMetricSending(MetricSender(), "save-into-mongodb-duration").    WithSavingPersonIntoRepository(NewMemoryCache()).    WithMetricSending(MetricSender(), "save-taxpayer-duration").    WithPreValidatingPerson(PersonAgeValidator)

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 9. Укрощение капризов

Мы вот только недавно произвели релиз последней задачи от заказчика 2, но он захотел изменить начальные требования. Такие изменения часто возникают на стороне заказчика, которые заставляют нас "перелопатить" весь код. Знакомо? На этот раз заказчик желает отказаться от оговорки из предыдущего эпизода и производить замер полного цикла сохранения данных о налогоплательщике вместе с валидацией. Если бы мы конструировали нашу бизнес-логику в виде сценария транзакции (transaction script), то это повлекло бы за собой непосредственное вмешательство в тело метода, copy-paste кода, что требует приложить силы, в том числе в процессе ревью, тестирования и т.п. В нашем же случае нам достаточно просто подвинуть вызов метода WithMetricSending в цепочке методов создания объекта службы в main.go:

// main.go// ... предыдущий код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithMetricSending(MetricSender(), "save-into-mongodb-duration").    WithSavingPersonIntoRepository(NewMemoryCache()).    WithPreValidatingPerson(PersonAgeValidator).    WithMetricSending(MetricSender(), "save-taxpayer-duration")

В коде выше мы поменяли местами второй WithMetricSending и WithPreValidatingPerson.

Задача от заказчика выглядит надуманной. Но напомню, что цель статьи не придумать качественные задачи заказчиков, а продемонстрировать пользу архитектуры кода при использовании подхода "Декорирование стратегией".

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 10. Взгляд в будущее

Этот заключительный эпизод всего лишь подчеркивает потенциал дальнейших доработок логики данного кода. Что ещё может пожелать заказчик от бизнеса или с технической стороны? Вариантов более чем достаточно. Может потребоваться функциональность отправки асинхронных событий об изменении информации о человеке (полезно при ведении журнала аудита, коммуникации с другими сервисами и т.д.). Может понадобиться введение механизма гомогенных и даже гетерогенных транзакций. Возможно, потребуется добавить запрос данных к соседнему микросервису. По техническим соображениям возможно будет нужен предохранитель (circuit-breaker) для таких запросов к другим сервисам. Наверняка нужно будет добавлять механизм трассировки (tracing). И многое-многое другое.

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

Эпилог. Подводим итоги

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

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

Но для больших корпоративных приложений наличие подобного подхода просто желательно-обязательно. Если продукт подразумевает длительную поддержку (обычно это условие присутствует всегда), то объектная модель приложения будет иметь значительное преимущество над незамысловатым "полотном" кода сценария транзакции. Я приведу далее график, в основе которого лежит график из "Шаблонов корпоративных приложений" Мартина Фаулера.

Что есть что на этом графике? Почему на осях нет чисел? Всё потому что график абстрактный. Он отражает качественный смысл содержимого, не количественный. По горизонтальной оси у нас время, прошедшее с момента начала разработки продукта. Или если желаете, количество добавлений новой функциональности в изначально разработанный продукт. Меру по вертикальной оси тоже можно выразить различными способами. Это может быть цена добавления новой строчки кода функционала в денежном эквиваленте; может быть время добавления новой функциональности; может быть количество потраченных нервных клеток разработчиком, ревьювером или тестировщиком. Красный график демонстрирует зависимость этих величин для подхода разработки, который называется сценарием транзакции (Transaction Script) последовательно следующие друг за другом инструкции. Синий график показывает эту зависимость для подхода модели предметной области (Domain Model).

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

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

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

Литература

  1. Макконнелл С. Совершенный код. Мастер-класс., 2020.

  2. Гамма Э., Хелм Р., Джонсон Р., Влиссидес Дж. Приемы объектно-ориентированного проектирования. Паттерны проектирования., 2020.

  3. Мартин Р. Чистая архитектура. Искусство разработки программного обеспечения., 2020

  4. Фаулер, Мартин. Шаблоны корпоративных приложений., 2020.

Подробнее..

Golang-дайджест 1 (14 31 января 2021)

11.02.2021 10:09:47 | Автор: admin

Свежая подборка новостей и материалов со ссылками

Интересное в этом выпуске

  • Поддержка ARM

  • Движок Diablo 2

  • Расшифровка паролей из браузеров

  • Сборщик js аналог webpack

Приятного чтения!

Новости события

  • Исправлена проблема, связанная с поиском PATH в ненадежных каталогах https://blog.golang.org/path-security

  • Выпущен релиз-кандидат 1 Go 1.16!

    • ARM в Go 1.16 добавлена поддержка 64-битной ARM-архитектуры на MacOS М1

    • go get-insecure флаг является устаревшим и будет удален в версии будущего

    • go get example.com/mod@patch теперь хочет, чтобы какая-то версия example.com/mod уже требовалась для основного модуля (тем не менее go get -u=patch продолжает исправлять даже недавно добавленные зависимости)

    • GOVCS новая переменная среды, ограничивающая инструменты управления версиями, которые go-команда может использовать для загрузки исходного кода

    • Добавлен пакет FS определяет основные интерфейсы файловой системы. Файловая система может быть предоставлена операционной системой хоста и другими пакетами

Предложения по улучшению языка

https://github.com/golang/go/issues/44022 Добавить оператор соответствия

func preprocess(example Example) *string    return match (example.Value, example.Name) {        0, "C" => {             return "Zero"        }        -100 .. 0, "C" => {           return "Ice"       }        0 .. 100, "C" =>           return "Hell";       }       _, _ => {            return "Unknown state";       }    }

https://github.com/golang/go/issues/44006 - syscall/js: Удалить тип Wrapper, чтобы избежать чрезмерного выделенияпамятии улучшить производительность

type BadWrapper struct {    Value js.Value}var escapeRoute *BadWrapper// Implements js.Wrapperfunc (this *BadWrapper) JSValue() js.Value {    escapeRoute = this // escape to heap    return this.Value}

https://github.com/golang/go/issues/43823 Поддержка времени с десятичной запятой для дробных секунд, пример: 02/12/2019 15:45:48,746

https://github.com/golang/go/issues/43774 Потоковый интерфейс AEAD

https://github.com/golang/go/issues/43659 Объявление параметров типа иразделениеэкземпляров

https://github.com/golang/go/issues/43557 Значения функций как итераторы

Материалы для обучения

Уроки для изучения GolangВведение в программирование на Go

Go в примерах

Маленькая книга о Go

50 оттенков Go: ловушки, подводные камни и распространенные ошибки новичков

Алан А.А. Донован, Брайан У. Керниган Язык программирования Go

Руководство для начинающих по разумным абстракциям с использованием Golang

Статьи

Инструменты

  • Приложение для просмотра, организации и обмена вашей коллекцией фотографий https://github.com/photoprism/photoprism

  • Игровой движок ARPG в том же духе, что и игры 2000-х годов и поддерживает игру в Diablo 2 https://github.com/OpenDiablo2/OpenDiablo2

  • Сервер Matrix второго поколения, написанный на Go.Призван предоставитьэффективную,надежнуюимасштабируемуюальтернативу Synapse https://github.com/matrix-org/dendrite

  • Сборщик JS в 100 раз быстрее webpackhttps://github.com/evanw/esbuild

  • Модульная, мощная, высокопроизводительная среда разработки приложений корпоративного класса от Golanghttps://github.com/gogf/gf

  • Официальная реализация протокола Ethereum на Golanghttps://github.com/ethereum/go-ethereum

  • Инструмент с открытым исходным кодом, который может помочь вам расшифровать данные из браузера: пароли, закладки, файлы cookie, историю https://github.com/moonD4rk/HackBrowserData

  • Slack API библиотека rest, websocket https://github.com/slack-go/slack

  • Веб-фаззинг, предназначенный для обнаружения элементов и контента в веб-приложениях или веб-серверах https://github.com/ffuf/ffuf

  • Инструменты для сканирования международных телефонных номеров с использованием только бесплатных ресурсов.Это позволяет сначала собрать стандартную информацию, такую как страна, область, оператор и тип линии, на любом международном телефонном номере.Затем поискать следы в поисковых системах, чтобы попытаться найти провайдера VoIP или определить владельца https://github.com/sundowndev/PhoneInfoga

Видео

Небольшая серия Пишем веб-приложение на Go,автор Сергей Гаврук

Серия из 26 видео на тему Погружение в Google Go,автор Роман Левищенко

Серия из 17 уроков на тему Уроки для начинающих, автор Лёша Маршал

Подкасты

Go Time: Англоязычные подкасты о GO

Live Shows: Предложения Go Language, о которых вы никогда не слышали (часть вторая)

GolangShow: Русскоязычный подкаст о Go

Сообщества

Форум в группах Google

Группа Golang RU в Telegram

Вопросы по языку на русскоязычном StackOverflow

Информация о митапах

Подробнее..

Golang-дайджест 2(1 28 февраля 2021)

01.03.2021 08:19:52 | Автор: admin

Свежая подборка новостей и материалов

Интересное в этом выпуске

  • Веб-браузер

  • Мониторинг почтовых служб

  • Сканер уязвимостей

  • Зашифрованная файловая система

Приятного чтения!

Материалы для обучения

Новости, события

  • Модули включены по умолчанию в Go 1.16 теперь go-команда по умолчанию создает пакеты в режиме с поддержкой модулей

  • Профилирование блоков в Go контролирует долю событий блокировки горутин

  • Generic предложение добавить дженерики принято

  • Embed новый пакет embed обеспечивает доступ к файлам, встроенным в программу во время компиляции, с помощью новой директивы //go:embed

  • Unicode пакет unicode и связанная с ним поддержка во всей системе были обновлены с Unicode 12.0.0 до Unicode 13.0.0, что добавляет 5930 новых символов, включая 4 новых скрипта и 55 новых эмодзи

Предложения по улучшению языка

  • https://github.com/golang/go/issues/44221 - encoding/csv: Добавить возможность получения номера строки записи

    Предложение предлагает новый метод:

    func (r *Reader) Line() int
    
  • https://github.com/golang/go/issues/44253 Предложение добавить в дженерики тип и размер массива

    type Array8[T any] interface {type [8]T}type ArraysOfSomeSizes[T any] interface {type [2]T, [4]T, [8]T, [16]T}
    

    предложение предлагает следующий синтаксис для выражения этой идеи:

    type Array[T any] interface {type []T}
    
  • https://github.com/golang/go/issues/36460 - cmd/go: Отложенная загрузка модуля

  • https://github.com/golang/go/issues/44551 Предложение добавить поддержку тестирования фаззинга

    func FuzzMarshalFoo(f *testing.F) {    // Seed the initial corpus f.Add("cat", big.NewInt(1341)) f.Add("!mouse", big.NewInt(0)) // Run the fuzz test   f.Fuzz(func(t *testing.T, a string, num *big.Int) {     t.Parallel() // seed corpus tests can run in parallel   if num.Sign() <= 0 {     t.Skip() // only test positive numbers  }  val, err := MarshalFoo(a, num)  if err != nil {      t.Skip()    }  if val == nil {      t.Fatal("MarshalFoo: val == nil, err == nil")   }  a2, num2, err := UnmarshalFoo(val)  if err != nil {      t.Fatalf("failed to unmarshal valid Foo: %v", err)  }  if a2 == nil || num2 == nil {    t.Error("UnmarshalFoo: a==nil, num==nil, err==nil")     }  if a2 != a || !num2.Equal(num) {     t.Error("UnmarshalFoo does not match the provided input")   }  })}
    
  • https://github.com/golang/go/issues/44412 Предложение добавить Time.UnixMilli и Time.UnixMicro

    // UnixMilli returns the local Time corresponding to the given Unix time,// msec milliseconds since January 1, 1970 UTC.func UnixMilli(msec int64) Time {if msec%1e3 < 0 {return unixTime(msec/1e3-1, int32((msec%1e3)1e6)+1e9)}return unixTime(msec/1e3, int32((msec%1e3)1e6))}// UnixMicro returns the local Time corresponding to the given Unix time,// usec milliseconds since January 1, 1970 UTC.func UnixMicro(usec int64) Time {if usec%1e6 < 0 {return unixTime(usec/1e6-1, int32((usec%1e6)1e3)+1e9)}return unixTime(usec/1e6, int32((usec%1e6)1e3))}
    

Статьи

Инструменты

  • Пример реализации чистой архитектуры в проектах Go (Golang)

  • Инструмент непрерывной доставки GitOps для Kubernetes Argo CD

  • Сканирование для различных протоколов TCP, DNS, HTTP, File на основе шаблонов Nuclei сканер уязвимостей

  • Плагин для Terraform, который позволяет управлять полным жизненным циклом ресурсов AWS. Этот провайдер поддерживается внутри группы HashiCorp AWS Provider Terraform

  • Высокопроизводительная библиотека по работе с json Замена "encoding / json"

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

  • Кросс-платформенное прокси сервер/клиент с шифрованием Brook

  • Настраиваемый механизм подсказок для любой оболочки, который может изменять строку подсказки с помощью функции или переменной Oh-my-posh

  • Горизонтально масштабируемая и распределенная база данных GraphQL с бэкендом графа Dgraph

  • Инструмент для изучения шахматных дебютов Chess-explorer-go

  • Небольшой и простой компилятор Go Babygo

  • Cli инструмент для выполнения sql запросов: поддержка sql, csv, ltsv, json, tbln Trdsql

  • Инструмент для работы с типом файлов MP4 Go-mp4

  • Платформа для создания приложений блокчейна на Golang Cosmos-SDK

  • Мониторинг почтовых служб, получение писем, проверка аккаунтов Сheck-mail

  • Высокопроизводительный, неблокирующий tcp фреймворк Nbio

  • Быстрый и гибкий DNS-сервер CoreDns

  • Веб-браузер умеет управлять cookie, историей, созданием вкладок, подменой юзер-агента Surf

  • Зашифрованная файловая система GoCryptfs

  • Консольное приложение для отслеживания и мониторинга статистики криптовалют в режиме реального времени Cointop

  • Интерфейс командной строки git Bit

  • Сервис собирает забавные сообщения о коммитах из Github Commits.lol

  • Структура файловой системы, обеспечивающая простой, унифицированный и универсальный API Afero

  • Реализация FrodoKEM, практическая инкапсуляция ключей с квантовой безопасностью FrodoKEM

  • Симулятор движения мыши Busy

Видео

Подкасты

Сообщества

Подробнее..

Golang-дайджест 3 (1 31 марта 2021)

01.04.2021 08:15:50 | Автор: admin

Свежая подборка новостей и материалов

Интересное в этом выпуске

  • Монитор горутин в терминале

  • Пикселизатор изображений

  • Проверка безопасности Go-кода

  • Dropbox load balancing

Приятного чтения!

Новости, события

  • Выпущен релиз 1.16.2

    • Archive/zip CVE-2021-27919 Reader.OpenAPI при работе с ZIP issues

    • encoding/xml CVE-2021-27918 бесконечный цикл при использовании xml.NewTokenDecoder issues

    • syscall & x/sys/windows переполнение буфера issues

    • time/tzdata использует неправильную зону issues

    • Посмотреть все фиксы можно тут

Предложения по улучшению языка

Материалы для обучения

Статьи

Инструменты

  • Framework для тестирования GoMock

  • Клиент redis Redigo

  • IPFS (межпланетная файловая система) это одноранговый протокол и сеть для организации распределенной файловой системы IPFS

  • Быстрый и точный счетчик кода scc

  • Квери билдер для Монги greenleaf

  • Монитор горутин в терминале roumon

  • Сравнение различных объектов. Используется для тестов go-testdeep

  • Инструмент для смены цвета текста в терминале gchalk

  • Автономный инструмент миграции для PostgreSQL tern

  • Dropbox свой L4 лоад балансер kglb

  • Пакет Go, который обеспечивает низкоуровневый доступ к Linux rtnetlink API rtnetlink-xdp

  • Инструмент проверки безопасности кода Go gosec

  • Облачный туннель inlets

  • Инструмент генерирует типобезопасный код из SQL sqlc

  • Инструменты для работы с данными в стиле мультиинструмента (например, xsv для CSV или jq для JSON) sq.io

  • Встраиваемая база данных, совместимая с MongoDB, и набор инструментов для Go LungoDB

  • Высокопроизводительный сервер приложений roadrunner.dev

  • Реализация размытия по Гауссу с линейным временем song2

  • Эффективный пикселизатор изображений pixelizer

  • Компилятор Go, предназначенный для использования в небольших местах, таких как микроконтроллеры, WebAssembly (Wasm) и инструменты командной строки tinygo

  • Сервер аутентификации и авторизации SSO authelia

  • Поиск в вашей файловой системе с помощью SQL-запросов fsql

  • Генератор прокси gRPC в JSON в соответствии со спецификацией HTTP grpc-gateway

  • Библиотека, предоставляющая набор функций, которые позволяют записывать и читать файлы XLSX / XLSM / XLTM excelize

  • Клиент GitHub API v3 go-github

  • Microservice Framework micro v3.2.0

Видео

Подкасты

Сообщества

Подробнее..

Пишем телеграм бота на Go и AWS Lambda

03.05.2021 02:05:54 | Автор: admin

Что будем делать?

Будем писать простой телеграм бот, отвечающий тем же сообщением, что и отправили.

Почему AWS Lambda ?

  1. Удобство деплоя, просто пишешь sls deploy, и lambda уже выгружена

  2. Платишь только за время, когда lambda работает

  3. Не надо настраивать никаких серверов, и беспокоиться о масштабировании

Что понадобится?

  • Установленный go

  • Nodejs и npm для установки serverless

  • AWS аккаунт для деплоя

TLDR

  • Клонируем репозиторий https://github.com/R11baka/echotgbot

  • Устанавливаем в .env файле BOT_TOKEN переменную

  • Компилируем бинарник env GOOS=linux go build -o bin/webhook main.go

  • Выгружаем лямбду с помощью sls deploy

  • Устанавливаем webhook с помощью BOT_TOKEN

Регистрация в AWS

  • Регистирируем пользователя в AWS aws console и получаем aws_access_key_id, и aws_secret_access_key и прописываем их в .aws/credentials файле

Вот как выглядит мой .aws/credentials

cat ~/.aws/credentials[default]aws_access_key_id = ADEFEFEFFEBDXK3aws_secret_access_key = Zy6ewfir/zGaT1B2/o9JDWDSssdrlaregion = us-west-1

Регистрация бота

Для начала, нам надо зарегистировать бота в BotFather. Идем по ссылке, отправляем команду BotFather /newbot, придумываем имя боту, описание. В конце, BotFather вернет нам токен бота.Этот токен понадобится,нам для дальнейшей разработки.

Установка Serverless

Serverless-это framework, облегчающий настройку, деплой AWS Lambda функций. Написан на node, поэтому для его установки понадобится nodejs и npm. Устанавливаем serverless через npm

npm install -g serverless

После установки serverless проверяем, все ли установилось

sls -vFramework Core: 2.35.0 (standalone)Plugin: 4.5.3SDK: 4.2.2Components: 3.8.2

Теперь можно приступить к конфигурации serverless. Все настройки для serverless лежат в serverless.yml файле,который мы и создадим со следующим контентом

service: echoBotuseDotenv: trueconfigValidationMode: error # если в конфиге,чтото неправильно ,то ошибкаframeworkVersion: '>=1.28.0 <2.50.0'provider:  region: "us-west-1"  lambdaHashingVersion: "20201221"  name: aws  runtime: go1.x  logRetentionInDays: 30 # сколько дней хранить логи  endpointType: regional  tracing: # включаем трейсинг для лямбды    apiGateway: true    lambda: true  iam:    role:      statements:        - Effect: "Allow"          Resource: "*"          Action:            - "xray:*"package:  patterns:    - "bin/webhook" # деплоить только бинарникfunctions:  webhook:     handler: bin/webhook    timeout: 15    description: simple echo bot    memorySize: 128 # размер памяти в мегабайтах для функции    environment:      BOT_TOKEN: ${env:BOT_TOKEN}    events:      - http:          path: /webhook          method: ANY          cors: false

Имлементация логики бота на Go

  1. Устанавливаем библиотеки telebot.v2 и aws-lambda-go

      go mod init testBotgo: creating new go.mod: module testBot  go get -u gopkg.in/tucnak/telebot.v2go: gopkg.in/tucnak/telebot.v2 upgrade => v2.3.5go: github.com/pkg/errors upgrade => v0.9.1 go get github.com/aws/aws-lambda-gogo: github.com/aws/aws-lambda-go upgrade => v1.23.0
    

2. Создаем файл main.go с контентом

package mainimport ("encoding/json""fmt""github.com/aws/aws-lambda-go/events""github.com/aws/aws-lambda-go/lambda"tb "gopkg.in/tucnak/telebot.v2""os")func main() {settings := tb.Settings{Token:       os.Getenv("BOT_TOKEN"),Synchronous: true,Verbose:     true,}tgBot, err := tb.NewBot(settings)if err != nil {fmt.Println(err)panic("can't create bot")}tgBot.Handle(tb.OnText, func(m *tb.Message) {message := m.TexttgBot.Send(m.Sender, message)})lambda.Start(func(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {var u tb.Updateif err = json.Unmarshal([]byte(req.Body), &u); err == nil {tgBot.ProcessUpdate(u)}return events.APIGatewayProxyResponse{Body: "ok", StatusCode: 200}, nil})}

Текущая документация в tucnak/telebot.v2 немного устарела, и если просто написать return вместо return events.APIGatewayProxyResponse{Body: "ok", StatusCode: 200}, nil, то телеграм будет повторно отправлять сообщения к боту.

Deploy бота

  • Создаем файл .env и вставляем API_TOKEN полученный от BotFather

    echo API_TOKEN={API_TOKEN_FROM_BOTFATHER} > .env
    
  • Проверяем serverlss конфиг с помощью команды. Не должно быть никаких ошибок.

    sls print
    
  • Потом собираем бинарник

    env GOOS=linux go build  -o bin/webhook main.go 
    
  • И выгружаем его с помощью serverless

    serverless deploy  -v
    

    При успешной выгрузке, мы получим в конце

    Service Informationservice: echoBotstage: devregion: us-west-1stack: echoBot-devresources: 11api keys:Noneendpoints:ANY - https://y7p31bwnu1.execute-api.us-west-1.amazonaws.com/dev/webhookfunctions:webhook: echoBot-dev-webhooklayers:None
    

    https://y7p31bwnu1.execute-api.us-west-1.amazonaws.com/dev/webhook => этот эндпоинт и token бота нужен нам, чтобы установить webhook

Интеграция с telegram

Осталось сообщить телеграму, какой эндпоинт дергать при получении сообщения. Делается это командой setWebhook

curl https://api.telegram.org/bot{YOUR_TOKEN}/setWebhook?url={YOUR_DEPLOYED_AWS_URL}

Проверка что webhook установлен, происходит с помощью getWebhookInfo

  ~ curl https://api.telegram.org/bot1324913549:AAE1zYMH6K3hF2TOgUQoIP-E1g4rMIamck/setWebhook\?url\= https://y7p31bwnu1.execute-api.us-west-1.amazonaws.com/dev/webhook{"ok":true,"result":true,"description":"Webhook was set"}  ~ curl https://api.telegram.org/bot1324913549:AAE1zYMH6K3hF2TOgUQoIP-E1g4rMIamck/getWebhookInfo{"ok":true,"result":{"url":"https://y7p31bwnu1.execute-api.us-west-1.amazonaws.com/dev/webhook","has_custom_certificate":false,"pending_update_count":0,"max_connections":40,"ip_address":"184.169.148.254"}}

Ошибки

Если что-то пошло не так,идем в CloudWatch и смотрим логи,или же из консоли также можно посмотреть логи

sls logs -f webhook
Подробнее..
Категории: Go , Golang , Serverless

Golang-дайджест 5 (1 31 мая 2021)

01.06.2021 16:23:51 | Автор: admin

Свежая подборка новостей и материалов

Интересное в этом выпуске

  • Выпущены версии Go 1.16.4 и 1.15.12

  • Эмулятор Atari VCS

  • Эмулятор Game Boy

  • Фантастическая библиотека 2D-игр

Приятного чтения!

Новости, события

Предложения по улучшению языка

Материалы для обучения

Уроки для изучения Golang

Введение в программирование на Go

Go в примерах

Маленькая книга о Go

Руководство для начинающих по разумным абстракциям с использованием Golang

Книга, посвященная синтаксису/семантике

Go-patterns (паттерны проектирования с примерами на Golang)

Статьи

Инструменты

  • Web-компоненты на Go gomponents

  • Простенькое решение для фича флагов go-feature-flag

  • Виртуальные машины Linux с автоматическим общим доступом к файлам, переадресацией портов и контейнером lima

  • Инструмент для безопасного разделения и распространения ваших закрытых ключей cocert

  • Создание конвейеров сборки в Go на этот пакет сильно повлиял пакет тестирования, что становится очевидным при запуске конвейера goyek

  • Простая структура RPC с определениями служб Protobuf twirp 8.0

  • Инструмент для генерации безопасных типов из SQL sqlc 1.8

  • Фантастическая библиотека 2D-игр Ebiten 2.1.0

  • Веб-фреймворк, вдохновленный Express.js. Так что, если вам нравится это в мире узлов, проверьте это Fiber 2.9.0

  • Реализации фильтров Блума Bloom 3.0

  • Высокопроизводительный минималистичный веб-фреймворк Echo 4.3

  • Усовершенствованный HTTP-клиент Heimdall 7.0

  • Основанная на графах база данных, написанная на Go EliasDB

  • Выполнение JavaScript из Go v8go 0.6.0

  • Терминальный клиент для Mangadesk

  • Middleware http для безопасности Secure

  • Простое и полное решение для флагов функций go-feature-flag

  • Управление доступом на основе ролей (RBAC) с сохранением базы данных Authority 2.0

  • Client для New Notion API go-notion

  • Эмулятор Atari VCS относительно полнофункциональный эмулятор, который включает поддержку контроллера и запись игрового процесса Gopher2600

  • Эмулятор Game Boy Advance, написанный на Go не для того, чтобы играть в фаворитов с классическими игровыми устройствами. Как насчет эмулятора для GBA от Nintendo Magia

  • Библиотека MySQL go-mysql 1.2.0

  • Менеджер email-рассылки listmonk

  • Экспериментальный инструмент рефакторинга Rf

  • GitHub Actions для сохранения старых зависимостей go-mod-archiver

  • Анализатор использования диска с консольным интерфейсом gdu 5.0

  • Быстрый, гибкий Linter for Go Revive

  • Библиотека изменения размера изображения с учетом содержимого Caire 1.3

  • Простая проверка типа JSON go-map-schema

  • Пакет вывода цветного текста в консоли color 1.12.0

  • Визуализация ответов в формате JSON, XML, двоичных данных и HTML-шаблонов Render 1.4.0

  • Официальный клиент Go для Elasticsearch go-elasticsearch 7.13.0

  • База данных SQL на чистом Go go-sqldb

  • io.Readerа io.Writer с ограничениями LimitIO

Видео

Плейлист с GopherCon Russia 2021

5-минутное видео, показывающее, насколько легко запускать и отлаживать тесты с помощью Delve

Подкасты

GenericTalks

Go Time

Сообщества

Вопросы по языку на русскоязычном StackOverflow

Информация о митапах

Форум в группах Google

https://t.me/vseins_tech

Подробнее..
Категории: Go , Golang , Digest

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru