Найти в Дзене
Nuances of programming

Использование конкурентности при создании API в Go

Источник: Nuances of Programming Когда в 2014 году я впервые начинал писать приложения на Golang, мое внимание сразу же привлекло самое необычное и интересное, что есть в этом языке: конкурентность и каналы. Провозившись с кучей строк кода с ошибками, едва поддающимися объяснению, я освоил кое-какие шаблоны. Эти шаблоны используют преимущества конкурентности, уменьшая при этом количество ошибок и значительно улучшая удобство кода с точки зрения его восприятия человеком. Эта статья написана с целью помочь сэкономить вам время и силы. Ведь в ней четко показано все то, что мне удалось за эти несколько лет самостоятельно собрать воедино. Надеюсь, это станет познавательным, применимым на практике реальным примером использования конкурентности в API. Шаблон конкурентности API Следуя приведенным ниже правилам, вы создадите API с высокой степенью конкурентности при минимуме ошибок и усилий. Разработайте блокирующие операции, чтобы… Вот и все! Придерживаясь этих пяти пунктов, вы напишете хорошо
Оглавление

Источник: Nuances of Programming

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

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

Шаблон конкурентности API

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

-2

Разработайте блокирующие операции, чтобы…

  1. Всегда запускаться асинхронно с помощью ключевого слова go для выполнения этих операций в многопоточном режиме.
  2. make (открывать) и close (закрывать) выделенные каналы для минимизации вероятности утечек памяти и взаимоблокировок.
  3. Использовать context.Context для остановки ожидающих запросов, которые больше не нужны.
  4. Использовать указатели для возвращения результатов вместо каналов. Так уменьшится количество каналов, которыми нужно управлять.
  5. Выдавать ошибки через <-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. И сэкономит немного времени и нервных клеток, избавляя от необходимости собирать по крупицам всю эту информацию самостоятельно.

Остаётся только попрактиковаться!

Читайте также:

Читайте нас в Telegram, VK

Перевод статьи Mark Salpeter: Concurrent API Patterns in Go