Byla vydána nová verze 0.4.15 (𝕏) svobodného operačního systému ReactOS (Wikipedie), jehož cílem je kompletní binární kompatibilita s aplikacemi a ovladači pro Windows. Přehled novinek i s náhledy v oznámení o vydání.
Byl představen rpi-image-gen, tj. oficiální nástroj pro vytváření vlastních softwarových obrazů pro zařízení Raspberry Pi.
Byla vydána nová major verze 8.0, aktuálně 8.0.1, softwaru pro správu elektronických knih Calibre (Wikipedie). Přehled novinek v poznámkách k vydání. Vypíchnuta je lepší podpora Kobo KEPUB formátu nebo integrovaný lokálně běžící engine Piper pro převod textu na řeč používaný pro čtení nahlas (již od verze 7.18).
Společnost OpenAI rozšířila své API o nové audio modely. Nový model pro převod textu na řeč (text-to-speech model) lze bez přihlašování vyzkoušet na stránce OpenAI.fm.
Příspěvek Bezpečnost paměti pro webové fonty na blogu Chrome pro vývojáře rozebírá, proč se pro zpracování webových fontů v Chrome místo FreeType nově používá v Rustu napsaná Skrifa z Fontations.
V pátek 21. a v sobotu 22. března proběhnou Arduino Days 2025, tj. každoroční „narozeninová oslava“ platformy Arduino. Na programu je řada zajímavých přednášek. Sledovat je bude možné na YouTube. Zúčastnit se lze i lokálních akcí. V sobotu v Praze na Matfyzu.
Komunitná konferencia Bratislava OpenCamp, ktorá sa uskutoční už o tri týždne 5. 4. 2025 na FIIT STU pozná svoj program – návštevníkom ponúkne 3 paralelné behy prednášok a workshopov na rôzne témy týkajúce sa otvoreného softvéru či otvorených technológií.
Časopis MagPi od nakladatelství Raspberry Pi se s číslem 151 přejmenoval na Raspberry Pi Official Magazine. I pod novým názvem zůstává nadále ve formátu pdf zdarma ke čtení.
Japonská SoftBank Group kupuje firmu Ampere Computing za 6,5 miliardy dolarů. Ampere Computing vyrábí 32-128jádrové procesory Ampere Altra a 192jádrové procesory AmpereOne.
Byla vydána (𝕏) nová verze 2025.1a linuxové distribuce navržené pro digitální forenzní analýzu a penetrační testování Kali Linux (Wikipedie). Přehled novinek v oficiálním oznámení na blogu.
Jazyk používá hodnoty a ukazatele velmi podobně jako jazyk C a naopak velmi odlišně než např. Java. Mírně rozvinutý příklad z tutoriálu:
type T struct { a, b int } // klíčové slovo var uvádí seznam deklarací a jejich typ. // deklarace může být následována operátorem přiřazení '=' a inicializátorem var t *T = new(T) // new(T) je v prvním přiblížení shodné s calloc(1, sizeof(T)) var u = new(T) // dtto, typ 'u' je v tomto případě zděděn z jeho inicializátoru // nebo více idiomaticky // ':=' je deklarace proměnné (se zděděním typu), // proto pouze současně s inicializátorem t := new(T) u := t // 'u' nyní ukazuje na stejnou instanci T jako ukazatelová proměnná 't' v := *t // 'v' nyní obsahuje kopii obsahu na který odkazuje 't' w := &v // 'w' nyní ukazuje na 'v' i := (*t).a // deklarujeme a inicializujeme 'i' typu 'int' // s explicitní dereferencí 't' // totéž idiomaticky i := t.a
Uživatelské referenční typy (jako java.lang.Object a všichni jeho potomci) ve smyslu možnosti jejich přímé deklarace neexistují. Programátorem deklarované typy v Go jsou vždy buď hodnotou („v“ výše) nebo ukazatelem („t“, „u“ a „w“ výše). Operátor získání adresy (&) a dereference (*) je nutné v Go psát a je tedy vždy explicitně uvedeno, jakého typu (hodnota vs ukazatel) je výraz s jedním z těchto operátorů. Jako výjimka z tohoto pravidla vypadá poslední řádek kódu výše. V tomto specifickém případě jazyk dovoluje programátorovi nebýt explicitní (a ušetřit pár úhozů do klávesnice), ale jenom proto, že samotné 't.a' nemá vlastně žádný sémantický obsah. 't' je ukazatel, nikoli struktura a selektor '.' nemá co „selektovat“. Ve všech ostatních případech, kdy by takováto „pomoc“ kompilátoru/jazyka mohla vést k nejednoznačnému výkladu, se vždy a všude v Go vyžaduje explicitní zápis/použití adresních operátorů.
Přesto referenční typy v Go vlastně existují, ale pouze v případě některých předdefinovaných, potenciálně generických objektů (mapa, řetězec, řez, kanál, …). Uživateli jazyka se tyto entity jeví jako hodnoty (tj. lze na ně například i normálně odkazovat ukazatelem), mají ale vždy referenční sémantiku. Pro příklad: pokud je m proměnná typu map[tkey]tvalue (golang.org/doc/go_spec.html#Map_types), pak po a = m jsou modifikace mapy ve stylu a[key] = value viditelné i přes hodnotu m, tj. m[key] nyní vrací value. Možná je/bude referenční sémantika ještě lépe vidět na tomto příkladu:
var a = []int{1, 2, 3} b := a b[0] = 9 println(a[0], a[1], a[2]) // vypíše '9 2 3'
Ukazatel v Go smí být nil (hodnota nil se typicky používá jako sentinel). Nil je pro přiřazení a porovnání typově kompatibilní s ukazatelem na jakýkoli typ (a pro pohodlí programátora i s některými zabudovanými a/nebo „referenčními“ typy: mapa, řez, rozhraní, funkce, kanál).
Aritmetika ukazatelů v Go neexistuje. Tento nástroj je v C mocným prostředkem pro některé oblasti systémového programování, současně ale potenciálně dosti nebezpečnou hračkou. Vůbec nejde jen o začínající nebo lajdácké programátory, záludné chyby v aritmetice ukazatelů v C se nevyhýbají žádnému programu napsanému lidskou bytostí. Go je typově a paměťově bezpečný jazyk a to se s aritmetikou ukazatelů nedá snadno, tedy spíše v podstatě asi vůbec nijak, skloubit dohromady.
Předdefinovaný typ string je podložen polem bajtů. Jazyk i standardní knihovna podporuje řetězce kódované v UTF-8 a jejich převody na pole Unicode codepointů (zjednodušeně celé číslo v rozsahu 0 až 0x10FFFF, které může ale vždy nemusí být kódem nějakého znaku) a zpět. Řetězec je, podobně jako třeba v Javě, imutabilní (neměnný), ale ve všem ostatním se chová jako []byte, tj. řez bajtů. Je to ovšem samostatný typ, který není pro přiřazení kompatibilní s []byte.
Řezy (slices) jsou značně podobné analogickým objektům např. v Pythonu. Jednou větou – řez je „pohled“ na nějaké pole s přidanou informací o začátku a délce řezu. Opět připomeňme referenční sémantiku řezů. Všechny různé řezy jednoho pole přistupují stále k tomu jednomu stejnému poli. Kde je tato vlastnost na překážku tam je nutné obsah řezu zkopírovat, třeba pomocí vestavěných funkcí append a copy.
// Použití append: s0 := []int{0, 0} s1 := append(s0, 2) // append a single element s1 == []int{0, 0, 2} s2 := append(s1, 3, 5, 7) // append multiple elements s2 == []int{0, 0, 2, 3, 5, 7} s3 := append(s2, s0...) // append a slice s3 == []int{0, 0, 2, 3, 5, 7, 0, 0} // Použití copy: var a = [...]int{0, 1, 2, 3, 4, 5, 6, 7} var s = make([]int, 6) var b = make([]byte, 5) n1 := copy(s, a[0:]) // n1 == 6, s == []int{0, 1, 2, 3, 4, 5} n2 := copy(s, s[2:]) // n2 == 4, s == []int{2, 3, 4, 5, 4, 5} n3 := copy(b, "Hello, World!") // n3 == 5, b == []byte("Hello")
Typ array je staré dobré pole se statickým typem, který obsahuje jeho délku a typ položky pole. Dynamická pole v Go neexistují, jejich funkčnost je nahrazena pomocí řezů. Pole/řez je v Go vždy jednorozměrné/ý, ale pochopitelně položkou pole/řezu může být i jiné pole nebo řez. Tento mechanismus vytváření vícerozměrných polí Go sdílí například s Javou. Podstatným rozdílem je ovšem to, že v Javě typ pole neobsahuje jeho délku, pole v Javě jsou tedy víc podobná Go řezům.
Složené typy jsou velmi podobné jako v jazyce C a rozložení v paměti je úmyslně implementováno s ohledem na co největší kompatibilitu s C. Na rozdíl od C ale ve složených typech Go nenajdeme nic podobného jako „union“, tedy strukturu jejíž členové se mohou překrývat. Úplně stejně jako v případě aritmetiky ukazatelů je toto případ šikovné a mocné jazykové konstrukce, která je ale ve své standardní implementaci (entity různých typů sdílejí stejnou adresu v paměti) prakticky neslučitelná s žádným paměťově a typově bezpečným jazykem.
Složené typy se velmi často používají v objektovém modelu Go (o pár kapitol níže), zde je jen nutno stručně podotknout, že strukturované typy lze zapouzdřit do dalších složených typů (jako v C), nejde přitom ale o mechanismus/sémantiku dědičnosti (jako v Javě).
// Jednoduchý složený typ type T struct { name string // jméno objektu value int // jeho hodnota } // Metoda T, její volání je např. s := instance_typu_T.String() func (t *T) String() string { return fmt.Sprintf("%s, %d", t.name, t.value) } // Schema základní kompozice typů type U struct { pair T next *U // další položka seznamu } // Schema kompozice s dědičností metod type V struct { T // anonymní pole typu T, metody T jsou přístupné také z instance V next *V } func (v *V) Swap(t T) (result T) { v.T, result = t, v.T // na anonymní pole se odkazujeme jménem jeho typu return }
Podrobněji ve specifikaci.
Modul (v Go se nazývá package) se sestává z libovolného množství zdrojových souborů v rámci jednoho adresáře. Ve smyslu viditelnosti entit deklarovaných uvnitř modulu jsou hranice mezi soubory irelevantní, naopak externí entity jsou viditelné jen v rámci souboru, který je explicitně importuje, nikoli v celém modulu. Toto je příklad nenápadných pravidel, která mohou působit jako intuitivně nebo dokonce náhodně volená. Takový byl zhruba můj osobní dojem v době, kdy jsem se s nimi seznamoval. Jakmile ale Go projekt na kterém pracujete, začne mít desítky modulů a každý modul třeba i desítku zdrojových souborů, mohou vám snadno začít stejná pravidla připadat jako moc dobrá pomoc pro kodéra a najednou vidíte, že všechny ostatní možnosti by vedly k řadě potíží, jež se nás naštěstí díky autorům této části specifikace netýkají.
Modul exportuje explicitně proměnné, typy, funkce/metody, konstanty.., které mají být viditelné zvenku poněkud neobvyklým, ale implementačně a v čitelnosti jednoduchým a účinným způsobem: všechny exportované entity mají identifikátory začínající velkým písmenem (Unicode kategorie Lu). V dobře psaném zdrojovém Go kódu jsou tedy zřetelně (bezkontextově) odlišitelné externí entity bez nutnosti konzultovat import klauzule a jimi odkazované moduly.
Na straně uživatele modulu jsou všechny odkazy na externí entity standardně vždy s prefixem jmenného prostoru, např. name.Identifier, kde name je implicitně jméno package (import "fmt"; fmt.Printf("X")), ale lze použít alias (import alias "fmt"; alias.Printf("X")) nebo speciální alias, se kterým sice není prefix nadále potřeba, ale může docházet ke kolizím při vícero importovaných modulech (import . "fmt"; Printf("X")).
Moduly mohou obsahovat libovolné množství inicializačních sekcí (func init()), takže inicializaci lze v rámci modulu jak centralizovat, tak inicializovat entitu v místě, kde je deklarovaná nebo spolu s jejími metodami apod. Co se víc hodí bude nejvíce záležet na konkrétním problému, který modul řeší, ale možnost volby je v tomto případě příjemná.
Cyklické závislosti mezi moduly nejsou podporovány. K překrývání importovaných identifikátorů nedochází (s výjimkou importu s aliasem "."). Inicializační funkce modulů jsou při inicializaci aplikace volány v korektním pořadí dle jejich vzájemné závislosti.
Pro správu paměti používá Go garbage collector (GC). Explicitní uvolnění objektů z paměti není možné. K objektu lze registrovat funkci, která se později automaticky zavolá před jeho uvolněním pomocí GC. Problematika automatické správy paměti je samostatné téma na mnoho článků, ba i vědeckých prací, zde se jí proto nijak podrobně zabývat nebudeme.
GC v Go přináší stejné výhody, ale i pochopitelně i shodné nevýhody jako v cca každém jiném GC jazyku. Pocit, že na uvolňování paměti lze z hlediska programátora zapomenout, je oprávněný jen z velmi malé části. Nedobře napsaný kód v libovolném GC jazyce generuje pro GC zbytečně mnoho práce (třeba alokace v cyklu, kterou ale přitom lze vytknout před něj). Za to se platí zpomalením/zpomalováním/přerušováním běhu aplikace a zvýšenými nároky na zdroje systému. Dva funkčně shodné programy v GC jazyce se mohou lišit až příliš snadno na škále od perfektně vyhovující aplikace až po program, který pro soustavný trashing není použitelný vůbec k ničemu, tedy pokud nepočítáme účinnou „výrobu“ frustrace jeho nešťastného uživatele.
Paralelismus je v Go integrální součást jazyka. Klíčovým slovem go se spustí paralelní vyhodnocení libovolného výrazu, kterým je samozřejmě i volání libovolné funkce nebo funkčního literálu (s případnou uzávěrou).
Paralelismus je kooperativní (jde o koprogramy) s význačnou výhodou, že jednotlivé koprogramy v Go nejsou při provádění svázány pevně s vláknem, ve kterém byly vytvořeny. Pro tuto vlastnost se v Go koprogramy nazývají gorutiny.
Gorutiny se liší od vláken dále významně i tím, že jsou velmi lehké. Jejich vytvoření a aktivace, rozhodování, zda je nutné gorutinu pozdržet, a přepínání kontextu probíhá v user landu (tj. v neprivilegovaném režimu procesoru, na x86 je to ring 3). Protože se předpokládá, že některé aplikace budou chtít někdy vytvářet enormní množství gorutin (řekněme silně zatížený databázový nebo webový server), jsou gorutiny implementovány se segmentovaným zásobníkem. Segment má například v implementaci pro x86_64 velikost 4 KB a zásobník se zvětšuje o další segment(y) až dle potřeby. Vytvořit například 100 000 gorutin může za příznivých okolností vyžadovat „jen“ 400 MB RAM, v případě stejného množství vláken nejspíše lze očekávat problémy nejen s požadavky na paměť.
Například zde se uvádí, že pro typické desktop PC bude v POSIX vláknech (pthreads) implicitní velikost zásobníku přidělená vláknu mít velikost 2 MB. Pro výše uvedený počet gorutin bychom tedy (s)potřebovali 200GB paměti. Je možné při vytváření pthread vlákna chtít též jen podobnou velikost jako 4 KB v Go. Pokud ale některá vlákna budou potřebovat zásobník větší velikosti, tak dle konkrétní implementace pthreads nám systém může vyhovět jeho zvětšením. Protože ale zásobník pthread vlákna je obvykle paměťově souvislý (v Go nikoli), má OS s rozšiřováním velkého počtu zásobníků – i když bude chtít zvětšovat jen některé z nich, potíže při přidělování nových virtuálních adres. Pthread zásobník vlákna může (kvůli zachování platnosti dříve vytvořených ukazatelů na položky v zásobníku) růst jen směrem dolů, adresu jeho „horního konce“ už není možné dynamicky měnit. A připomeňme ještě, že standardní implementace pthreads přepíná mezi vlákny v privilegovaném režimu procesoru (kernel mode) a to je z hlediska času řádově náročnější než jinak podobné přepnutí, které používají gorutiny.
Pro komunikaci mezi gorutinami se velmi doporučuje místo sdílených dat, které vyžadují explicitní synchronizaci (např. mutex), raději používat kanály. Kanály jsou staticky typované, s vyrovnávací frontou nebo bez (tj. asynchronní nebo synchronní).
Zasílání a čtení zpráv z kanálů je samo o sobě z hlediska paralelismu bezpečné. Při dodržení konvence, při které gorutina odeslanou zprávou nijak dále nepoužívá, nejlépe tak, že se zbaví jakékoli reference na ni a naopak gorutina na straně příjmu modifikuje jen přijaté zprávy a lokální entity, pak pro korektní paralelní zpracování problému není nutné udělat nic dalšího a je to z hlediska programátora velmi pohodlné; snižuje se počet chyb, kterých se může dopustit. Pochopitelně ale toto samo o sobě nemůže zabránit například dead locku a/nebo jiným nekorektnostem návrhu/implementace.
Pro shrnutí výše uvedených konceptů používá Go slogan v záhlaví (viz též golang.org a blog.golang.org). Vypůjčíme si ještě malou ukázku z Effective Go:
c := make(chan int) // Alokace nového kanálu. // Spusť třídění v nové gorutině a až bude dokončeno, // pošli o tom zprávu komunikačním kanálem. go func() { list.Sort() c <- 1 // Pošli zprávu/signál dokončení, // v tomto případě na zasílané hodnotě nezáleží. }() dělejPoNějakýČasNějakouJinouUžitečnouPráci() <-c // Počkej na dokončení třídění, přijatou hodnotu z komunikačního kanálu // v tomto případě ignorujeme. Zajímá nás jen synchronizace gorutin, // tj. událost příjmu z kanálu => list je setříděn.
Hezkou a myslím dost didaktickou ukázkou již o trochu složitějšího využití paralelismu gorutin, s důrazem na způsob uvažování nad návrhem konstrukce, je paralelní implementace Eratosthenova síta. Příklad je to skutečně jenom výukový, ve skutečnosti tato implementace začne rychle ztrácet dech i v porovnání s klasickým sekvenčním programem. „Profi“ implementace stále stejného zadání (generování všech prvočísel do daného limitu) v Go s využitím paralelismu gorutin je k vidění třeba tady a autor uvádí, že naměřil na svém 2,4GHz Core i5 stroji čas pro určení prvních 50 847 534 prvočísel menších než 1 000 000 000 v hodnotě kolem 0,7 sekundy.
Součástí standardní knihovny, je také modul netchan. S jeho pomocí lze vytvářet složitější systémy pro distribuované zpracování tím, že netchan „protahuje“ koncept kanálu přes síťové připojení. Možnosti paralelismu v Go se tím dále rozšiřují a v tomto případě jsou důvody očekávat dobrou škálovatelnost současně s malými nároky na množství kódu, který takovou distribuovanou architekturu implementuje. Jde ve své podstatě o ekvivalent síťových socketů, ale s typovou podporou (přesněji řečeno pomocí interface{} a typově bezpečného castingu) uživatelsky definovaných entit.
V dnešním textu jsme poprvé pootevřeli krabici s novou hračkou a prohlédli si některé z větších stavebních prvků Dupla, ehm... Go. V příští části se pokusíme plynule přejít na přehled používání kostek LeGo (např. objektové programování, rozhraní, uzávěry, ...).
Jan Mercl a Ondřej Surý pracují v Laboratořích CZ.NIC, výzkumném a vývojovém centru správce české národní domény.
Nástroje: Tisk bez diskuse
Tiskni
Sdílej:
A i korutiny jdou řešit lépe a v existujících jazycích (OpenMP, Boost Thread, ThreadWeaver, Scheduler/Linux Activations)imho gorutiny maji byt neco jineho nez vlakna a v kombinaci s kanaly to muze byt docela mocne. i kdyz osobne by se mi libilo spis, kdyby zpravy zaslane kanalem definitivne neslo precist, jak to ma udelane treba sing#
Go je typově a paměťově bezpečný jazyk a to se s aritmetikou ukazatelů nedá snadno, tedy spíše v podstatě asi vůbec nijak, skloubit dohromady.Přijatelný kompromis je systém vlastních alokátorů, jak ho např. má C++ nebo Ada.
Go je typově a paměťově bezpečný jazyk a to se s aritmetikou ukazatelů nedá snadno, tedy spíše v podstatě asi vůbec nijak, skloubit dohromady.Jazyky se silnějším typovým systémem by s tím neměly mít problém. Viz třeba ATS.
Třeba tohle? http://yosefk.com/c++fqa/defective.html#defect-7
a to vznikne jednoduchým std::map<std::string, std::string> STL je hodně blbě navržená knihovna a kdykoliv musím s C++ dělat, tak se jí obloukem vyhýbám a využívám raději přímo Clib. (cstdlib, cstdio, c*)
nikoliv - třeba místo komplexních templates mohli naprogramovat opravdový dynamický arrays s měnitelnou délkou a tak mohly být dělaný i stringy - např. jako to dělá jazyk D. Btw, Dčkový string jako array je v benchmarku asi 3x rychlejší než std::string, daleko lépe navržený a mnohem elegantnější co se týče úprav, vyhledávání apod. (např: slicing: string a = "hello world"; string b = a[6 .. $]; /* b je "world" */)
co se týče std::map, D má asociativní arrays: int[string] x = [ "foo":5, "bar":10 ]; x["y"] = 143; který jsou opět efektivnější než std::map a mnohem lépe se s nimi pracuje.
Špatně, std::map není hashtable, ale binary search tree; hashtable v C++ je std::unordered_map, který je ale až součástí TR1.
Co se týče asociativní array v D, tak je to ve skutečnosti opravdu hashtable.
To s map není špatný návrh, to je naopak hodně propracovaný návrh.No, to se přece nevylučuje. A jestli je skutečný typ té StringToStringMapy opravdu takový, jak se v odkazovaném dokumentu píše (což nemám nejmenší chuť zjišťovat), pak je to skvělý příklad toho, jak mizerný může být takový propracovaný návrh.
Nemám nic proti templates ani metaprogrammingu, ale STL a Boost mi přijdou dost "overengineered" a rozvrstvené; asi je to tím, že pracuju hlavně s Cčkem, kde nic takového neexistuje - ale jako správně udělanou mid/high-level standardní knihovnu si představuju něco jako Phobos2 z jazyka D https://github.com/D-Programming-Language/phobos/tree/master/std - umí toho dost (včetně algorithm knihovny pro různý filtrování polí a takové věci) a přitom je srozumitelně a jednodušše naprogramovaná; Co se týče kontejnerů jako vector, map atd, nic takového není třeba, protože jsou podporovány dynamic arrays / hashtables na kterých je možno provádět bez problému slicing, vyhledávání apod a pro filtrování (např. odstraňování elementů, nebo nalezení největšího čísla v array apod) je možné využít algorithm knihovnu (např. template "reduce")
template<typename T> class Something { static void func(void) { T::tady bude asi problém} };
C++ je dobrý jazyk, ale vyžaduje praxi, protože půlka jeho vlastností je určena pro méně než jedno procento dobře navrženého kódu (např. pointerová aritmetika nebo private členové).Nebylo by pak lepsi rozdelit jej na 2 dobre spolupracujici jazyky, a tu pulku umistit do toho nizkourovnoveho? Ale ja to chapu. Podle me C++ melo sve misto v historii jako urcity meziclanek - jazyk na psani komplikovanych aplikaci v dobe, kdy kompilatory byly jeste v plenkach. Mista, kde se opravdu hodi nasadit C++ - slozity program, ktery musi byt zaroven rozumne rychly - dost ubyvaji, namatkou jeste treba herni enginy. (Na druhou stranu, Common Lisp tyto problemy resil taky. I kdyz ta nizka uroven mohla byt v nem trochu lepsi.)
GC v Go přináší stejné výhody, ale i pochopitelně i shodné nevýhody jako v cca každém jiném GC jazyku. Pocit, že na uvolňování paměti lze z hlediska programátora zapomenout, je oprávněný jen z velmi malé části.Ze statistik o tvorbě programů vyplývá něco jiného. GC se zavádí právě kvůli problémům programátorů s uvolňováním paměti.
Nedobře napsaný kód v libovolném GC jazyce generuje pro GC zbytečně mnoho práce (třeba alokace v cyklu, kterou ale přitom lze vytknout před něj). Za to se platí zpomalením/zpomalováním/přerušováním běhu aplikace a zvýšenými nároky na zdroje systému. Dva funkčně shodné programy v GC jazyce se mohou lišit až příliš snadno na škále od perfektně vyhovující aplikace až po program, který pro soustavný trashing není použitelný vůbec k ničemu, tedy pokud nepočítáme účinnou „výrobu“ frustrace jeho nešťastného uživatele.Tohle není věc specifická pro jazyky s GC. Tuhle chybu může udělat i vývojář v jazyku bez GC.