Jak správně používat context.Context v Go 1.7

Tento příspěvek bude hovořit o nové knihovně v Go 1.7, kontextové knihovně a kdy a jak ji správně používat. Požadované čtení ke spuštění je úvodní příspěvek, který hovoří o knihovně a obecně o tom, jak se používá. Dokumentaci pro kontextovou knihovnu si můžete přečíst na tip.golang.org.

Jak integrovat Context do vašeho API

Nejdůležitější věcí, kterou byste měli pamatovat při integraci kontextu do vašeho API, je to, že je zamýšleno, aby bylo dosaženo rozsahu. Například by mělo smysl existovat podél jediného databázového dotazu, ale nemělo by smysl existovat podél databázového objektu.

V současné době existují dva způsoby, jak integrovat objekty Context do vašeho rozhraní API:

  • První parametr volání funkce
  • Nepovinná konfigurace na struktuře požadavků

Příklad prvního naleznete na stránce Dialer.DialContext na síťovém balíčku. Tato funkce provádí normální operaci vytáčení, ale ruší ji podle objektu Context.

func (d * Dialer) DialContext (ctx context.Context, network, řetězec adresy) (Conn, chyba)

Příklad druhého způsobu integrace kontextu naleznete v balíčku net / http Request.WithContext

func (r * Request) WithContext (ctx context.Context) * Request

Tím se vytvoří nový objekt požadavku, který končí podle daného kontextu.

Kontext by měl procházet vaším programem

Skvělým mentálním modelem používání kontextu je, že by měl procházet vaším programem. Představte si řeku nebo tekoucí vodu. To obecně znamená, že je nechcete ukládat někde jako ve struktuře. Nechcete to ani držet, víc, než je nezbytně nutné. Kontext by měl být rozhraním, které je předáváno z funkce, aby fungovalo na vašem zásobníku volání, podle potřeby rozšířené. V ideálním případě je kontextový objekt vytvořen s každou žádostí a vyprší, když žádost skončí.

Výjimkou z neukládání kontextu je situace, kdy jej musíte vložit do struktury, která se používá čistě jako zpráva předaná kanálem. To je uvedeno v příkladu níže.

V tomto příkladu porušujeme toto obecné pravidlo neukládat kontext vložením do zprávy. Toto je však vhodné použití kontextu, protože stále protéká programem, ale spíše podél kanálu, než trasování zásobníku. Zde si také všimněte, jak se kontext používá na čtyřech místech:

  • Časový limit q v případě, že je procesor příliš plný
  • Chcete-li q vědět, zda by měla zpracovat zprávu
  • Časový limit q odeslání zprávy zpět na newRequest ()
  • Časový limit newRequest () čeká na odpověď zpět z ProcessMessage

Všechny blokovací / dlouhé operace by měly být proveditelné

Když zrušíte možnost uživatelů zrušit dlouhodobě provozované operace, spojíte goroutinu déle, než chce uživatel. Když se Context přesune do standardní knihovny s Go 1.7, snadno se stane standardní abstrakcí pro vypršení časového limitu nebo ukončení časně dlouhých provozních operací. Pokud píšete knihovnu a vaše funkce mohou blokovat, je to perfektní případ použití pro kontext.

Ve výše uvedeném příkladu je ProcessMessage rychlou operací, která neblokuje, takže kontext je zjevně nadměrný. Pokud se však jednalo o mnohem delší operaci, pak použití Context volajícím umožňuje, aby newRequest pokračoval, pokud výpočet trvá příliš dlouho.

Hodnoty Context.Value a scoped scoped (upozornění)

Nejspornější částí kontextu je hodnota, která umožňuje libovolné hodnoty umístěné do kontextu. Zamýšlené použití pro Context.Value, z původního blogového příspěvku, jsou hodnoty s rozsahem požadavků. Hodnota s rozsahem požadavku je hodnota odvozená z dat v příchozím požadavku a po ukončení požadavku zmizí. Jako požadavek skáče mezi službami, tato data jsou často udržována mezi RPC hovory. Pokusme se nejprve objasnit, co je nebo není hodnota s rozsahem požadavku.

Zřejmými údaji s rozsahem žádosti by mohlo být to, kdo žádost podá (ID uživatele), jak ji podávají (interní nebo externí), odkud ji podávají (IP uživatele), a jak důležitý by měl být tento požadavek.

Připojení k databázi není hodnota s rozsahem požadavku, protože je globální pro celý server. Na druhé straně, pokud se jedná o připojení, které má metadata o aktuálním uživateli, aby automaticky vyplnil pole, jako je ID uživatele nebo provedl ověření, může být považováno za požadavek s rozsahem.

Logger není hodnocen podle požadavku, pokud je umístěn na objektu serveru nebo je singletonem balíčku. Pokud však obsahuje metadata o tom, kdo požadavek odeslal, a pokud je v požadavku povoleno protokolování ladění, stane se žádost o rozsah.

Data s rozsahem žádosti však bohužel mohou zahrnovat velké množství informací, protože v určitém smyslu všechna zajímavá data v aplikaci pocházejí z požadavku. Tím je dána široká definice toho, co by mohlo být zahrnuto do Context.Value, což usnadňuje zneužití. Já osobně mám užší pohled na to, co je v Context.Value vhodné a pokusím se vysvětlit svou pozici ve zbývající části tohoto příspěvku.

Context.Value zakrývá tok vašeho programu

Skutečným důvodem, proč je na správné používání Context.Value tolik omezení, je to, že zakrývá očekávaný vstup a výstup funkce nebo knihovny. Mnoho lidí nenávidí stejné důvody, proč lidé nenávidí singletony. Parametry funkce jsou jasnou, soběstačnou dokumentací toho, co je nutné k tomu, aby se funkce chovala. To usnadňuje testování funkce a důvody pro intuitivní i pozdější refaktor. Zvažte například následující funkci, která provádí ověřování z kontextu.

func IsAdminUser (ctx context.Context) bool {
  x: = token.GetToken (ctx)
  userObject: = auth.AuthenticateToken (x)
  návrat userObject.IsAdmin () || userObject.IsRoot ()
}

Když uživatelé volají tuto funkci, vidí pouze, že to vyžaduje kontext. Požadované části k poznání, zda je uživatel správcem, jsou však jasně dvě věci: ověřovací služba (v tomto případě používaná jako singleton) a ověřovací token. Můžete to reprezentovat jako vstupy a výstupy jako níže.

IsAdminUser tok

Pojďme jasně reprezentovat tento tok pomocí funkce a odstranit všechny singletony a kontexty.

func IsAdminUser (tokenový řetězec, authService AuthService) int {
  userObject: = authService.AuthenticateToken (token)
  návrat userObject.IsAdmin () || userObject.IsRoot ()
}

Tato definice funkce je nyní jasným vyjádřením toho, co je třeba vědět, pokud je uživatel správcem. Toto znázornění je také zřejmé uživateli funkce a činí srozumitelnější refaktoring a opětovné použití funkce.

Context.Value a reality velkých systémů

Silně se vcítím do touhy strčit položky v Context.Value. Složité systémy mají často vrstvy middlewaru a více abstrakcí podél zásobníku volání. Hodnoty vypočtené v horní části zásobníku hovorů jsou pro vaše volající únavné, obtížné a zjevně ošklivé, pokud je musíte přidat ke každému volání funkce mezi horním a dolním, aby se šířila jen jednoduchá akce, jako je ID uživatele nebo autorizační token. Představte si, že byste museli přidat další parametr nazvaný „ID uživatele“ k desítkám funkcí mezi dvěma voláními ve dvou různých balíčcích, jen abyste dali balíčku Z vědět o tom, který balíček A zjistil? Rozhraní API by vypadalo ošklivě a lidé by na vás křičeli, že to navrhnete. DOBRÝ! Jen proto, že jste vzal tuto ošklivost a zakryl ji uvnitř Context.Value nedělá vaše API nebo design lepší. Obskurnost je opakem dobrého návrhu API.

Context.Value by měla informovat, nikoli ovládat

Informovat, ne ovládat. Toto je primární mantra, o které se domnívám, že by vás měl vést, pokud správně používáte kontext. Obsah context.Value je určen pro správce, nikoli pro uživatele. Nikdy by to nemělo být vyžadováno jako vstup pro zdokumentované nebo očekávané výsledky.

Chcete-li objasnit, zda se vaše funkce nemůže chovat správně z důvodu hodnoty, která může nebo nemusí být uvnitř kontextu. Hodnoty, vaše API příliš zakrývá požadované vstupy. Kromě dokumentace existuje také očekávané chování vaší aplikace. Pokud se funkce například chová jako dokumentovaná, ale způsob, jakým aplikace tuto funkci používá, má praktické chování, když potřebuje něco v kontextu, aby se choval správně, pak se přiblíží k ovlivnění kontroly nad programem.

Jedním příkladem informování je ID požadavku. Obecně se používají v protokolovacích nebo jiných agregačních systémech k seskupování požadavků. Skutečný obsah ID požadavku nikdy nezmění výsledek příkazu if a pokud ID požadavku chybí, nic nezmění výsledek funkce.

Dalším příkladem, který odpovídá definici inform je logger. Přítomnost nebo nedostatek loggeru nikdy nemění tok programu. Také to, co je nebo není přihlášeno, není při většině použití obvykle zdokumentováno ani spoléhá na chování. Pokud je však existence API nebo obsah protokolu zdokumentována v API, pak se logger přesunul z informovat do řízení.

Dalším příkladem informování je IP adresa příchozí žádosti, pokud jediným účelem této IP adresy je vyzdobit logovací zprávy IP adresou uživatele. Pokud však dokumentace nebo očekávané chování vaší knihovny znamená, že některé adresy IP jsou důležitější a méně pravděpodobné, že budou omezeny, pak se adresa IP přesunula z informovat do řízení, protože je nyní vyžadován vstup nebo alespoň vstup, který mění chování.

Připojení k databázi je nejhorším příkladem objektu, který se má umístit do kontextu. Hodnota, protože zjevně řídí program a je vyžadován vstup pro vaše funkce.

Blogový příspěvek golang.org na context.Context je potenciálním příkladem toho, jak správně používat context.Value. Podívejme se na vyhledávací kód zveřejněný v blogu.

func Search (ctx context.Context, řetězec dotazu) (Výsledky, chyba) {
 // Připravte si požadavek Google Search API.
 // ...
 // ...
 q: = req.URL.Query ()
 q.Set („q“, dotaz)
// Pokud ctx přenáší IP adresu uživatele, předejte ji na server.
 // Google API používají IP uživatele k rozlišení požadavků iniciovaných serverem
 // z požadavků koncového uživatele.
 pokud userIP, ok: = userip.FromContext (ctx); OK {
   q.Set („userip“, userIP.String ())
 }

Primární měřící tyčka ví, jak existence userIP v dotazu mění výsledek požadavku. Pokud je IP v systému sledování protokolu rozlišována tak, aby lidé mohli ladit cílový server, pak to čistě informuje a je v pořádku. Pokud uživatelIP, který je uvnitř požadavku, změní chování volání REST nebo má sklon snižovat pravděpodobnost jeho omezení, začne ovládat pravděpodobný výstup vyhledávání a již není vhodný pro Context.Value.

Blogový příspěvek také zmiňuje autorizační tokeny jako něco, co je uloženo v kontextu. To jasně porušuje pravidla vhodného obsahu v Context.Value, protože řídí chování funkce a je vyžadován vstup pro tok vašeho programu. Místo toho je lepší vytvořit tokeny explicitním parametrem nebo členem struktury.

Patří Context.Value dokonce?

Kontext dělá dvě velmi odlišné věci: jedna z nich vyprší dlouhodobé operace a druhá nese požadované hodnoty. Rozhraní v Go by měla být o popisu chování API chce. Neměly by se jednat o sáčky funkcí, které se často vyskytují společně. Je nešťastné, že jsem nucen zahrnout chování týkající se přidávání libovolných hodnot k objektu, když vše, co mě zajímá, je vypršení časového limitu požadavků na útěk.

Alternativy k Context.Value

Lidé často používají Context.Value v širší abstrakci middlewaru. Tady vám ukážu, jak zůstat uvnitř tohoto druhu abstrakce, aniž by bylo nutné zneužívat Context.Value. Ukážeme si příklad kódu, který používá HTTP middlewares a Context.Value k propagaci ID uživatele nalezeného na začátku middlewaru. Poznámka: Go 1.7 obsahuje kontext na objektu http.Request. Také jsem trochu volná se syntaxí, ale doufám, že význam je jasný.

Toto je příklad toho, jak se Context.Value často používá v řetězcích middlewaru k nastavení propagace ID uživatele. První middleware, addUserID, aktualizuje kontext. Potom zavolá další obslužný program v řetězci middlewaru. Později je hodnota ID uživatele uvnitř kontextu extrahována a použita. Ve velkých aplikacích si dokážete představit, že tyto dvě funkce jsou velmi daleko od sebe.

Nyní ukážeme, jak můžeme pomocí stejné abstrakce udělat totéž, ale nemusíme zneužívat Context.Value.

V tomto příkladu můžeme stále používat stejná abstrakce middlewaru a stále máme pouze hlavní funkci vědět o řetězci middlewaru, ale UserID používejte typově bezpečným způsobem. Proměnná chainPartOne je řetězec middlewaru až do okamžiku, kdy extrahujeme UserID. Tato část řetězce pak může vytvořit další část řetězce, chainWithAuth, pomocí přímého UserID.

V tomto příkladu můžeme udržet kontext pouze na ukončení časně dlouhých provozních funkcí. Také jsme jasně dokumentovali, že struct UseUserID potřebuje UserID, aby se choval správně. Toto jasné oddělení znamená, že když lidé později upraví tento kód nebo se pokusí znovu použít UseUserID, vědí, co mohou očekávat.

Proč výjimka pro správce

Přiznávám, že v Context.Vyjímám výjimku pro správce. Hodnota je poněkud libovolná. Mým osobním důvodem je představit si dokonale navržený systém. V tomto systému by nebylo nutné provádět introspekci aplikace, žádné protokoly ladění a malá potřeba metrik. Systém je dokonalý, takže problémy s údržbou neexistují. Realitou bohužel je, že musíme ladit systémy. Vložení této informace o ladění do objektu Context je kompromisem mezi dokonalým rozhraním API, které by nikdy nevyžadovalo údržbu, a skutečností, že si přejete navléknout informace o ladění přes rozhraní API. Zvláště bych se však nehádal s někým, kdo chce ve svém API explicitně uvést i ladicí informace.

Zkuste nepoužít kontext

Můžete se dostat na mnohem větší potíže, než kolik stojí za to se pokusit použít kontext. Zdůrazňuji, jak snadné je něco přidat do kontextu. Hodnotu a načíst ji později v nějaké vzdálené abstrakci, ale snadnost použití je nyní placena bolestí při pozdější refaktorizaci. Téměř nikdy není potřeba jej používat, a pokud ano, je pozdější obtížné refaktorizovat váš kód, protože se stává neznámým (zejména kompilátorem), jaké vstupy jsou potřebné pro funkce. Je to velmi sporný doplněk k kontextu a může snadno dostat jednoho do více problémů, než to stojí za to.