Golang context для чайников

2023-10-07T13:37:24+05:00 | 5 минут чтения | Обновлено 2024-07-20T13:48:37+05:00

Это кусок курса по go, который я преподавал когда-то давно.

В Go есть стандартный пакет сontext. У него две основные задачи:

  • отменять какие-либо операции
  • передавать параметры от задачи к задаче

Отмена каких-либо операций

Пример 1. Отмена тяжелого процесса

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

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

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

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

Пример 2. Отмена параллельных операций если цель достигнута

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

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

Аналогично нужно поступить когда мы пишем программу, которая выполняет несколько параллельных действий. Например, нам нужно подобрать pin code от секретного архива. Чтобы ускорить дело, вы запустили 4 горутины. Первая перебирает pin от 0 до 2499, вторая от 2500 до 4999 и так далее. Представьте что первая подобрала pin код, очевидно остальным горутинам нужно сказать о том, что можно прекратить работу.

Как можно отменить контекст?

Отменять контекст можно разными способами

  • явно - context.WithCancel
  • по таймауту - context.WithTimeout
  • при наступлении какого-либо дедлайна context.WithDeadline

При использовании контекста с context.WithTimeout или context.WithDeadline мы также сохраняем возможность отменить такой контекст явно.

Контексты в Go иммутабельны, другими словами, мы не можем изменить контекст, а может только создать новый контекст на основании другого. За исключением context.Background(). Это такой контекст-заглушка для создания других контекстов. Ну например, если мы хотим получить контекст с таймаутом то мы должны написать:

// Создаем контекст с таймаутом в 5 секунд
// Второй возвращаемый параметр это функция canel, которая 
// используется для явной отмены контекста
ctx, cancel := context.WithTimeout(context.Background(), time.Second * 5)

Пример прерывания тяжелой функции

Давайте разберем простой пример:


package main

import (
	"context"
	"fmt"
	"math/rand"
	"time"
)


// Эта функция эмулятор тяжелой задачи. Она генерирует 
// случайные числа в бесконечном цикле и если число
// меньше чем 0.0000001, то выводит его на экран.
// Контекст используется для остановки этой функции
func heavyTask(ctx context.Context) {
	
	for {
		select {
		case <- ctx.Done():
			// Если в канал возвращаемый функцией ctx.Done() прилетает значение, значит нужно прекратить работу
			fmt.Println("Heavy task canceled")
			return
		default:
			// Если никаких значений в канал не поступало, то продолжаем нашу работу
			t := rand.Float64()
			if t < 0.0000001 {
				println(t)
			}
		}
	}
}


// Запускает функцию эмулятор тяжелой задачи. 
// Эта функция, будет отменена по таймауту через 5 секунд или явно
// если вы нажмете Enter на клавиатуре
func main() {
	// Создаем новый контекст с таймаутом 5 секунд
	ctx, cancel := context.WithTimeout(context.Background(), time.Second * 5)
	// На всякий случай отменяем наш контекст при завершении работы программы
	defer cancel()

	// Запускаем горутину которая при нажатии на клавишу Enter на клавиатуре явно отменяет наш контекст
	go func() {
		fmt.Scanln()
		cancel()
	}()

	// Запускаем нашу функцию эмулятор
	heavyTask(ctx)
}

Пример отмены работы группы горутин

Давайте разберем еще один пример, подберем случайно сгенерированный pin код.

package main

import (
	"context"
	"fmt"
	"math/rand"
	"time"
)

// Эта функция перебирает числа от start до end и если 
// число соответствует secretPin то,
// отправляет найденное число в канал result
func findPin(ctx context.Context, start int, end int, secretPin int, result chan int) {
	for i := start; i < end; i++ {
		select {
		case <-ctx.Done():
			fmt.Printf("Pin finder %v-%v number canceled\n", start, end)
			return
		default:
			if i == secretPin {
				// Ура, мы нашли то, что искали
				result <- i
				return
			}
		}
	}
}

func main() {
	// Загадываем секретное число
	var secretPin = rand.Intn(10000)
	result := make(chan int)

	// Создаем контекст с явной отменой
	ctx, cancel := context.WithCancel(context.Background())

	// Запускаем 4 горутины каждая из которых ищет число в заданном диапазоне
	for i := 0; i < 4; i++ {
		go findPin(ctx, i*2500, (i+1)*2500, secretPin, result)
	}

	// Ждем результата
	found := <-result
	fmt.Printf("Secret pin found %v\n", found)

	// После того как число найдено отменяем контекст
	cancel()

	// Ждем немного чтобы горутины успели напечатать данные в консоль, 
  // правильно был бы использовать для
	// ожидания работы всех горутин sync.WaitGroup, 
  // но наш пример намеренно упрощен.
	time.Sleep(time.Second)
}

Передача параметров

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

Для передачи параметров вместе с контекстом нужно использовать специальную функцию withValue, а для получения функцию контекста Value

ctx := context.WithValue(context.Background(), "username", "Foo")

fmt.Printf("%v\n", ctx.Value("username")) // Напечатает Foo
fmt.Printf("%v\n", ctx.Value("password")) // Напечатает nil

Пример на go playgound https://go.dev/play/p/yTnUAG7qFt4

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

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

Заключение

  • Когда стоит применить контекст? Тогда, когда мы вызываем функцию которая может потенциально долго отвечать.
  • Создание контекста начинается с контекста заглушки context.Background()
  • Контекст обычно передается первым аргументом в функцию
  • Избегайте передачи параметров через контекст, если можно передать их через аргументы функции, то используйте аргументы функции.

© 2022 - 2024 pahanini.com - записная книжка

🌱 Powered by Hugo with theme Dream.

О себе

Меня зовут Павел. Работаю в sima-land.ru

Отвечаю за большую часть ИТ инфраструктуры. Умею писать код, руководить людьми. Разбираюсь во многих аспектах ИТ: программирование, управление, архитектура.

Еще у меня много разных хобби не связанных с ИТ.

Этот сайт я использую как записную книжку. Может эта информация будет полезна кому-то еще.

Фото

Все фото на сайте мои и чаще никак не связаны с темой статьи.

Лицензия(Creative Commons)

Контент сайта лицензирован CC BY-NC-SA 4.0

All contents of this website are licensed under CC BY-NC-SA 4.0

Социальные сети