Как правилно да използвате контекста. Контекст в Go 1.7

Тази публикация ще говори за нова библиотека в Go 1.7, контекстната библиотека и кога или как правилно да я използвате. Задължително четене за начало е уводната публикация, която говори малко за библиотеката и като цяло как се използва. Можете да прочетете документация за контекстната библиотека на tip.golang.org.

Как да интегрирате Context във вашия API

Най-важното нещо, което трябва да запомните, когато интегрирате Context във вашия API е, че той е предназначен да бъде обхванат. Например би имало смисъл да съществува по едно запитване към база данни, но не би имало смисъл да съществува по обект на база данни.

В момента има два начина за интегриране на обектите от контекста във вашия API:

  • Първият параметър на извикване на функция
  • Незадължителна конфигурация в структура на заявка

За пример на първия вижте Dialer.DialContext в мрежата за пакети. Тази функция прави нормална операция на набиране, но я отменя според обекта Context.

func (d * Dialer) DialContext (ctx контекст. Контекст, мрежа, адресен низ) (Conn, грешка)

За пример на втория начин за интегриране на контекста, вижте пакета net / http's Request.WithContext

func (r * Заявка) WithContext (ctx context.Context) * Заявка

Това създава нов обект Request, който завършва в съответствие с дадения контекст.

Контекстът трябва да тече през вашата програма

Чудесен умствен модел на използване на Context е, че той трябва да преминава през вашата програма. Представете си река или течаща вода. Това обикновено означава, че не искате да го съхранявате някъде като в структура. Нито искате да го поддържате повече от строго необходимото. Контекстът трябва да бъде интерфейс, който се предава от функция, за да функционира надолу в стека ви повиквания, допълнен според нуждите. В идеалния случай обект Context се създава с всяка заявка и изтича, когато заявката приключи.

Единственото изключение да не съхранявате контекст е, когато трябва да го поставите в структура, която се използва чисто като съобщение, което се предава през канал. Това е показано в примера по-долу.

В този пример нарушаваме това общо правило да не съхраняваме Context, като го поставяме в съобщение. Това обаче е подходящо използване на Context, тъй като все още тече през програмата, но по канал, а не следа в стека. Също така забележете тук как Контекстът се използва на четири места:

  • За да изчакате q, в случай че процесорът е твърде пълен
  • За да уведомят q дали трябва дори да обработва съобщение
  • За да изчакате q изпращането на съобщението до newRequest ()
  • За да изчакате новRequest (), чакащ отговор обратно от ProcessMessage

Всички блокиращи / дълги операции трябва да бъдат отменяни

Когато отнемете възможността потребителите да отменят продължителни операции, вие обвързвате goroutut по-дълго, отколкото потребителят иска. С преместването на Context в стандартната библиотека с Go 1.7, той лесно ще се превърне в стандартната абстракция за определяне на времето или приключване на ранните продължителни операции. Ако пишете библиотека и функциите ви може да блокират, това е перфектен случай за използване на Context.

В горния пример ProcessMessage е бърза операция, която не се блокира, така че контекстът очевидно е излишен. Ако обаче това беше много по-дълга операция, то използването на Context от обаждащия се позволява newRequest да продължи, ако отнема твърде много време за изчисляване.

Context.Value и обхват на заявките (предупреждение)

Най-спорната част на Context е Value, която позволява произволни стойности, поставени в контекст. Предназначената употреба за Context.Value от оригиналната публикация в блога е стойности, обхванати от заявката. Стойността на обхвата на заявката е тази, получена от данните във входящата заявка и отпада, когато заявката приключи. Тъй като заявката отскача между услугите, тези данни често се поддържат между RPC повиквания. Първо, нека се опитаме да изясним какво е или не искане.

Очевидните данни за обхвата на заявката могат да бъдат кой прави заявката (идентификационен номер на потребителя), как я правят (вътрешна или външна), откъде я правят (потребителски IP) и колко важна трябва да бъде тази заявка.

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

Логърът не е обхванат заявка, ако седи на сървърния обект или е сингъл на пакета. Ако обаче съдържа метаданни за това кой е изпратил заявката и може би ако в заявката е активирана регистрация на грешки, тогава тя става обхваната на заявката.

За съжаление данните, обхванати от заявката, могат да обхващат голям набор от информация, тъй като в някакъв смисъл всички интересни данни в приложението идват от заявка. Това поставя широко определение за това какво може да бъде включено в Context.Value, което улеснява злоупотребата. Аз лично имам по-тясна представа за това какво е подходящо в Context.Value и ще се опитам да обясня позицията си в останалата част от тази публикация.

Context.Value затъмнява потока на вашата програма

Истинската причина толкова много ограничения са поставени за правилното използване на Context.Value е, че той затъмнява очакваното въвеждане и извеждане на функция или библиотека. Мнозина го мразят по същите причини хората мразят единични. Параметрите на дадена функция са ясни, самодостатъчна документация за това какво е необходимо, за да може функцията да се държи. Това прави функцията лесна за тестване и разсъждение за интуитивно, както и за рефактор по-късно. Например, помислете за следната функция, която прави автентификацията от Context.

func IsAdminUser (ctx context.Context) bool {
  x: = token.GetToken (ctx)
  userObject: = auth.AuthenticateToken (x)
  върнете userObject.IsAdmin () || userObject.IsRoot ()
}

Когато потребителите се обаждат на тази функция, те виждат само, че е необходим контекст. Но необходимите части, за да знаете дали потребителят е администратор, са две неща: услуга за удостоверяване (в случая се използва като сингъл) и маркер за удостоверяване. Можете да го представите като входове и изходи като по-долу.

Поток IsAdminUser

Нека представим ясно този поток с функция, премахвайки всички единични бутони и контексти.

func IsAdminUser (символен низ, authService AuthService) int {
  userObject: = authService.AuthenticateToken (маркер)
  върнете userObject.IsAdmin () || userObject.IsRoot ()
}

Тази дефиниция на функцията вече е ясно представяне на това, което трябва да се знае, ако потребителят е администратор. Това представяне е очевидно и за потребителя на функцията и прави рефакторинга и повторната употреба на функцията по-разбираема.

Context.Value и реалностите на големите системи

Силно съчувствам на желанието да изтласкам елементи в Context.Value. Сложните системи често имат слоеве на междинен софтуер и множество абстракции по стека на повикванията. Стойностите, изчислени в горната част на стека на обажданията, са досадни, трудни и обикновени грозни за вашите обаждащи се, ако трябва да ги добавите към всяко обаждане на функция между горната и долната част, за да разпространявате нещо просто като потребителски идентификатор или токен за автентичност. Представете си, ако трябваше да добавите друг параметър, наречен „потребителски идентификатор“, на десетки функции между две повиквания в два различни пакета, само за да уведомите пакет Z за кой пакет А разбра? API ще изглежда грозно и хората ще викат към вас, че го проектирате. ДОБРЕ! Само защото сте приели тази грозота и сте я затъмнили в Context.Value не прави вашия API или дизайн по-добър. Несигурността е обратното на добрия дизайн на API.

Context.Value трябва да информира, а не да контролира

Информирайте, а не контролирайте. Това е основната мантра, която според мен трябва да ръководи, ако използвате контекста. Съдържанието на context.Value е за поддръжници, а не потребители. Никога не трябва да се изисква въвеждане на документирани или очаквани резултати.

За да поясним, ако функцията ви не може да се държи правилно поради стойност, която може да е или не може да бъде вътре в контекста. Валя, тогава вашият API затъмнява необходимите входове твърде силно. Освен документацията, съществува и очакваното поведение на вашето приложение. Ако функцията, например, се държи като документирано, но начинът, по който приложението ви използва функцията, има практическо поведение да се нуждаете от нещо в Context, за да се държи правилно, тогава той се приближава до влияние върху контрола на вашата програма

Един пример за информиране е идентификационен номер на заявка. Обикновено те се използват в логване или други системи за агрегиране, за да групират заявки заедно. Действителното съдържание на ID на заявката никога не променя резултата от оператор if и ако идентификационният номер на заявката липсва, не прави нищо, за да промени резултата от функция.

Друг пример, който отговаря на дефиницията на inform, е дърводобив. Наличието или липсата на дърводобив никога не променя потока на програма. Освен това, това, което е или не е регистрирано, обикновено не се документира или не се разчита на поведението в повечето приложения. Ако обаче съществуването на регистрация или съдържанието на дневника са документирани в API, тогава дървородателят се е преместил от информацията към контрола.

Друг пример за информиране е IP адресът на входящата заявка, ако единствената цел на този IP адрес е да украсява лог съобщения с IP адреса на потребителя. Ако обаче документацията или очакваното поведение на вашата библиотека е, че някои IP адреси са по-важни и е по-малко вероятно да бъдат заглушени, то IP адресът се е преместил от инфо за контрол, защото сега се изисква въвеждане или поне въвеждане, което променя поведението.

Връзката с база данни е пример за най-лошия случай на обект, който трябва да се постави в контекст. Оценка, защото очевидно контролира програмата и се изисква вход за вашите функции.

Публикуването в блога golang.org в контекста.Контекст е потенциално противоположен пример за това как правилно да използвате контекста.Value. Нека разгледаме кода за търсене, публикуван в блога.

func Search (ctx context.Context, query string) (Резултати, грешка) {
 // Подгответе заявката за API за търсене на Google.
 // ...
 // ...
 q: = req.URL.Query ()
 q.Set ("q", заявка)
// Ако ctx носи IP адреса на потребителя, препратете го на сървъра.
 // API на Google използват потребителския IP за разграничаване на инициирани от сървъра заявки
 // от заявки на крайния потребител.
 ако userIP, ok: = userip.FromContext (ctx); Добре {
   q.Set (“userip”, userIP.String ())
 }

Основната измервателна пръчка е знанието как съществуването на потребителски потребител на заявката променя резултата от заявка. Ако IP се отличава в система за проследяване на лога, така че хората да могат да отстраняват грешки в целевия сървър, тогава той чисто информира и е наред. Ако потребителският потребител, който е вътре в заявка, промени поведението на REST обаждането или има тенденция да го направи по-малко вероятно да бъде заглушен, тогава той започва да контролира вероятния изход на Търсене и вече не е подходящ за Context.Value.

В публикацията в блога също се споменават маркери за оторизация като нещо, което се съхранява в контекста.Value. Това ясно нарушава правилата за подходящо съдържание в Context.Value, защото той контролира поведението на функцията и се изисква вход за потока на вашата програма. Вместо това е по-добре да направите маркерите явен параметър или член на структура.

Дали Context.Value дори принадлежи?

Контекстът прави две много различни неща: едното от тях прекъсва продължителните операции, а другото носи стойности за обхват на заявката. Интерфейсите в Go трябва да описват поведенията, които API иска. Те не трябва да се хващат с торбички от функции, които често се срещат заедно. Жалко е, че съм принуден да включа поведение относно добавянето на произволни стойности към обект, когато единственото, което ме интересува, е да определям времето за бягства заявки.

Алтернативи на Context.Value

Хората често използват Context.Value в по-широка абстракция на среден софтуер. Тук ще покажа как да остана вътре в този вид абстракция, докато все още няма нужда да злоупотребявате с Context.Value. Да покажем някакъв примерен код, който използва HTTP среден софтуер и Context.Value за разпространение на потребителски идентификатор, намерен в началото на междинен софтуер. Забележка Go 1.7 включва контекст на обекта http.Request. Също така, аз съм малко свободен със синтаксиса, но се надявам значението да е ясно.

Това е пример за това, как Context.Value често се използва в веригите за междинен софтуер за настройка на разпространение на потребителски идентификатор. Първият междинен софтуер, addUserID, актуализира контекста. След това се обажда на следващия манипулатор във веригата на междинен софтуер. По-късно стойността на потребителския идентификатор в контекста се извлича и използва. В големи приложения можете да си представите, че тези две функции са много далеч една от друга.

Нека сега покажем как с една и съща абстракция можем да направим едно и също, но не е необходимо да злоупотребяваме с Context.Value.

В този пример все още можем да използваме същите абстракции на междинен софтуер и все още имаме само основната функция, която знаем за веригата на междинен софтуер, но използваме UserID по сигурен начин. Променливата chainPartOne е верига за междинен софтуер, когато извличаме UserID. След това тази част от веригата може да създаде следващата част от веригата, chainWithAuth, използвайки директно UserID.

В този пример можем да задържим Context до просто прекратяване на ранните дълги функции. Също така ясно документирахме, че на структурата UseUserID се нуждае от UserID, за да се държи правилно. Това ясно разделяне означава, че когато по-късно хората префабрикуват този код или се опитват да използват повторно UseUserID, те знаят какво да очакват.

Защо изключението за поддръжниците

Признавам, че създавам изключение за поддръжниците в Context.Value е донякъде произволен. Личното ми разсъждение е да си представя идеално проектирана система. В тази система няма да има нужда от интроспекция на приложение, няма нужда от журнали за отстраняване на грешки и малка нужда от показатели. Системата е перфектна, така че не съществуват проблеми с поддръжката. За съжаление, реалността е такава, че трябва да отстраняваме грешки в системите. Поставянето на тази информация за отстраняване на грешки в Context обект е компромис между перфектния API, който никога няма да се нуждае от поддръжка, и реалността на желанието да се предава информация за отстраняване на грешки в API. Аз обаче не бих спорил особено с някой, който иска да направи дори изясняването на информацията за отстраняване на грешки в своя API.

Опитайте се да не използвате контекста.Value

Можете да се сблъскате с повече проблеми, отколкото си струва да опитате да използвате контекста. Валя. Съчувствам колко лесно е просто да добавите нещо в контекста. Проверете и го извлечете по-късно в някаква далечна абстракция, но лекотата на използване сега се плаща от болката при рефакторинг по-късно. Почти никога не е необходимо да го използвате и ако го направите, това прави префакторингът на кода ви по-късно много труден, защото става неизвестно (особено от компилатора) какви входове са необходими за функциите. Това е много спорно допълнение към Context и може лесно да се сблъска с повече проблеми, отколкото си струва.