Найти в Дзене
Worst Programming

Golang, ну все. Хорош... Не делай так... Как правильно отменять контекст.

Оглавление
Идеальная программа. Ее я написал уже сто раз и каждый раз без багов.
Идеальная программа. Ее я написал уже сто раз и каждый раз без багов.

Ну ладно, я сразу к делу.

Когда я вижу какую то очередную статью типа "как корректно завершить приложение на go" в которой вижу вот такие кусочки кода, то сразу хватаюсь за голову...

cancel используется для немедленного освобождения ресурсов. ок... я сделал это сегодня
cancel используется для немедленного освобождения ресурсов. ок... я сделал это сегодня

Ну ладно, я не буду тут паясничать, сразу скажу, что не так. Во-первых, в представленном коде defer cancel - это как если вы выходя из дома решили не закрывать дверь - она же сама постепенно закроется, пока вы спускаетесь по лестнице. Я сейчас, как автор этой "сломанной статьи" приведу пример из официальной документации GO.

взято отсюда: https://pkg.go.dev/context#WithTimeout
взято отсюда: https://pkg.go.dev/context#WithTimeout

Не понятно, что не так? А вот что: скажи те ка мне, что отменяет автор? Какой то контекст? Что то, что работало в его приложении в рамках контекста, а потом с помощью отмены контекста получило сигнал о прерывании работы? Звучит логично, но в коде написано совершенно не то - когда приложение получило сигнал о прерывании os.SIGTERM, автор создает пустой контекст с таймаутом и завершает его отложенным вызовом при выходе из процедуры main...

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

Давайте раз и навсегда (в стопервый раз и навсегда) запомним.

context.Сontext

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

  1. контекст = иду за едой;
  2. событие, которое обессмысливает контекст = доставка пиццы;
  3. отмена контекста = звонок мне на телефон.

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

примерная последовательность действий
примерная последовательность действий

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

На 28 строке я создаю контекст - в этом месте я еще не иду в магазин за едой, но уже знаю, что если я задержался дольше, чем на 3 секунды - то "нахер" мне такая еда... все отменится.

30я строка - отдельной рутиной я запускаю свой поход за хлебом. Передаю ей контекст, через который жена может мне позвонить и все отменить (который, кстати сказать, отменится сам через 3 секунды), а вот мне еще права на запись в канал done, чтоб жена знала, что я вернулся домой (сама то она ведь не поймет).

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

signal.Notify([канал], [список сигналов ОС, которые будем получать])
  • SIGINT - сигнал об остановке нашего приложения от ОС
  • SIGTERM - сигнал о завершении нашего приложения от ОС

после вызова signal.Notify эти типы сигналов не будут завершать ваше приложение, а будут поступать в канал, который вы прочтете и сами завершите свое приложение.

На строке 35 сигнал будет получен и... о боже! Почему cancel без defer? Ладно, больше не паясничаю... defer - это отложенный запуск, процедура вызываемая с defer будет выполнена только после выхода из текущей процедуры (той в которой написан defer). Зачем нам сообщать об отмене по выходу из main? Мы что совсем идиоты? Ведь выход из main освободит все ресурсы немедленно и наша отмена вообще никому не будет нужна... Это как если бы мне позвонила жена, я бы поднял трубку, а она решила "похер, лучше я разведусь..." и положила трубку... ахах.... Было бы неприятно. Ну так вот и defer - это неприятно, если ты не знаешь четко, что это такое и что он нужен там, где ты его пишешь.

Дальше все просто - ждем done, потому что пиццу можно есть только, когда муж вернулся, иначе будет горько...

Давай еще пару советов и без сарказма

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

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

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

Хорошие гошные библиотеки умеют сами работать в контекстах. NewRequestWithContext
Хорошие гошные библиотеки умеют сами работать в контекстах. NewRequestWithContext

Видите, там наверху есть defer close(done) - вот нормальный отложенный вызов - когда мы выйдем из процедуры, канал done закроется, сообщив вызывающей рутине о завершении процесса. Проще говоря: "эй, я все сделал и уже почистил за собой!". Ну а http.DefaultClient.Do само должно следить за актуальностью контекста, который был ассоциирован с http реквестом и прерывать выполнение, почуяв cancel или timeout. Таковы правила.

Ну ок, а как быть, если у нас есть самописные длинные асинхронные процессы, как правильно получать и обрабатывать отмену контекста в них? Да все очень просто...

это, если в цикле несколько итераций some stuff
это, если в цикле несколько итераций some stuff

Циклите? Просто проверяйте в начале каждой итерации, что <- ctx.Done канал не закрылся.

Читаете еще из какого-то канала? Читайте сразу из двух!
Читаете еще из какого-то канала? Читайте сразу из двух!

Читаете другой канал? Читайте из него, а еще и читайте из ctx.Done, тогда будете знать, когда остановиться... Ну и все в таком духе, паттернов несколько, а ctx.Done() один.

Напоследок

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

Не делай так!
Не делай так!

Не делай так... handleTermination получает адрес функции cancel и запускает sendTerminationMessage, который вроде как должен выполнить все нужные делишки, которые нужно выполнить перед завершением, а потом сделать cancel...

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

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

Слоеный пирог. Мммм...
Слоеный пирог. Мммм...

Обозначь четкие границы контекстов и не пиши гайдов, если не знаешь, как это работает...