Portál AbcLinuxu, 26. leden 2020 01:05

Google Go v příkladech 2.

16. 6. 2011 | Jan Mercl
Články - Google Go v příkladech 2.  

V dříve publikovaných článcích na AbcLinuxu bylo zmiňováno, že Go je OOP jazyk, ale přitom nemá/nezná třídy a sémantika dědičnost je v něm atypická, řekněme přinejmenším z pohledu člověka zvyklého na např. C++ či třeba Javu. Tyto jazyky pro mnohé představují, vcelku oprávněně, něco jako současný standard z hlediska implementace OOP technik. Dnes se pokusíme některé obvyklé případy OOP, zhruba v duchu právě zmíněných jazyků, napsat v Go a to způsobem, který je v něm přirozený.

Obsah

Budeme se věnovat pouze tomu, jak se instance „tříd“ chovají (metody), nikoli tomu jak „vypadají“ (položky struktur). To druhé bylo již v minulosti probíráno a možná se podrobněji objeví jako některé z dalších témat. Poznamenejme, že v Go jsou navíc definice „rozložení“ obsahu typu a popis jeho chování, ortogonální koncepty – lze je studovat i navrhovat (téměř) nezávisle na sobě. Výhradu slovem „téměř“ ponechejme sémantice typu – často je prostě význam (pro programátora) oněch dvou tváří typu spolu provázán – i když o tom kompilátoru nic neříkáme. Příklady jsou, pro lepší přiblížení se stávajícím programátorům v jiných jazycích, „opřeny“ o strukturované typy, rád bych připomenul, že v Go lze metody definovat i pro další, např. číselné typy (pojmenované).

V příkladech si prosím v řetězcových literálech místo \x25 představte znak procenta (%). Je to jen způsob jak obejít chybu 1770.

Příklad 1: Abstraktní základní „třída“, virtuální metody

link

Budeme implementovat jednoduché OOP zadání: Chceme čistě abstraktní základní třídu a napsat její různé konkrétní implementace. Jak vidno, z pohledu (zjednodušené) OOP klasiky, všechny metody „třídy“ budou (muset být) virtuální.

package main

import (
    "fmt"
)

type Base interface {
    M()
}

type T struct {
    i int
}

func (t *T) M() {
    fmt.Printf("ř.16: \x25#v\n", t)
}

type U struct {
    s string
}

func (u *U) M() {
    fmt.Printf("ř.24: \x25#v\n", u)
}

func main() {
    var t, u Base = &T{42}, &U{"foo"}
    t.M()
    u.M()
}

Upravit/Spustit

Výstup

ř.16: &main.T{i:42}
ř.24: &main.U{s:„foo“}

Na ř.7 definujeme (veřejné – s velkým písmenem na začátku) rozhraní a na ř.8 deklarujeme jeho jedinou metodou M. Metody veřejného rozhraní, které má být možné implementovat i v jiných modulech, musí být také veřejné, opět tedy s velkým písmenem na začátku. Celá definice rozhraní Base je v tomto případě sémanticky shodná s definicí abstraktní třídy v jiném typickém OOP jazyku.

Na ř.11 a 19 definujeme typy, které na ř.15 a 23 implementují všechny (jednu) metodu/y rozhraní Base. Díky tomu je zde sémantika shodná s „klasickým“ popisem tříd, která dědí z/jsou v typové hierarchii potomkem „třídy“ Base. Jen zde, v typickém Go OOP duchu, u typů implementujících rozhraní Base (čti např. „extends Base“), nemusíme tento fakt oznamovat. Kompilátor se pouze při typové kontrole na ř.28 ujistí, že typy T a U implementují Base a lze je tedy přiřadit do proměnných 't' a 'u'.

Na ř.29 a 30 už jen zavoláme metodu M instancí 't' a 'u' a na výstupu programu si ověříme, že vše proběhne dle předpokladu.

Rozhraní v Go mají klasickou OOP dědičnost (úplnou a vícenásobnou), viz též poslední část specifikace. Pokud je někdo zvyklý uvažovat v třídách/hierarchii tříd a potřebuje pouze virtuální metody (situace velmi blízká Javě), může je v Go nahradit pomocí rozhraní docela snadno a největší rozdíl je „záměna“ klíčových slov class vs interface, v sémantice je shoda podle mě úplná. Go programátor jen nebude vůbec mluvit o třídách, takže si s „klasickým“ OOP programátorem chvilku nebudou rozumět – i když oba vlastně budou v tomto případě dělat totéž.

Příklad 2: DRY

link

Neboli „Don't repeat yourself“. Na ř.16 a 24 příkladu 1 děláme vlastně totéž. Jeden příkaz na aplikaci principu DRY asi nestačí, ale teď jde jen o schémata přístupu a řešení. Výpis (pomocný/ladící) instance, s rozlišením jak typu tak obsahu je často používaný pomocník. Nebyl by problém si napsat zcela ne-OOP funkci, která – díky Go reflection (Javaisté vědí, C++ RTTI by nám také ale v tomto případě také stačilo) – vypíše takové informace. Proč si ale zaplevelovat jmenný prostor, když to může být metoda, že? Čeho ale metoda? Rozhraní Base? Jistě by to tak být mohlo, ale to bychom si nic nového neukázali. Prostě by k metodě Base.M přibyla nějaká metoda Base.Show a tu bychom museli dvakrát implementovat pro typy T a U (a všechny další typy implementující Base). Napsat tohoto pomocníka jako statické metody T a U také moc nepomůže, opět bychom se dvakrát téměř opakovali. Nyní nastává vhodná příležitost pro Go styl dědičnosti. Řekněme, že pomocná metoda bude jen dočasná/ladící a neveřejná (to není podmínkou). Z pohledu „tříd“ T a U dědí z Base, z pohledu Go T a U implementuje Base a dědí z 'base' (zvolené jméno 'base' není ničím podstatné).

package main

import (
    "fmt"
    "path"
    "runtime"
)

type Base interface {
    M()
}

type base struct{}

func (b base) show(me interface{}) {
    pc, file, line, ok := runtime.Caller(1)
    if !ok {
        panic(":-/")
    }

    fmt.Printf("\x25#x \x25s.\x25d: \x25#v\n", pc, path.Base(file), line, me)
}

type T struct {
    base
    i int
}

func (t *T) M() {
    t.show(t)
}

type U struct {
    base
    s string
}

func (u *U) M() {
    u.show(u)
}

func main() {
    var t, u Base = &T{i: 42}, &U{s: "foo"}
    t.M()
    u.M()
}

Upravit/Spustit

Výstup:

0x400deb prog.go.30: &main.T{base:main.base{}, i:42}
0x400e3b prog.go.39: &main.U{base:main.base{}, s:„foo“}

Na ř.13 definujeme typ 'base' a na ř.15 jeho metodu 'show'. Na ř.25 a 34 uvádíme 'base' jako položku struktur typu T a U. Pole bez jména, pouze s názvem typu je v Go struktuře (myšleno klíčové slovo struct) způsob, jak zdědit obsah takto děděného typu a současně i jeho metody. Alternativně by šlo totéž i s ukazatelem (viz diskuzi „“anonymous field“), ale to nyní pomineme. Oproti OOP klasice ale přijímač (obvykle prostě ukazatel) metody takového předka má typ pouze onoho předka, nikoli typu, který jej dědí. Proto na ř.30 a 39 metodě show předáváme i přijímač metody M.

Příklad 3: Selektivní přístup

link

Další „standardní situací“ v OOP je případ, kdy dědíme téměř všechno od předchůdce až na jednu nebo jen několik málo metod. Ta/ty pak nějak modifikují chování zděděné/ých metod/y Zcela schematicky to v Go lze řešit takto:

package main

type base struct{}

func (b *base) m1() {
    println("m1()")
}

func (b *base) m2() {
    println("m2()")
}


type successor struct {
    base
}

func (o *successor) m2() {
    println("taky m2(), ale jinak")
}

func main() {
    b, s := &base{}, &successor{}
    println("typ base")
    b.m1()
    b.m2()
    println("typ successor")
    s.m1()
    s.m2()
}

Spustit/Upravit

typ base
m1()
m2()
typ successor
m1()
taky m2(), ale jinak

Na ř.3 definujeme základní typ/předchůdce 'base', na ř.5 a 9 jeho metody m1 a m2. Na ř.14 definujeme odvozený typ/následníka 'successor' a na ř.15 dědíme vše z předchůdce 'base'. Na ř.18 předefinováváme metodu m2, samozřejmě pouze pro typ 'successor'. Z výstupu programu vidno, že dle očekávání typ následníka podědil metodu m1, ale používá vlastní metodu m2. Mimochodem, pokud bychom v této 'overriden' metodě potřebovali volat metodu m2 předchůdce, (jako v Javě pomocí super), napsali bychom třeba před ř.19 „o.base.m2()“. Možná si to budete chtít vyzkoušet v Go playgroundu. U všech příkladů stačí klepnout na odkaz „Upravit/Spustit“.

Na závěr

link

Dnešní díl byl určen hlavně programátorům, kteří jsou navyklí na implementaci OOP mechanismů ve stylu C++ a/nebo Javy. Go je OOP jazyk, ale právě jmenovaní mají často při prvním pohledu na Go pocit, že jsou ztraceni, protože v Go nenacházejí svoje oblíbené, protože skoro neustále používané, třídy a jejich dědičnost. Pokusili jsme se ukázat, že většinu dobrého, co lze v OOP najít, Go implementuje, jen tak trochu jinak. Jestli možná v něčem i snad o trochu lépe nechť posoudí laskavý čtenář sám.

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 v příkladech 1.
Následující díl: Ošetřování chyb v Go

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.