Portál AbcLinuxu, 26. dubna 2024 14:59

Google Go v příkladech 1.

18. 5. 2011 | Jan Mercl
Články - Google Go v příkladech 1.  

Zahajujeme navazující seriál o programování v Go, tentokrát v příkladech. Ukázky se soustředí na malý (nebo jeden) okruh problémů a budou – i vzhledem k rozsahu článku/dílu seriálu – jen schematické.

Na druhé straně každá ukázka je v době odevzdání (Go se již mění pomaleji, ale ke změnám stále dochází) kompletní program, který je možno přeložit (pokud neukazuje úmyslně nepřeložitelnou chybu). Novinkou, přinejmenším snad v článcích o Go na AbcLinuxu, je možnost si na Go doopravdy „sáhnout“ a to i bez instalace jeho distribuce. Na domácí stránce Go je už nějaký čas tzv. Go playground. V něm je možné, v rámci některých pochopitelných omezení, zadávat a upravovat zdrojový kód v Go, následně jej nechat na vzdáleném serveru přeložit/sestavit a v případě úspěchu i provést. Pokud výsledný program úspěšně doběhne ve svém časovém limitu, zobrazí Go playground i vše, co zapsal program napsal do svého standardního výstupu.

Veškeré ukázky kódu jsou následovány odkazem, který příklad otevře v Go playgroundu. Tam je pak možné ukázku/program upravovat a nakonec klepnutím na tlačítko „COMPILE & RUN“ (nebo stiskem shift-enter) zkusit provést. Neověřoval jsem, ale předpokládám, že povolený Javascript bude nejspíše nutný. Pokud si „pískoviště“ zkusíte, může se Vám stát, že některá z dnešních ukázek bude hlásit timeout. Obvykle postačí ji v takovém případě zkusit provést ještě jednou, ale úspěch může záležet na zcela vnějších faktorech (možná dle okamžitého zatížení některého Google serveru), takže nic se v tomto směru nedá slíbit. Z diskuzí jinde pochází ještě jedna rada – někdy stačí v případě potíží (timeout provádění) jen pozměnit whitespace programu a tím ovlivnit použití kompilační cache playgroundu, která je v současné implementaci podezřelá, že je někdy příčinou problému.

V době odevzdávání článku se navíc ještě našla nemilá chyba (nahlášená), která se projevuje ve Firefoxu a (zatím) znemožňuje použití Go playground z FF. Doufejme, že chyba bude do doby vyjití článku opravena.

Obsah

Odlož paniku, vzchop se a jdi (defer panic, recover and go)

link

Příklad 1.

link
package main 

import "fmt"

func f(x int, c chan string) { 
    defer func() { 
        if e := recover(); e != nil { 
            c <- fmt.Sprintf("f(%d): %s", x, e) 
        } 
    }() 

    if x == 2 { 
        panic("x == 2 není podporováno") 
    } 

    c <- fmt.Sprintf("f(%d) == %d", x, 2*x) 
} 

func main() { 
    c := make(chan string) 
    for i := 1; i <= 2; i++ { 
        go f(i, c) 
        defer fmt.Printf("(%d) %s\n", i, <-c) 
    } 
} 

Upravit/Spustit

Výstup:

(2) f(2): x == 2 není podporováno
(1) f(1) == 2

Klíčové slovo go spustí provádění dané funkce/funkčního literálu v nové gorutině (ř.22). Gorutina nemůže volajícímu přímo nic vrátit (ve smyslu funkční hodnoty), volající navíc pokračuje bez čekání na ukončení gorutiny. Pokud chceme s paralelně prováděnou gorutinou komunikovat, je standardním Go idiomem použití komunikačních kanálů. V ukázce je kanál vytvořen na ř.20, funkce f (ř.5) standardně tímto kanálem odpovídá, gorutina jej získala jako argument volání. Na ř.16 odpovídá normálně a na ř.8 při výjimce (k té za chvíli).

Na ř.23 hlavní fuknce programu vypisuje výsledky vrácené gorutinami kanálem. V tomto případě pomocí odloženého příkazu klíčovým slovem defer (k němu opět za moment). Důležité je si uvědomit, že pokud funkce main() programu spustí libovolný počet gorutin, ale nijak nepočká na jejich provedení (tj. kdybychom třeba zakomentovali ř.23), tak celý program skončí svoje provádění dříve než budou mít další/jiné gorutiny příležitost dokončit svoji práci nebo ji dokonce vůbec zahájit. Možná se to bude někomu zdát jako zbytečná poznámka, ale v diskuzích na golang-nuts je tato chyba v dotazech začínajících programátorů v Go vidět dost často. Podobně chybné, jen opačně, je na konci main kvůli tomu např. psát nějaké zpoždění odhadovaného počtu sekund (třeba pomocí time.Sleep). Pokud je pro správnou činnost programu třeba nechat tu či onu gorutinu dokončit práci, je nutné se k takové události synchronizovat. Čekáním na příchod signálu/hlášení/výsledku kanálem je automaticky takovou synchronizací.

V dané ukázce by bylo možné čekat na známý počet hlášení (tj. 2) třeba v dalším cyklu. Na ř.23 využíváme pro automatické spárování potřebného počtu synchronizací příkaz defer. Jeho sémantika je jednoduchá – defer příkaz, kterým musí být volání funkce – způsobí zřetězení volání takové funkce těsně před návratem z funkce, ve které provádíme příkaz defer. Defer je možné vyvolávat opakovaně, odložené volání se koná v LIFO pořadí. Teprve až poté, co je celý tento „lokální zásobník“ vyprázdněn, dojde k návratu z funkce. V našem případě z funkce main, dojde proto k ukončení celého programu, leč řádně, až po ukončení práce obou (instancí) gorutiny f.

Právě popsaný mechanismus defer, zvláště pak skutečnost, že odložené příkazy se provedou ještě před návratem z dané funkce, se využívají pro ošetřování výjimek. Na ř.7 vidíme volání zabudované funkce recover, v rámci odloženého volání funkčního literálu na ř.6. Recover() vrací za běžných okolností nil. Pokud bylo normální provádění funkce ukončeno výjimkou, pak recover, použitý v odloženě vyvolané funkci, vrací aktuální chybový objekt. Protože recover voláme odloženě (ř.6), tak v případě výjimky, během provádění ř.8, „vidíme“ v uzávěře odložené funkce ještě platné parametry funkce f. Proto můžeme bez obav na ř.8 poslat kanálem c chybové hlášení. Kanál typu string je zde použit jenom pro zjednodušení příkladu, v main() se o obsah hlášení nijak nezajímáme, jen je vypíšeme.

Na ř.12 uměle vytváříme chybu, představte si pod tím obecný případ neplatného argumentu/stavu/jiné chyby. Bylo by zde možné opět přímo poslat něco do kanálu c a provést return. Pokud už ale máme ošetřeno pomocí defer zachytávání výjimek, je pohodlnější toto zachycení vyvolat jednoduše pomocí panic. Navíc takto zachytíme i neošetřenou výjimku z libovolné nižší úrovně provádění (vizte příklad 2). Argumentem panic je chybový objekt. Tato zabudovaná funkce je generická, tj. chybový objekt může být jakéhokoli typu. Pro změnu chování funkce dle typu chybového objektu vizte následující příklad.

Příklad 2.

link
package main 

import ( 
    "fmt"
    "os"
) 

func div(a, b int) int { 
    if b == -2 || b&0xff == 0xaa { 
        panic(fmt.Errorf("nepovolený argument %d", b)) 
    } 

    return a / b 
} 

var p *int 

func f(x int) (y *int, err os.Error) { 
    y = new(int) 

    defer func() { 
        if e := recover(); e != nil { 
            y = nil 
            err = e.(os.Error) 
        } 
    }() 

    switch x { 
    case -1: 
        *y = *p 
    case 2: 
        panic("TODO:f(2)") 
    default: 
        *y = div(0x12345678, x) 
    } 
    return 
} 

func main() { 
    for i := -3; i <= 3; i++ { 
        v, err := f(i) 
        if err != nil { 
            fmt.Printf("f(%d) – %s\n", i, err) 
            continue 
        } 

        fmt.Printf("f(%d) == %d\n", i, *v) 
    } 
}

Upravit / Spustit

Výstup:

f(-3) == -101806632
f(-2) – nepovolený argument -2
f(-1) – runtime error: invalid memory address or nil pointer dereference
f(0) – runtime error: integer divide by zero
f(1) == 305419896
panic: TODO:f(2) [recovered]
    panic: interface conversion: string is not os.Error: missing method String

runtime.panic+0xac /tmp/sandbox/go/src/pkg/runtime/proc.c:1060
    runtime.panic(0x45a1f0, 0xf84001fd90)
itab+0x101 /tmp/sandbox/go/src/pkg/runtime/iface.c:134
    itab(0x4547b0, 0x43ec38, 0x0, 0xf84003e048, 0x1, …)
runtime.ifaceE2I+0xe3 /tmp/sandbox/go/src/pkg/runtime/iface.c:473
    runtime.ifaceE2I(0x4547b0, 0x43ec38, 0xf840001320, 0x2b42b055cdf8, 0xf84000b780, …)
runtime.assertE2I+0x43 /tmp/sandbox/go/src/pkg/runtime/iface.c:488
    runtime.assertE2I(0x4547b0, 0x43ec38, 0xf840001320, 0xf84000b7b0, 0xf840001320, …)
main._func_001+0x75 /tmpfs/gosandbox-5522aa24_eb79397b_f981defe_0fef1eaf_56f2c89a/prog.go:24
    main._func_001(0xf8400000b0, 0xf840001330, 0x401f4f, 0x2b42b055c100, 0x2b42b055cfb8, …)
----- stack segment boundary -----
runtime.panic+0xf6 /tmp/sandbox/go/src/pkg/runtime/proc.c:1041
    runtime.panic(0x43ec38, 0xf840001320)
main.f+0x114 /tmpfs/gosandbox-5522aa24_eb79397b_f981defe_0fef1eaf_56f2c89a/prog.go:32
    main.f(0x2, 0x642528660000000c, 0xf84000c740, 0x200000002, 0x12, …)
main.main+0x39 /tmpfs/gosandbox-5522aa24_eb79397b_f981defe_0fef1eaf_56f2c89a/prog.go:41
    main.main()
runtime.mainstart+0xf /tmp/sandbox/go/src/pkg/runtime/amd64/asm.s:77
    runtime.mainstart()
runtime.goexit /tmp/sandbox/go/src/pkg/runtime/proc.c:178
    runtime.goexit()
----- goroutine created by -----
_rt0_amd64+0x8e /tmp/sandbox/go/src/pkg/runtime/amd64/asm.s:64

Tato ukázka rozvíjí příklad 1 v oblasti výjimek. Na ř.10 vyvoláváme výjimku ve funkci div, zachytává se ale o úroveň výše, na ř.22 ve funkci f. Na ř.13 budeme občas dělit nulou bez kontroly, odchycení je ale stejné. Funkce f (ř.18) je modelová funkce vracející ukazatel a příznak chyby. Na ř.19 je častý případ, kdy na počátku provádění funkce vytváříme nový objekt (alokace). V případě zachycení výjimky chceme místo ukazatele na objekt zajistit (bezpečnější) vrácení hodnoty nil, to zajišťuje ř.23. Na ř.30 občas způsobíme další typ výjimky – dereferenci nil ukazatele. Na ř.32 opět občas „zpanikaříme“, tentokrát úmyslně s chybovým objektem odlišného typu, v tomto případě string. Na ř.24 totiž provádíme run time typovou kontrolu (type assertion) chybového objektu e. Pro typ string tato kontrola selže, způsobí opět další výjimku, která už ukončí běh celého programu. To je modelové schéma, kdy potřebujeme rozlišit výjimky, které vracíme volajícímu jako chybu (s nadějí na zotavení programu) a situace, kdy pokračovat nelze – zde modelově vstupujeme do části kódu, který je teprve třeba napsat. Proto nám ve výstupu neprojdou všechny hodnoty cyklu z ř.40 a pro i == 2 program skončí neošetřenou chybou a tedy výpisem zásobníku.

Vracení chyby typu os.Error je záležitost víceméně (ustáleného) zvyku. Na druhé straně os.Error není statický typ, je to rozhraní, takže v případě potřeby jemnějšího rozlišení kategorií chyb nebo když je nutné různé chyby doplnit o přidané informace – nám nic nebrání použít uživatelsky definovaný typ. Stačí bude‑li implementovat jedinou metodu rozhraní os.Error – String(), tj. převod na řetězec.

Všechny typy jsou si rovné, ale některé jsou si rovnější

link

Příklad 3.

link
package main 

type bad interface{} 

func (b bad) foo() { 
    println("Hi from bad") 
} 

type ok interface { 
    foo() 
} 

type bar struct{} 

func (b bar) foo() { 
    println("Hi from bar") 
} 

func main() { 
    var iface bad 
    iface.foo() 
    bar{}.foo() 
} 

Upravit / Spustit

Výstup:

prog.go:5: invalid receiver type bad
prog.go:21: iface.foo undefined (type bad has no field or method foo)

Všechny pojmenované typy v Go mohou mít k sobě přidružené metody. Jednou z „optických“ ne ortogonalit tohoto pravidla je to, že typ rozhraní může metody pouze deklarovat avšak nikoli implementovat. Pro všechny ostatní typy je tomu právě naopak. Příklad 3 na ř.5 implementuje metodu rozhraní bad (ř.3) a to nám kompilátor nedovolí. Jako cvičení si lze vyzkoušet uvést tento příklad do přeložitelného stavu.

Ještě jedna (po)drobnost omezuje typy, ke kterým je možné definovat metody. Typ (přijímače metody) může být sám o sobě ukazatelem (ukazatelovým typem), ale jím odkazovaný typ už ukazatelem být nesmí. Vizte též specifikaci.

Třikrát totéž není totéž

link

Říká se, vcelku oprávněně, že je dobré, když jedna sémantická konstrukce je v programovacím jazyce vyjádřitelná jenom jedním způsobem. Podíváme se nyní na tři verze skoro stejného schématu. Úmyslně je v Go napíšeme různými způsoby. Myslím ale, že v žádném příkrém rozporu s první větou tohoto odstavce nebudeme. Různé implementace v tomto případě přinášejí ne zcela shodnou sémantiku – byť rozdíly jsou, dle úhlu pohledu, spíše jen v menších detailech. Každá implementace se ale hodí pro trochu jinou situaci. Představme si třeba rozdíl mezi situací kdy jsme odkázáni na použití existující funkce/metody třetích stran vs. stav, kdy si „operátor“ můžeme/chceme a/nebo musíme napsat sami.

Modelové schéma v našem případě je nějaká abstraktní operace nad adresářovým stromem souborového systému, přesněji řečeno provedení nějaké činnosti nad jednotlivými adresáři stromu. Kdo chce, může v tom vidět i Visitor pattern. Ve standardní knihovně existuje funkce filepath.Walk, kterou se zde inspirujeme. Pro jednoduchost degenerujeme procházení stromu na návštěvu pouze jeho kořene a modelově chceme jedním mechanismem získávat dvě různé informace – počet souborů v adresáři nebo jejich celkovou velikost. Další možnosti konkrétní „operace“ (třeba v duchu příkazů cat, echo, mv, …) si jistě lze domyslet.

Příklad 4a.

link
package main 

type visitor func(string) 

func files(dir string) { 
    println(dir, "obsahuje celkem 42 souborů") 
} 

func bytes(dir string) { 
    println(dir, "obsahuje celkem 524287 bajtů") 
} 

func walk(path string, v visitor) { 
    v(path) 
} 

func main() { 
    walk("/", files) 
    walk("/bin", bytes) 
} 

Upravit / Spustit

Výstup:

/ obsahuje celkem 42 souborů
/bin obsahuje celkem 524287 bajtů

Příklad 4a je, volně řečeno, ve stylu Cčka. Bez použití (možnosti použití) rozhraní nebo třeba virtuálních metod objektů je použití funkčního typu pro „operaci“ jednoduchým řešením – a v případě C víceméně také jediným. Pomíjíme zde nepoměrně složitější možnosti, které přináší třeba GObject, protože to už nehovoříme o jazyku, ale o jeho knihovně. Ostatně nějaký mechanismus jako vtable a vcall jde s konečným úsilím implementovat snad vždy i v ne OOP programovacím jazyku.

Řešení (schématu) je zcela nekomplikované, jediná deklarace (funkčního typu visitor na ř.3), použitá v definici funkce walk na ř.13, je zde „nutná“ navíc jen v rámci snad dobrého stylu. Typ parametru 'v' by mohl být definován i typovým literálem – lze si ověřit na „pískovišti“ (nezkusíte si to?).

Příklad 4b.

link
package main 

type visitor interface { 
    visit() 
} 

type files string 

func (dir files) visit() { 
    println(dir, "obsahuje celkem 42 souborů") 
} 

type bytes string 

func (dir bytes) visit() { 
    println(dir, "obsahuje celkem 524287 bajtů") 
} 

func walk(path visitor) { 
    path.visit() 
} 

func main() { 
    walk(files("/")) 
    walk(bytes("/bin")) 
} 

Upravit/Spustit

Výstup:

/ obsahuje celkem 42 souborů
/bin obsahuje celkem 524287 bajtů

Příklad 4b představuje obvyklý přístup, který by se asi dal nazvat Go idiomatický. Ostatně toto řešení je (schématicky) identické implementaci svého vzoru (vizte filepath.Walk a rozhraní filepath.Visitor). Na ř.3 deklarujeme rozhraní. Na ř.7 a 13 deklarujeme pojmenované typy, které toto rozhraní implementují (ř.9 a 15). Na ř.24 a 25 provádíme konverzi literálu typu string na typ files resp. bytes a tím volíme „operaci“, kterou od funkce walk požadujeme. Konverze je zde použita jen pro zkratku příkladu, jistě si snadno domyslíte obecnější případ entit různých typů.

Příklad 4c.

link
package main 

type visitor func(string) 

func (v visitor) visit(dir string) { 
    v(dir) 
} 

func (v visitor) walk(path string) { 
    v.visit(path) 
} 

var files visitor = func(dir string) { 
    println(dir, "obsahuje celkem 42 souborů") 
} 

var bytes visitor = func(dir string) { 
    println(dir, "obsahuje celkem 524287 bajtů") 
} 

func main() { 
    files.walk("/") 
    bytes.walk("/bin") 
} 

Upravit / Spustit

Výstup:

/ obsahuje celkem 42 souborů
/bin obsahuje celkem 524287 bajtů

V příkladu 3 jsme si připomenuli, že metody může implementovat každý (pojmenovaný) typ, který není rozhraní. V příkladu 4c využijeme jeden, možná trochu překvapivý, důsledek tohoto pravidla. V Go můžou mít funkce metody, korektně řečeno – metody lze implementovat i pro (pojmenovaný) funkční typ. Autor nezná dostatečně dobře dostatečný počet jiných programovacích jazyků aby mohl posloužit příkladem „stejně/podobně jako jako v jazyce X“, jen odhaduje, že něco podobného by možná šlo „spáchat“ přinejmenším v Javascriptu?

Tato implementace nejvíce připomíná styl funkčních jazyků. Na ř.3 deklarujeme funkční typ visitor. Mimochodem deklarace je zcela stejná jako „C“ verzi v příkladu 4a. Na ř.5 a 9 definujeme metody typu visitor. Na ř.13 a 17 deklarujeme proměnné files a bytes typu visitor, v obou případech s inicializátorem pomocí funkčního literálu. Toto je implementační detail, mohli bychom definovat obyčejné funkce (s podpisem shodným s typem visitor), pak by ovšem přístup k jejich metodám byl možný pouze po konverzi ve stylu:

func files(dir string) { 
    println(dir, "obsahuje celkem 42 souborů") 
}

//...

visitor(files).walk("/") 

Mírně složitější verze s funkčním inicializátorem, použitá v příkladu 4c, se vyplatí, pokud budeme takovou funkci, tedy přesněji metody takového funkčního typu, volat častěji, protože taková volání jsou potom krátká a dobře čitelná jak vidno na ř.22 a 23.

Příklad 4c způsobuje v této zkratkovité podobě kontroverzní reakce – minimálně u kolegy, který Go nepoužívá, ale je ochotný tyto články před odevzdáním číst a kontrolovat. Podstatně podrobněji se tomuto „triku“ věnuje část Effective Go – Interface methods, na konci oddílu v diskuzi typu HandlerFunc a jeho metody ServeHTTP. Celý mechanismus popsaný v posledně uvedeném odkazu se reálně používá ve standardní knihovně.

Na závěr

link

Pokračování by mělo vyjít snad za měsíc. Bude-li mít někdo z čtenářů zvláštní zájem o nějaké téma ke zpracování pak nechť dá prosím vědět s dostatečným předstihem.

Laboratoře CZ.NIC

O autorovi

Jan Mercl, autor textu, pracuje v Laboratořích CZ.NIC jako programátor pro výzkum a vývoj.

Seriál Google Go (dílů: 8)

První díl: Google Go – 1. narozeniny, poslední díl: Google Go – 2. narozeniny.
Předchozí díl: Google Go – pokročilejší témata
Následující díl: Google Go v příkladech 2.

Další články z této rubriky

LLVM a Clang – více než dobrá náhrada za GCC
Ze 4 s na 0,9 s – programovací jazyk Vala v praxi
Reverzujeme ovladače pro USB HID zařízení
Linux: systémové volání splice()
Programování v jazyce Vala - základní prvky jazyka

ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.