Пишите надежный и эффективный код
По мере роста размера приложения становится все более важным отслеживать активные процессы и правильно обрабатывать их, когда они больше не нужны. В противном случае это может привести к пустой трате вычислительного времени и ресурсов.
Рассмотрим запрос, поступающий на веб-сервер, как только он получен, сервер начинает выполнять соответствующие задачи. Однако в случае отключения клиента сервер продолжит обработку запроса, даже если его ответ больше не нужен. Пакет контекста может помочь нам определить, когда такие процессы больше не нужны и их можно остановить.
В этом посте мы рассмотрим, как создать и использовать пакет контекста для повышения надежности приложения Golang.
Контекст
Пакет context в go — это встроенная библиотека, определенная в интерфейсе Context. Метод Done особенно важен, поскольку он позволяет нам определить, когда контекст завершается:
type Context interface { ... Done() <-chan struct{} // Other methods below ... }
Рассмотрим следующую горутину:
func GetValue(ctx context.Context, output chan<- int) error { for { v, err := GetInt(ctx) if err != nil { return err } select { case <-ctx.Done(): return nil case output <- v: } } }
Структура for-select будет продолжать отправлять значения в выходной канал до тех пор, пока канал метода Done не будет разблокирован.
По соглашению в Go контекст задается первым параметром функции. В этой ситуации контекст используется для остановки операции, которая выполняется как горутина. Обычно всякий раз, когда у вас есть вызов API в вашем коде, вы также должны включать в него контекст.
Создание контекста
Существуют разные способы создания контекста. Это можно делать всякий раз, когда вы запускаете подпрограммы, для которых необходимо контролировать их завершение. Кроме того, горутины могут даже иметь дочерние процедуры, управляемые путем получения других контекстов от родительского.
Контекст TODO
TODO — это пустой фиктивный контекст, который можно использовать, когда еще не ясно, как должен обрабатываться конкретный запрос или подпрограмма. Его создание происходит следующим образом:
ctx := context.TODO()
Фоновый контекст
Существует еще один вид базового контекста, который можно использовать, он называется Background
. Это тоже пустой контекст, для его создания используется функция context.Background
из пакета.
ctx := context.Background()
Фон против TODO
Сначала оба контекста кажутся одинаковыми. Как оказалось, они есть, но они служат для демонстрации разных целей в вашем коде.
- Контекст TODO сигнализирует о том, что предстоит еще поработать. Другими словами, его можно использовать как временный вход для определенного запроса API или горутины.
- Фоновый контекст дает понять, что вы намерены создать пустой контекст. Обычно он служит корневым контекстом, из которого могут быть получены другие контексты.
Поскольку оба пусты, они мало что могут сделать. Давайте посмотрим, как мы можем извлечь из них более сложные контексты.
Передача значений в контекст
Контексты в Go могут переносить значения, которые можно идентифицировать по определенному ключу (точно так же, как работает карта).
func task(ctxWithTimeout context.Context) { // New Context creation. ctx := context.WithValue(ctxWithTimeout , "id", 0) // Retrieving the id value id:= ctx.Value("id") ... }
Значения внутри контекстов являются переменными только для чтения, их можно использовать для обмена значениями между несколькими горутинами.
Хотя контексты могут нести в себе любую ценность, вы должны использовать их с осторожностью. Если вы включите слишком много переменных внутри, это может привести к нечеткому и сложному в сопровождении коду. Как правило, если вам нужно хранить переменную, которая будет использоваться только в одном месте, лучше передать ее как обычный параметр.
Завершение контекста
Пустой контекст Background или TODO не имеет каких-либо указаний относительно завершения. К счастью, существуют разные способы создания контекста, точно определяющего, когда он должен остановиться.
Завершение функцией отмены
Функция отмены обеспечивает прямой способ управления отменой контекста:
ctx, cancel := context.WithCancel(ctx) outputInts := make(chan int) go GetValue(ctx, outputInts) for i := 1; i <= 3; i++ { print(<-outputInts) } cancel()
На приведенной ниже диаграмме показано, что происходит во фрагменте выше:
Где V1, V2, V3 обозначают значения 1, 2 и 3.
Функция WithCancel
дает возможность контролировать точный момент окончания контекста. В приведенном выше примере подпрограмма GetValue
отправляет в выходной канал три значения, которые будут считаны и распечатаны. После третьей итерации цикла for вызывается функция отмены.
В структуре for-select после вызова функции отмены будет включена первая альтернатива оператора select, которая сразу же завершает горутину.
Важно отметить, что родительский контекст (тот, который используется в качестве входных данных для функции WithCancel
) не закончится при вызове функции отмены, он останется нетронутым. Напротив, когда родительский контекст завершается, все его дочерние контексты также завершаются.
Завершение с крайним сроком
Существует возможность завершить контекст, определив крайний срок, до которого он должен быть завершен. Например:
Данная задача начала выполняться сегодня в 15:00, в контексте крайнего срока мы можем определить, что она должна завершиться к 15:10.
После того, как мы достигнем крайнего срока, контекст автоматически отправит сигнал завершения задачам, зависящим от него.
Контекст крайнего срока может быть создан с помощью функции WithDeadline
, которая принимает в качестве входных данных родительский контекст и значение Golang Time, указывающее, когда его следует отменить. Как и в случае с функцией отмены, когда крайний срок превышает родительский контекст, это не будет затронуто.
cancelDeadline := time.Now().Add(10 * time.Second) ctx, cancel := context.WithDeadline(ctx, cancelDeadline) defer cancel() outputInts := make(chan int) go GetValue(ctx, outputInts) for i := 1; i <= 3; i++ { print(<-outputInts) }
В этом случае мы указали крайний срок контекста с помощью пакета времени в Go, который является еще одной доступной стандартной библиотекой.
Функция WithDeadline
также возвращает функцию отмены, которая позволяет нам отменить контекст до достижения крайнего срока.
В приведенном выше примере, взяв момент t=0 в качестве начала, мы установили крайний срок на момент t=10.
Завершение с тайм-аутом
Наконец, есть также возможность завершить контекст, передав значение тайм-аута в момент создания контекста. Он работает почти так же, как и крайний срок, разница в том, что мы указываем непосредственную продолжительность времени для контекста.
В случае с функцией WithDeadline
нам нужно было указать точную временную метку, с которой контекст должен был завершиться. В ситуации тайм-аута мы указываем, как долго он должен длиться.
Обычно этот вариант будет более полезным по сравнению с предыдущим. Тем не менее, имея оба варианта, вы можете выбрать тот, который подходит вам больше всего.
ctx, cancel := context.WithTimeout(ctx, 10 * time.Second) defer cancel() outputInts := make(chan int) go GetValue(ctx, outputInts) for i := 1; i <= 3; i++ { print(<-outputInts) }
Думая о запросе к серверу, например, вы можете установить, что запрос должен занимать не более 5 секунд. Функция WithTimeout
берет на себя ответственность за определение точного момента, когда контекст должен закончиться.
Вот некоторые из способов, которые мы можем использовать для оптимизации использования ресурсов в приложении Golang. Это может быть полезно, чтобы избежать проблем с утечкой памяти, которые могут быть труднее понять по мере роста приложения.
Кроме того, в случаях, когда вы используете сервер, вам может потребоваться выполнить некоторые задачи по очистке перед завершением работы приложения.
Лучшие практики
Кроме того, вот некоторые из лучших практик, которые мы используем при работе с контекстным пакетом в go.
- Всегда вызывайте функцию отмены с оператором отсрочки. Он позаботится об отмене контекста, несмотря на то, сколько операторов return было добавлено.
- Контекст TODO следует использовать как временный ввод контекста. Замените его, как только станет ясно, какой контекст следует там использовать.
- Не забудьте указать тайм-аут/сроки выполнения задач, которые потенциально могут оставаться без дела в течение длительного времени.
- Используйте фоновый контекст в корне вашего приложения и извлекайте из него другие контексты.
- То, что вы можете хранить что угодно в контексте, не означает, что вы должны это делать. Подумайте, действительно ли это необходимо, так как это может затруднить понимание вашего кода.
Заключение
В этом руководстве мы рассмотрели некоторые параметры, которые пакет контекста дает нам для решения некоторых распространенных проблем в Go при управлении несколькими горутинами. Пакет предоставляет несколько опций для гибкого создания и обработки контекстов.
Контексты дают возможность определить, когда задача должна завершиться, и при необходимости выполнить любое задание по очистке, функции WithTimeout
, WithDeadline
и WithCancel
предоставляют много возможностей.
В целом, это еще один инструмент, который можно использовать для разработки более надежного и эффективного кода. Спасибо, что дочитали до конца, надеюсь, этот урок был вам полезен!