Когда в 2014 году я впервые начинал писать приложения на Golang, мое внимание сразу же привлекло самое необычное и интересное, что есть в этом языке: конкурентность и каналы. Провозившись с кучей строк кода с ошибками, едва поддающимися объяснению, я освоил кое-какие шаблоны. Эти шаблоны используют преимущества конкурентности, уменьшая при этом количество ошибок и значительно улучшая удобство кода с точки зрения его восприятия человеком.
Эта статья написана с целью помочь сэкономить вам время и силы. Ведь в ней четко показано все то, что мне удалось за эти несколько лет самостоятельно собрать воедино. Надеюсь, это станет познавательным, применимым на практике реальным примером использования конкурентности в API.
Шаблон конкурентности API
Следуя приведенным ниже правилам, вы создадите API с высокой степенью конкурентности при минимуме ошибок и усилий.
Разработайте блокирующие операции, чтобы…
- Всегда запускаться асинхронно с помощью ключевого слова
go
для выполнения этих операций в многопоточном режиме. make
(открывать) иclose
(закрывать) выделенные каналы для минимизации вероятности утечек памяти и взаимоблокировок.- Использовать
context.Context
для остановки ожидающих запросов, которые больше не нужны. - Использовать указатели для возвращения результатов вместо каналов. Так уменьшится количество каналов, которыми нужно управлять.
- Выдавать ошибки через
<-chan error
. Так вы дождетесь завершения блокирующих операций до возвращения ответов.
Вот и все! Придерживаясь этих пяти пунктов, вы напишете хорошо организованный и удобный для восприятия человеком код на Go, обладающий высокой степенью конкурентности и не подверженный взаимоблокировкам или утечкам памяти.
Пример кода
Ниже приведен пример реализации с использованием этих правил. Он показывает, как создать удобную для восприятия человеком кодовую базу API с высокой степенью конкурентности, легкую в тестировании и сопровождении.
API-запросы
Этап 1 реализации этого шаблона — это асинхронная отправка всех API-запросов и блокирующих операций.
// Piece — это фрагмент результата
type Piece struct {
ID uint `json:"id"`
}
// getPiece вызывает `GET /piece/:id`
func getPiece(ctx context.Context, id uint, piece *Piece) <-chan error {
out := make(chan error)
go func() {
// Корректное управление памятью — всегда закрывайте... каналы
defer close(out)
// NewRequestWithContext немедленно отменит свой запрос, когда
// источник вызова отменит контекст
req, err := http.NewRequestWithContext(
ctx,
"GET",
fmt.Sprintf("api.url.com/piece/%d", id),
nil,
)
if err != nil {
out <- err
return
}
// Отправляем запрос
rsp, err := http.DefaultClient.Do(req)
if err != nil {
out <- err
return
} else if rsp.StatusCode != http.StatusOK {
out <- fmt.Errorf("%d: %s", rsp.StatusCode, rsp.Status)
return
}
// Выполняем парсинг ответа в piece
defer rsp.Body.Close()
if err := json.NewDecoder(rsp.Body).Decode(piece); err != nil {
out <- err
return
}
}()
return out
}
Ответы от API
Этап 2 реализации шаблона — это объединение нескольких блокирующих операций и API-запросов в один ответ от API struct
.
/ Результат — комбинация нескольких блокирующих операций,
// которые будут получены одновременно
type Result struct {
FirstPiece *Piece `json:"firstPiece,omitempty"`
SecondPiece *Piece `json:"secondPiece,omitempty"`
ThirdPiece *Piece `json:"thirdPiece,omitempty"`
}
// GetResult — это `http.HandleFunc`, который получает (с помощью GET) `Result`(результаты)
func GetResult(w http.ResponseWriter, r *http.Request) {
// Выполняем парсинг и проверку вводимых данных...
// getResult немедленно остановится, когда произойдет отмена http.Request
var result Result
if err := <-getResult(r.Context(), &result); err != nil {
w.Write([]byte(err.Error()))
w.WriteHeader(http.StatusInternalServerError)
return
}
// Выполняем маршалинг ответа
bs, err := json.Marshal(&result)
if err != nil {
w.Write([]byte(err.Error()))
w.WriteHeader(http.StatusInternalServerError)
return
}
// Успешно!
w.Write(bs)
w.WriteHeader(http.StatusOK)
}
// getResult возвращает результат многочисленных вызовов API с высокой степенью конкурентности
func getResult(ctx context.Context, result *Result) <-chan error {
out := make(chan error)
go func() {
// Корректное управление памятью
defer close(out)
// Функция cancel позволит остановить все ожидающие запросы, когда один
// завершится неуспешно
ctx, cancel := context.WithCancel(ctx)
// Merge позволяет получить все ошибки, возвращенные из всех
// вызовов в `getPieces` в одном `<-chan error`.
// Когда не вернется ни одной ошибки, Merge будет ждать, пока все
// `<-chan error` закроются, прежде чем продолжить
for err := range util.Merge(
getPiece(ctx, 1, result.FirstPiece),
getPiece(ctx, 2, result.SecondPiece),
getPiece(ctx, 3, result.ThirdPiece),
) {
if err != nil {
// Отменяем все ожидающие запросы
cancel()
// Выдаем ошибку в источник вызова
out <- err
return
}
}
}()
return out
}
Функция Merge
Этап 3 — это реализация единственной функции ‘слияния’ func
. Даже если вы хорошо разбираетесь в Go, эта часть паззла наверняка будет самой сложной, а вероятность возникновения ошибок здесь — самой высокой. Рекомендую скопировать и вставить этот код прямо в пакет util
или использовать тот, что есть в пакете конвейера Delivery Hero.
package util
import (
"sync"
)
// Merge объединяет нескольких каналов ошибок в один
func Merge(errChans ...<-chan error) <-chan error {
mergedChan := make(chan error)
// Создаем WaitGroup, которая ожидает закрытия всех errChans
var wg sync.WaitGroup
wg.Add(len(errChans))
go func() {
// Когда все errChans будут закрыты, закрываем mergedChan
wg.Wait()
close(mergedChan)
}()
for i := range errChans {
go func(errChan <-chan error) {
// Ожидаем закрытия каждого errChan
for err := range errChan {
if err != nil {
// Объединяем содержимое каждого errChan в mergedChan
mergedChan <- err
}
}
// Сообщаем WaitGroup, что один из errChans закрыт
wg.Done()
}(errChans[i])
}
return mergedChan
}
Подведем итоги
Существует много разных подходов в отношении к конкурентности в Go. Здесь мы изложили четкий и эффективный подход к конкурентности при создании API: он сохраняет код хорошо организованным и сводит к минимуму ошибки управления памятью.
Надеюсь, этот подход станет для вас полезным и применимым на практике примером конкурентности в Golang. И сэкономит немного времени и нервных клеток, избавляя от необходимости собирать по крупицам всю эту информацию самостоятельно.
Остаётся только попрактиковаться!
Читайте также:
- Создаем настраиваемую цепочку обязанностей в Go
- Go. Прорабатываем 25 основных вопросов собеседования
- Почему Dockerfile больше не нужен для создания контейнера в Go
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Mark Salpeter: Concurrent API Patterns in Go