Obsah
1. Rozhraní, metody, gorutiny a kanály v programovacím jazyku Go
4. Předání ukazatele na strukturu (objekt) do volané metody
6. Rozhraní jako datový typ při volání funkcí
7. Metody a rozhraní (struktura vyhovující rozhraní)
8. Vícenásobná implementace stejného rozhraní
9. Řez se strukturami implementujícími společné rozhraní
10. Řezy rozhraní jako parametry funkce
11. Datový typ implementující větší množství rozhraní
15. Spuštění většího množství gorutin na pozadí
16. Kanály – technologie pro komunikaci mezi gorutinami
17. Implementace workerů s využitím gorutin a kanálů
18. Deadlock a jeho detekce v runtime
19. Repositář s demonstračními příklady
1. Rozhraní, metody, gorutiny a kanály v programovacím jazyku Go
Většina vlastností programovacího jazyka Go, s nimiž jsme se alespoň prozatím seznámili, ve skutečnosti nebyla nijak revoluční – kromě automatické správy paměti (GC) a plnohodnotné podpory Unicode se vlastně jednalo o typový systém převzatý z céčkovských jazyků a vylepšený o mnohem silnější typovou kontrolu, řezy, pole s kontrolou mezí, řetězce (ty jsou zde plnohodnotným datovým typem) a samozřejmě o nové datové typy (zejména o mapy). Mezi další vylepšení, s nímž jsme se již seznámili, patří možnost vrátit větší množství hodnot z funkcí, což využijeme i v dnešních příkladech.
Jazyk Go se ovšem stal populární i z jiných důvodů. Především se v jeho typovém systému mohou používat rozhraní (interface) a kromě toho Go podporuje tvorbu takzvaných gorutin, což jsou funkce běžící asynchronně k hlavnímu vláknu (někdy se setkáme s označením light-weight process). Mezi jednotlivými gorutinami je možné realizovat komunikaci s využitím další technologie – takzvaných kanálů (channel). Kanály lze využít jak k běžné synchronizaci, tak i k realizaci front se zprávami (message queue). Užitečný a v kontextu mainstreamových jazyků i přelomový je však fakt, že gorutiny i kanály lze v programech použít velmi snadno; stačí pouze pochopit základní koncept, klíčové slovo go a operátor ← .
Právě popisu rozhraní, gorutin a kanálů bude věnován dnešní článek, i když složitější příklady budou vysvětleny až příště.
2. Metody
Nejdříve se seznámíme s konceptem metod, jež slouží pro implementaci funkcionality svázané s nějakým typem záznamu (struktury). S metodami se setkáme v dnes již klasických programovacích jazycích podporujících objektově orientované programování – v takovém případě jsou metody svázány s nějakou třídou popř. se jejich hlavičky mohou objevit v rozhraních. Ovšem v programovacím jazyku Go se s třídami nesetkáme (s rozhraními ovšem ano). Metody zde jsou implementovány jako funkce u kterých je kromě jména, parametrů a typů návratových hodnot specifikován i takzvaný receiver (příjemce), což je v praxi právě datový typ záznam (resp. struktura) popř. se alternativně může jednat o ukazatel na typ záznam.
Příjemce je – což je specifické právě pro Go – zapisován ještě před jméno metody. Podívejme se na několik příkladů. Nejdříve si uvedeme běžnou funkci bez parametrů vracející hodnotu typu float64:
func length() float64 { ... ... ... }
Metoda s příjemcem typu Line bude vypadat odlišně, protože se před jméno metody musí zapsat právě specifikace příjemce (dvojice identifikátor + typ umístěná do kulatých závorek):
func (line Line) length() float64 { ... ... ... }
Pokud nyní vytvoříme typ záznam pojmenovaný Line:
type Line struct { x1, y1 float64 x2, y2 float64 }
…můžeme metodu length zavolat s využitím běžné tečkové notace, která se mj. používá i v běžných objektově orientovaných jazycích:
line1 := Line{x1: 0, y1: 0, x2: 100, y2: 100} line_length := line1.length()
Povšimněte si, že se v těle metody pracuje s příjemcem line stejně jako s běžným parametrem – interně se totiž o parametr skutečně jedná (v podstatě jde o obdobu this v Javě či self v Pythonu):
func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) }
Podívejme se nyní na úplný zdrojový kód dnešního prvního demonstračního příkladu, v němž je deklarována struktura/záznam nazvaná Line a taktéž metoda length svázaná s touto strukturou:
package main import ( "fmt" "math" ) type Line struct { x1, y1 float64 x2, y2 float64 } func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) } func main() { line1 := Line{x1: 0, y1: 0, x2: 100, y2: 100} fmt.Println(line1) line_length := line1.length() fmt.Println(line_length) }
Po spuštění tohoto demonstračního příkladu by se měla nejdříve zobrazit výchozí hodnota záznamu typu Line tak, jak byla inicializovaná a na druhém řádku vypočtená délka úsečky:
{0 0 100 100} 141.4213562373095
Metody se od funkcí liší ještě v jednom směru – jejich jméno nemusí být v daném modulu unikátní, protože specifikace příjemce stačí sama od sebe na odlišení metody pro různé typy i na odlišení metody od funkce. Následující příklad je tedy zcela korektní:
package main import ( "fmt" "math" ) type Point struct { x1, y1 float64 } type Line struct { x1, y1 float64 x2, y2 float64 } func length() float64 { return 0 } func (point Point) length() float64 { return 0 } func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) } func main() { line1 := Line{x1: 0, y1: 0, x2: 100, y2: 100} fmt.Println(line1) line_length := line1.length() fmt.Println(line_length) }
3. Metody s parametry
Vyzkoušejme si nyní vytvořit metodu s parametry, aby byl ještě více zřejmý jak zápis, jakým se metoda deklaruje ve zdrojovém kódu, tak i technika jejího volání v čase běhu programu (runtime). Parametry metody se zapisují do kulatých závorek za jménem metody, tedy stejně, jako u běžných funkcí. Následující metodě se při jejím volání předá jak vlastní příjemce typu Line, tak i dvojice parametrů dx a dy, které jsou typu float16:
func (line Line) translate(dx, dy float64) { ... ... ... }
Vzhledem k tomu, že příjemcem je běžná datová struktura (neboli záznam), mohlo by se zdát, že implementace metody pro posun úsečky v rámci souřadného systému je triviální – ostatně přístup k položkám struktury máme a víme, že jsou měnitelné (mutable):
func (line Line) translate(dx, dy float64) { fmt.Printf("Translating line %v by %f %f\n", line, dx, dy) line.x1 += dx line.y1 += dy line.x2 += dx line.y2 += dy }
Ve skutečnosti je to však nepatrně složitější, a to z toho důvodu, že v programovacím jazyce Go se při volání funkcí i metod parametry předávají hodnotou a nikoli odkazem. To znamená, že příjemce, který je interně metodě předán jako skrytý parametr, je naklonovanou kopií původní struktury a navenek se tedy změna souřadnic nijak neprojeví (pouze uvnitř metody uvidíme nové hodnoty souřadnic).
Můžeme se o tom snadno přesvědčit po překladu a spuštění tohoto demonstračního příkladu, který sice neskončí s chybou, ovšem po návratu z metody translate budou souřadnice v záznamu line nezměněny:
package main import ( "fmt" "math" ) type Line struct { x1, y1 float64 x2, y2 float64 } func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) } func (line Line) translate(dx, dy float64) { fmt.Printf("Translating line %v by %f %f\n", line, dx, dy) line.x1 += dx line.y1 += dy line.x2 += dx line.y2 += dy } func main() { line1 := Line{x1: 0, y1: 0, x2: 100, y2: 100} fmt.Println(line1) line1.translate(5, 5) fmt.Println(line1) line_length := line1.length() fmt.Println(line_length) }
Výsledek, který získáme po spuštění tohoto příkladu ukazuje, že se položky struktury vně metody skutečně nezměnily (viz první a třetí řádek):
{0 0 100 100} Translating line {0 0 100 100} by 5.000000 5.000000 {0 0 100 100} 141.4213562373095
4. Předání ukazatele na strukturu (objekt) do volané metody
Jakým způsobem se tedy musí postupovat, když budeme chtít ve svém programu implementovat metodu, která mění stav záznamu, pro který byla vytvořena? Řešení je ve skutečnosti velmi jednoduché – namísto předání struktury hodnotou, tedy tak, jak jsme to udělali v předchozím příkladu:
func (line Line) translate(dx, dy float64) { fmt.Printf("Translating line %v by %f %f\n", line, dx, dy) line.x1 += dx line.y1 += dy line.x2 += dx line.y2 += dy } line1.translate(5, 5)
…předáme do metody ukazatel na strukturu. To znamená nutnost změny samotné metody (odlišný typ příjemce a odlišný přístup k prvkům záznamu), ovšem samotné její volání bude stejné (!) jako v předchozím příkladu (nemusíme zjišťovat adresu struktury pomocí unárního operátoru &):
func (line *Line) translate(dx, dy float64) { fmt.Printf("Translating line %v by %f %f\n", *line, dx, dy) (*line).x1 += dx (*line).y1 += dy (*line).x2 += dx (*line).y2 += dy } line1.translate(5, 5)
Takto upravený demonstrační příklad již bude provádět všechny operace takovým způsobem, jak jsme si přáli:
package main import ( "fmt" "math" ) type Line struct { x1, y1 float64 x2, y2 float64 } func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) } func (line *Line) translate(dx, dy float64) { fmt.Printf("Translating line %v by %f %f\n", *line, dx, dy) (*line).x1 += dx (*line).y1 += dy (*line).x2 += dx (*line).y2 += dy } func main() { line1 := Line{x1: 0, y1: 0, x2: 100, y2: 100} fmt.Println(line1) line1.translate(5, 5) fmt.Println(line1) line_length := line1.length() fmt.Println(line_length) }
Na třetím řádku je patrné, že úsečka byla skutečně v rovině posunuta o pět jednotek doprava a nahoru:
{0 0 100 100} Translating line {0 0 100 100} by 5.000000 5.000000 {5 5 105 105} 141.4213562373095
Ve skutečnosti je však v programovacím jazyce Go možné napsat metodu ještě jednodušeji – pouhou změnou typu příjemce (na ukazatel), nikoli změnou samotného těla metody:
func (line *Line) translate(dx, dy float64) { fmt.Printf("Translating line %v by %f %f\n", *line, dx, dy) line.x1 += dx line.y1 += dy line.x2 += dx line.y2 += dy }
Výše uvedený zápis je zajímavý, protože sice předáváme záznam typu Line přes ukazatel, ovšem v těle metody přistupujeme k položkám struktury pouze s použitím tečkové notace a nikoli přes (*line).položka. Důvod pro toto nepochybně praktické chování (které není například v C či C++ možné) jsme si již vysvětlili ve dvanácté kapitole předchozí části <a>tohoto seriálu.
Podívejme se nyní na upravený příklad, který vznikl zjednodušením příkladu předchozího:
package main import ( "fmt" "math" ) type Line struct { x1, y1 float64 x2, y2 float64 } func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) } func (line *Line) translate(dx, dy float64) { fmt.Printf("Translating line %v by %f %f\n", *line, dx, dy) line.x1 += dx line.y1 += dy line.x2 += dx line.y2 += dy } func main() { line1 := Line{x1: 0, y1: 0, x2: 100, y2: 100} fmt.Println(line1) line1.translate(5, 5) fmt.Println(line1) line_length := line1.length() fmt.Println(line_length) }
Výsledkem budou stejné zprávy, jako v předchozím příkladu:
{0 0 100 100} Translating line {0 0 100 100} by 5.000000 5.000000 {5 5 105 105} 141.4213562373095
5. Rozhraní (interface)
Ve druhé části článku se seznámíme s konceptem takzvaných rozhraní (interface). Jedná se o předposlední datový typ, s nímž se setkáme (posledním typem je kanál zmíněný v závěru článku). Rozhraní v jazyku Go byla inspirována protokoly, s nimiž jsme se mohli setkat například v programovacím jazyku Smalltalk: ve stručnosti jde o specifikaci metod (jmen, parametrů, návratových typů), které jsou společné pro entity s nějakou sdílenou vlastností nebo vlastnostmi. V rozhraní se však nijak nespecifikuje vlastní chování, tj. těla metod. V Go navíc není nutné explicitně určovat, které záznamy implementují dané rozhraní – tuto informaci si dokáže automaticky odvodit překladač (poněkud nepřesně se toto chování nazývá duck typing).
Při deklaraci nového rozhraní (tj. při vytváření nového datového typu) je nutné specifikovat jak jméno rozhraní, tak i seznam hlaviček metod, které jsou součástí rozhraní (tento seznam ovšem může být prázdný, nicméně je nutné ho zapsat pomocí prázdného bloku {}). Příkladem rozhraní s jedinou metodou může být datový typ pojmenovaný OpenShape, v němž je předepsána jediná metoda length bez parametrů a s návratovou hodnotou float64 (u metody předepsané v rozhraní se ovšem neuvádí příjemce – ten si Go odvodí automaticky na základě dalšího kódu):
type OpenShape interface { length() float64 }
V rozhraní může být předepsáno větší množství metod:
type ClosedShape interface { area() float64 perimeter() float64 }
Nebo naopak nemusí být předepsána žádná metoda:
type Shape interface { }
Následuje příklad, v němž je pouze deklarována trojice rozhraní:
package main type Shape interface { } type OpenShape interface { length() float64 } type ClosedShape interface { area() float64 perimeter() float64 } func main() { }
6. Rozhraní jako datový typ při volání funkcí
Rozhraní jakožto plnohodnotný datový typ je možné použít pro specifikaci typu parametru (parametrů) ve funkcích. Opět si to vyzkoušejme na našem příkladu s rozhraním nazvaným OpenShape, v němž je předepsána jediná metoda length():
type OpenShape interface { length() float64 }
Nyní můžeme napsat funkci (běžnou funkci), které se předá libovolná struktura implementující rozhraní OpenShape a tato funkce vrátí hodnotu získanou zavoláním metody OpenShape.length():
func length(shape OpenShape) float64 { return shape.length() }
func compute_open_shape_length(shape OpenShape) float64 { return shape.length() }
V šestém demonstračním příkladu se pokusíme funkci length() zavolat a předat jí strukturu/záznam Line:
package main import ( "fmt" "math" ) type OpenShape interface { length() float64 } type Line struct { x1, y1 float64 x2, y2 float64 } func length(shape OpenShape) float64 { return shape.length() } func main() { line1 := Line{x1: 0, y1: 0, x2: 100, y2: 100} fmt.Println(line1) line_length := length(line1) fmt.Println(line_length) }
Ovšem vzhledem k tomu, že struktura Line prozatím rozhraní OpenShape neimplementuje (v Go se mluví o tom, že struktura nevyhovuje rozhraní), nebude možné program spustit:
./06_interface_implementation.go:12:2: imported and not used: "math" ./06_interface_implementation.go:33:23: cannot use line1 (type Line) as type OpenShape in argument to length: Line does not implement OpenShape (missing length method)
7. Metody a rozhraní (struktura vyhovující rozhraní)
Co přesně tedy musíme udělat pro to, aby struktura Line vyhovovala (satisfy) rozhraní OpenShape a v něm předepsané metodě length()? Je toho překvapivě málo, protože jediné, co musíme udělat, je implementace metody length() s příjemcem Line. Tato implementace bude jednoduchá, protože metoda bude vracet délku úsečky, tj. vzdálenost mezi body [x1, y1] a [x2, y2]:
func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) }
Již vytvořením této metody jsme dosáhli toho, že Line bude vyhovovat rozhraní OpenShape! Tuto skutečnost si jazyk Go ověří jak při překladu, tak i po spuštění aplikace.
Korektní chování si otestujeme na tomto demonstračním příkladu:
package main import ( "fmt" "math" ) type OpenShape interface { length() float64 } type Line struct { x1, y1 float64 x2, y2 float64 } func length(shape OpenShape) float64 { return shape.length() } func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) } func main() { line1 := Line{x1: 0, y1: 0, x2: 100, y2: 100} fmt.Println(line1) line_length := length(line1) fmt.Println(line_length) }
Po spuštění tohoto příkladu dostaneme žádoucí výsledek:
{0 0 100 100} 141.4213562373095
Metody předepsané v rozhraní musí být implementovány zcela přesně, a to včetně návratového typu. Pokud typ nepatrně změníme (float32 → float64), nebude Line rozhraní OpenShape vyhovovat:
package main import ( "fmt" "math" ) type OpenShape interface { length() float32 } type Line struct { x1, y1 float64 x2, y2 float64 } func length(shape OpenShape) float32 { return shape.length() } func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) } func main() { line1 := Line{x1: 0, y1: 0, x2: 100, y2: 100} fmt.Println(line1) line_length := length(line1) fmt.Println(line_length) }
Pokus o překlad nyní skončí s chybou:
./07_B_wrong_return_type.go:37:23: cannot use line1 (type Line) as type OpenShape in argument to length: Line does not implement OpenShape (wrong type for length method) have length() float64 want length() float32
8. Vícenásobná implementace stejného rozhraní
Nyní si ukážeme poněkud složitější příklad, který je odvozen od klasického „školního“ příkladu s hierarchií geometrických tvarů. Budeme mít deklarována dvě rozhraní, jedno pro otevřené křivky (nebo chcete-li tvary) a druhé pro křivky uzavřené:
type OpenShape interface { length() float64 } type ClosedShape interface { area() float64 }
Dále vytvoříme dvě funkce, které budou akceptovat jakoukoli strukturu/záznam implementující dané rozhraní:
func length(shape OpenShape) float64 { return shape.length() } func area(shape ClosedShape) float64 { return shape.area() }
Následně již můžeme bez problémů první rozhraní implementovat konkrétní strukturou představující úsečku. Následující řádky plně dostačují, aby struktura Line vyhovovala prvnímu rozhraní OpenShape:
type Line struct { x1, y1 float64 x2, y2 float64 } func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) }
Dále vytvoříme tři struktury, které vyhovují druhému rozhraní ClosedShape:
type Circle struct { x, y float64 radius float64 } type Ellipse struct { x, y float64 a, b float64 } type Rectangle struct { x, y float64 width, height float64 } func (rect Rectangle) area() float64 { return rect.width * rect.height } func (circle Circle) area() float64 { return math.Pi * circle.radius * circle.radius } func (ellipse Ellipse) area() float64 { return math.Pi * ellipse.a * ellipse.b }
Vše si samozřejmě opět otestujeme:
package main import ( "fmt" "math" ) type OpenShape interface { length() float64 } type ClosedShape interface { area() float64 } func length(shape OpenShape) float64 { return shape.length() } func area(shape ClosedShape) float64 { return shape.area() } type Line struct { x1, y1 float64 x2, y2 float64 } type Circle struct { x, y float64 radius float64 } type Ellipse struct { x, y float64 a, b float64 } type Rectangle struct { x, y float64 width, height float64 } func (line Line) length() float64 { return math.Hypot(line.x1-line.x2, line.y1-line.y2) } func (rect Rectangle) area() float64 { return rect.width * rect.height } func (circle Circle) area() float64 { return math.Pi * circle.radius * circle.radius } func (ellipse Ellipse) area() float64 { return math.Pi * ellipse.a * ellipse.b } func main() { line1 := Line{x1: 0, y1: 0, x2: 100, y2: 100} fmt.Println("Line") fmt.Println(line1) fmt.Println(length(line1)) fmt.Println(line1.length()) fmt.Println() fmt.Println("Rectangle") r := Rectangle{x: 0, y: 0, width: 100, height: 100} fmt.Println(r) fmt.Println(area(r)) fmt.Println(r.area()) fmt.Println() fmt.Println("Circle") c := Circle{x: 0, y: 0, radius: 100} fmt.Println(c) fmt.Println(area(c)) fmt.Println(c.area()) fmt.Println() fmt.Println("Ellipse") e := Ellipse{x: 0, y: 0, a: 100, b: 50} fmt.Println(e) fmt.Println(area(e)) fmt.Println(e.area()) fmt.Println() }
Povšimněte si, že můžeme volat jak funkci length a area, tak i přímo metody Line.length(), Circle.area() atd.:
Line {0 0 100 100} 141.4213562373095 141.4213562373095 Rectangle {0 0 100 100} 10000 10000 Circle {0 0 100} 31415.926535897932 31415.926535897932 Ellipse {0 0 100 50} 15707.963267948966 15707.963267948966
9. Řez se strukturami implementujícími společné rozhraní
Vzhledem k tomu, že se s využitím rozhraní může v programovacím jazyce Go realizovat polymorfní chování, asi nás nepřekvapí, že je povoleno vytvářet pole popř. řezy (rozdílných) struktur implementujících stejné rozhraní. Následující zápis je tak zcela korektní, protože všechny tři typy struktur (Rectangle, Circle i Ellipse) implementují rozhraní ClosedShape, což je překladačem samozřejmě kontrolováno:
shapes := []ClosedShape{ Rectangle{x: 0, y: 0, width: 100, height: 100}, Circle{x: 0, y: 0, radius: 100}, Ellipse{x: 0, y: 0, a: 100, b: 50}}
V předchozím úryvku byl vytvořen řez se třemi strukturami, ovšem můžeme snadno vytvořit i pole:
shapes := [...]ClosedShape{ Rectangle{x: 0, y: 0, width: 100, height: 100}, Circle{x: 0, y: 0, radius: 100}, Ellipse{x: 0, y: 0, a: 100, b: 50}}
Následně je možné iterovat přes všechny prvky pole či řezu a volat buď přímo implementované metody nebo funkce akceptující jako svůj parametr jakýkoli typ vyhovující rozhraní ClosedShape:
for _, shape := range shapes { fmt.Println(shape) fmt.Println(area(shape)) fmt.Println(shape.area()) fmt.Println() }
Podívejme se nyní na konkrétní realizaci výše popsaného polymorfního chování:
package main import ( "fmt" "math" ) type ClosedShape interface { area() float64 } func area(shape ClosedShape) float64 { return shape.area() } type Circle struct { x, y float64 radius float64 } type Ellipse struct { x, y float64 a, b float64 } type Rectangle struct { x, y float64 width, height float64 } func (rect Rectangle) area() float64 { return rect.width * rect.height } func (circle Circle) area() float64 { return math.Pi * circle.radius * circle.radius } func (ellipse Ellipse) area() float64 { return math.Pi * ellipse.a * ellipse.b } func main() { shapes := []ClosedShape{ Rectangle{x: 0, y: 0, width: 100, height: 100}, Circle{x: 0, y: 0, radius: 100}, Ellipse{x: 0, y: 0, a: 100, b: 50}} for _, shape := range shapes { fmt.Println(shape) fmt.Println(area(shape)) fmt.Println(shape.area()) fmt.Println() } }
Po spuštění příkladu se vypíšou informace o třech strukturách – souřadnice, plocha a znovu plocha (pokaždé se ovšem volá jiná funkce resp. metoda):
{0 0 100 100} 10000 10000 {0 0 100} 31415.926535897932 31415.926535897932 {0 0 100 50} 15707.963267948966 15707.963267948966
10. Řezy rozhraní jako parametry funkce
Dynamické chování programovacího jazyka Go (alespoň v oblasti, v níž se používají rozhraní) jde samozřejmě ještě dále, protože můžeme deklarovat funkci akceptující řez rozhraním, tj. v praxi řez obsahující struktury vyhovující specifikovanému rozhraní. Taková funkce může vypadat například následovně:
func print_areas(shapes []ClosedShape) { for _, shape := range shapes { fmt.Println(shape) fmt.Println(area(shape)) fmt.Println(shape.area()) fmt.Println() } }
Tuto funkci lze zavolat a přitom jí předat následující řez:
shapes := []ClosedShape{ Rectangle{x: 0, y: 0, width: 100, height: 100}, Circle{x: 0, y: 0, radius: 100}, Ellipse{x: 0, y: 0, a: 100, b: 50}} print_areas(shapes)
Opět se podívejme na ucelený demonstrační příklad, v němž se výše zmíněná funkce používá:
package main import ( "fmt" "math" ) type ClosedShape interface { area() float64 } func area(shape ClosedShape) float64 { return shape.area() } type Circle struct { x, y float64 radius float64 } type Ellipse struct { x, y float64 a, b float64 } type Rectangle struct { x, y float64 width, height float64 } func (rect Rectangle) area() float64 { return rect.width * rect.height } func (circle Circle) area() float64 { return math.Pi * circle.radius * circle.radius } func (ellipse Ellipse) area() float64 { return math.Pi * ellipse.a * ellipse.b } func print_areas(shapes []ClosedShape) { for _, shape := range shapes { fmt.Println(shape) fmt.Println(area(shape)) fmt.Println(shape.area()) fmt.Println() } } func main() { shapes := []ClosedShape{ Rectangle{x: 0, y: 0, width: 100, height: 100}, Circle{x: 0, y: 0, radius: 100}, Ellipse{x: 0, y: 0, a: 100, b: 50}} print_areas(shapes) }
Výsledek po spuštění tohoto příkladu bude shodný s výsledkem příkladu předchozího:
{0 0 100 100} 10000 10000 {0 0 100} 31415.926535897932 31415.926535897932 {0 0 100 50} 15707.963267948966 15707.963267948966
11. Datový typ implementující větší množství rozhraní
Datová struktura (záznam) samozřejmě může vyhovovat většímu množství rozhraní, což si můžeme snadno ukázat na struktuře pojmenované dosti obecně Type, která sice neobsahuje žádné položky, což však vůbec nevadí:
type Type struct{}
V programu je dále deklarována dvojice rozhraní:
type Interface1 interface { method1() } type Interface2 interface { method2() }
Nyní postačuje implementovat obě metody, aby Type vyhovovala oběma rozhraním:
func (Type) method1() { fmt.Println("Type.method1") } func (Type) method2() { fmt.Println("Type.method2") }
Podívejme se nyní na příklad, v němž jsou tato rozhraní deklarována a použita:
package main import "fmt" type Interface1 interface { method1() } type Interface2 interface { method2() } type Type struct{} func (Type) method1() { fmt.Println("Type.method1") } func (Type) method2() { fmt.Println("Type.method2") } func f1(i Interface1) { fmt.Println("Interface1.f1") i.method1() } func f2(i Interface2) { fmt.Println("Interface2.f2") i.method2() } func main() { t := Type{} t.method1() t.method2() fmt.Println() f1(t) fmt.Println() f2(t) fmt.Println() }
Povšimněte si, že můžeme bez problémů volat i funkce f1 a f2 a přitom jim předat strukturu typu t:
Type.method1 Type.method2 Interface1.f1 Type.method1 Interface2.f2 Type.method2
12. Skládání rozhraní
Další užitečnou vlastností typového systému programovacího jazyka Go je možnost při deklaraci nového rozhraní použít již existující rozhraní (této operaci se říká kompozice nebo též embedding, vložení). Díky tomu, že se v rozhraní nachází pouze deklarace hlaviček metod a nikoli jejich těla, nedojde při skládání k žádným nežádoucím a potenciálně nejednoznačným stavům (na rozdíl od vícenásobné dědičnosti). Podívejme se, jak lze rozhraní pojmenované Interface2 vytvořit a použít přitom již existující rozhraní se jménem Interface1:
package main import "fmt" type Interface1 interface { method1() } type Interface2 interface { Interface1 method2() } type Type struct{} func (Type) method1() { fmt.Println("Type.method1") } func (Type) method2() { fmt.Println("Type.method2") } func f1(i Interface1) { fmt.Println("Interface1.f1") i.method1() } func f2(i Interface2) { fmt.Println("Interface2.f2") i.method2() } func main() { t := Type{} t.method1() t.method2() fmt.Println() f1(t) fmt.Println() f2(t) fmt.Println() }
Chování příkladu po jeho spuštění:
Type.method1 Type.method2 Interface1.f1 Type.method1 Interface2.f2 Type.method2
13. Gorutiny
Ve třetí části článku si ukážeme způsob vytvoření a zavolání takzvaných gorutin, o nichž jsme se zmínili v úvodní kapitole. Vytvoření gorutiny je ve skutečnosti velmi snadné. Ve chvíli, kdy například máme deklarovanou funkci message():
func message(id int) { fmt.Printf("gorutina %d\n", id) }
Můžeme tuto funkci spustit přímo (synchronně):
message(1)
nebo nepřímo (asynchronně):
go message(1)
Jediným rozdílem je použití klíčového slova go.
V některých případech se setkáme s tím, že je implementace gorutiny uvedena přímo za klíčovým slovem go, což je samozřejmě možné, protože funkce jsou plnohodnotným datovým typem:
go func(msg string) { fmt.Println(msg) }("Hello world")
14. Spuštění gorutiny
Gorutinu samozřejmě můžeme spustit přímo z hlavního vlákna aplikace:
package main import "fmt" func message(id int) { fmt.Printf("gorutina %d\n", id) } func main() { fmt.Println("main begin") go message(1) go message(2) fmt.Println("main end") }
Po spuštění tohoto příkladu však s velkou pravděpodobností vůbec neuvidíme hlášení vypsané oběma gorutinami. Je tomu tak z toho důvodu, že hlavní vlákno s funkcí main skončí ještě předtím, než dojde k inicializaci a spuštění gorutin.
Jedno z řešení (ne příliš dobré!) spočívá v tom, že v hlavním vláknu počkáme na dokončení gorutin. Naše gorutiny jsou tak krátké a doslova primitivní, že plně postačuje počkat „pouze“ dvě sekundy:
package main import ( "fmt" "time" ) func message(id int) { fmt.Printf("gorutina %d\n", id) } func main() { fmt.Println("main begin") go message(1) go message(2) time.Sleep(2 * time.Second) fmt.Println("main end") }
Výsledek po spuštění:
main begin gorutina 1 gorutina 2 main end
15. Spuštění většího množství gorutin na pozadí
Můžeme si samozřejmě vyzkoušet složitější příklady, například trojici gorutin, které zapisují řetězec na jediný řádek s různým zpožděním:
package main import ( "fmt" "time" ) func print_chars() { for ch := 'a'; ch <= 'z'; ch++ { fmt.Printf("%c", ch) time.Sleep(200 * time.Millisecond) } } func print_dots() { for i := 0; i < 30; i++ { fmt.Print(".") time.Sleep(200 * time.Millisecond) } } func print_spaces() { for i := 0; i < 60; i++ { fmt.Print(" ") time.Sleep(110 * time.Millisecond) } } func main() { fmt.Println("main begin") go print_chars() go print_spaces() go print_dots() time.Sleep(6 * time.Second) fmt.Println("main end") }
Výsledek může (ale nemusí) vypadat následovně:
main begin a. b. .c .d e. .f g. .h i. j. .k l. .m n. o. p. .q .r .s t. u. .v .w .x .y z. . . . . main end
Gorutiny lze volat z jiných gorutin:
package main import ( "fmt" "time" ) func print_chars() { for ch := 'a'; ch <= 'z'; ch++ { fmt.Printf("%c", ch) time.Sleep(200 * time.Millisecond) } } func print_dots() { for i := 0; i < 30; i++ { fmt.Print(".") time.Sleep(200 * time.Millisecond) } } func print_spaces() { go print_chars() go print_dots() for i := 0; i < 60; i++ { fmt.Print(" ") time.Sleep(110 * time.Millisecond) } } func main() { fmt.Println("main begin") go print_spaces() time.Sleep(6 * time.Second) fmt.Println("main end") }
16. Kanály – technologie pro komunikaci mezi gorutinami
Knihovny či nové jazykové konstrukce umožňující používání kanálů (či front) pro asynchronní komunikaci mezi různými částmi vyvíjených aplikací, se v posledních několika letech těší poměrně velké popularitě. Ta je způsobena dvěma faktory. První důvod spočívá ve snaze o zjednodušení návrhu (či porozumění) vyvíjené aplikace, zejména ve chvíli, kdy se v rámci jednoho programu předávají data (resp. objekty) mezi částmi, jejichž funkce může být dobře izolována od částí ostatních.
Druhý důvod je poněkud prozaičtější – v některých situacích je nutné dosáhnout zvýšení efektivity aplikace (například zvýšit počet odpovědí, které může server vygenerovat za určitou časovou jednotku) a přitom není možné či vhodné využívat řešení založené na použití většího množství vláken spravovaných přímo operačním systémem. Naprosto typickým příkladem jsou virtuální stroje JavaScriptu, které povětšinou umožňují běh aplikace v jediném vláknu (což je ovšem s ohledem na „kvalitu“ některých programových kódů spíše výhodou…).
Některé programovací jazyky, zejména pak v tomto seriálu popisovaný jazyk Go, obsahují prostředky sloužící pro zajištění asynchronní komunikace přímo v syntaxi (a samozřejmě též v sémantice) jazyka. Konkrétně v případě jazyka Go se jedná o gorutiny představené v předchozích kapitolách, které jsou doplněny o specializované operace sloužící pro zápis či čtení dat z kanálů. Tyto specializované operace jsou představovány operátorem <- (ten má dva významy v závislosti na tom, zda je před operátorem uveden identifikátor představující kanál či nikoli).
Typickým příkladem, v němž se použije kanál, je komunikace mezi producentem a konzumentem:
+-----------+ +-----------+ | producent | +-----+ | konzument | | |... > ... |kanál} ... > ... | | | go block | +-----+ | go block | +-----------+ +-----------+
Kanál může být v případě potřeby využíván více producenty (i konzumenty), takže se původní schéma komunikace může změnit:
+-----------+ | producent | | #1 |... >......... | go block | : +-----------+ : : +-----------+ : +-----------+ | producent | +-----+ | konzument | | #2 |... > ... |kanál} ... < ... | | | go block | +-----+ | go block | +-----------+ : +-----------+ : +-----------+ : | producent | : | #3 |... >........: | go block | +-----------+
Ukažme si nyní velmi jednoduchý příklad použití kanálu při čekání na dokončení gorutiny. Jedná se o mnohem čistší řešení problému, s nímž jsme se setkali v předchozích kapitolách. Nyní je gorutině předán kanál (s kapacitou jediného čísla typu int) a kód ve funkci main nejprve gorutinu zavolá a posléze čeká na hodnotu zapsanou do kanálu. Toto čekání je blokující – hlavní blok tedy skutečně bude v daném místě čekat na operaci zápisu do kanálu. Kromě vlastní hodnoty se při čtení z kanálu získá i jeho stav, tj. zda je kanál stále otevřen a připraven pro komunikaci:
package main import "fmt" func message(id int, channel chan int) { fmt.Printf("gorutina %d\n", id) // zápis libovolné hodnoty do kanálu channel <- 1 } func main() { channel := make(chan int) fmt.Println("main begin") go message(1, channel) fmt.Println("waiting...") // blokující čtení z kanálu code, status := <-channel fmt.Printf("received code: %d and status: %t\n", code, status) fmt.Println("main end") }
17. Implementace workerů s využitím gorutin a kanálů
Prozatím bez podrobnějšího popisu si ukažme, jak lze implementovat jednoduchý systém s workery s využitím gorutin a kanálů. Workerům se úlohy předávají přes kanál task_channel, druhý kanál slouží pro jejich ukončení:
package main import "fmt" func worker(task_channel chan int, worker_done chan bool) { fmt.Println("worker started") for { value, more := <-task_channel if more { fmt.Printf("worker received task with parameter %d\n", value) } else { fmt.Println("finishing worker") worker_done <- true fmt.Println("worker finished") return } } } func main() { task_channel := make(chan int) worker_done := make(chan bool) fmt.Println("main begin") go worker(task_channel, worker_done) for i := 1; i <= 10; i++ { fmt.Printf("sending task with parameter %d\n", i) task_channel <- i } //close(task_channel) fmt.Println("waiting for workers...") code, status := <-worker_done fmt.Printf("received code: %t and status: %t\n", code, status) fmt.Println("main end") }
Práce jednoho workera:
main begin sending task with parameter 1 worker started worker received task with parameter 1 sending task with parameter 2 sending task with parameter 3 worker received task with parameter 2 worker received task with parameter 3 sending task with parameter 4 sending task with parameter 5 worker received task with parameter 4 worker received task with parameter 5 sending task with parameter 6 sending task with parameter 7 worker received task with parameter 6 worker received task with parameter 7 sending task with parameter 8 sending task with parameter 9 worker received task with parameter 8 worker received task with parameter 9 sending task with parameter 10 waiting for workers... worker received task with parameter 10 finishing worker worker finished received code: true and status: true main end
Workerů můžeme spustit více, zde konkrétně tři:
package main import ( "fmt" "time" ) func worker(id int, task_channel chan int, worker_done chan bool) { fmt.Printf("worker %d started\n", id) for { value, more := <-task_channel if more { fmt.Printf("worker %d received task with parameter %d\n", id, value) time.Sleep(2 * time.Second) } else { fmt.Printf("finishing worker %d\n", id) worker_done <- true fmt.Printf("worker %d finished\n", id) return } } } func main() { task_channel := make(chan int) worker_done := make(chan bool) fmt.Println("main begin") for i := 1; i <= 3; i++ { go worker(i, task_channel, worker_done) } time.Sleep(2 * time.Second) for i := 1; i <= 10; i++ { fmt.Printf("sending task with parameter %d\n", i) task_channel <- i } close(task_channel) fmt.Println("waiting for workers...") code, status := <-worker_done fmt.Printf("received code: %t and status: %t\n", code, status) fmt.Println("main end") }
Nyní se workeři o práci poměrně spravedlivě dělí:
main begin worker 1 started worker 2 started worker 3 started sending task with parameter 1 sending task with parameter 2 sending task with parameter 3 sending task with parameter 4 worker 2 received task with parameter 2 worker 1 received task with parameter 1 worker 3 received task with parameter 3 worker 3 received task with parameter 4 sending task with parameter 5 sending task with parameter 6 sending task with parameter 7 worker 2 received task with parameter 5 worker 1 received task with parameter 6 worker 3 received task with parameter 7 sending task with parameter 8 worker 1 received task with parameter 8 sending task with parameter 9 sending task with parameter 10 worker 2 received task with parameter 9 worker 3 received task with parameter 10 waiting for workers... finishing worker 1 worker 1 finished received code: true and status: true main end
18. Deadlock a jeho detekce v runtime
Kanály, které jsme až doposud pro komunikaci mezi gorutinami používali, mohly obsahovat pouze jediný prvek, takže se jejich chování dá shrnout přibližně takto:
- Zápis do prázdného kanálu je neblokující operace (kanál se zápisem zaplní, další zápis již tedy bude blokující, pokud mezitím nedojde ke čtení).
- Zápis do neprázdného kanálu je blokující operace.
- Čtení z prázdného kanálu je blokující operace (počká se na další zápis).
- Čtení z neprázdného kanálu je neblokující operace (kanál se vyprázdní).
V praxi však může dojít i k problémové situaci, a to ve chvíli, kdy použijeme větší množství kanálů, přičemž dvě gorutiny budou navzájem čekat na zápis provedený do různých kanálů. V takovém případě dojde k takzvanému deadlocku, což si můžeme relativně snadno otestovat:
package main import "fmt" func worker(task_channel chan int, worker_done chan bool) { fmt.Println("worker started") for { value, more := <-task_channel if more { fmt.Printf("worker received task with parameter %d\n", value) } else { fmt.Println("finishing worker") worker_done <- true fmt.Println("worker finished") return } } } func main() { task_channel := make(chan int) worker_done := make(chan bool) fmt.Println("main begin") go worker(task_channel, worker_done) for i := 1; i <= 10; i++ { fmt.Printf("sending task with parameter %d\n", i) task_channel <- i } // !!! // close(task_channel) // !!! fmt.Println("waiting for workers...") code, status := <-worker_done fmt.Printf("received code: %t and status: %t\n", code, status) fmt.Println("main end") }
Kanál task_channel není uzavřen, tudíž workeři nikdy neskončí a budou čekat na data, která nedostanou:
main begin sending task with parameter 1 worker started worker received task with parameter 1 sending task with parameter 2 sending task with parameter 3 worker received task with parameter 2 worker received task with parameter 3 sending task with parameter 4 sending task with parameter 5 worker received task with parameter 4 worker received task with parameter 5 sending task with parameter 6 sending task with parameter 7 worker received task with parameter 6 worker received task with parameter 7 sending task with parameter 8 sending task with parameter 9 worker received task with parameter 8 worker received task with parameter 9 sending task with parameter 10 waiting for workers... worker received task with parameter 10 fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: main.main() /home/tester/temp/out/go-root/article_04/20_workers_deadlock.go:45 +0x1f1 goroutine 5 [chan receive]: main.worker(0xc00005e060, 0xc00005e0c0) /home/tester/temp/out/go-root/article_04/20_workers_deadlock.go:15 +0xf2 created by main.main /home/tester/temp/out/go-root/article_04/20_workers_deadlock.go:33 +0xed exit status 2
19. Repositář s demonstračními příklady
Zdrojové kódy všech dnes popsaných demonstračních příkladů byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má doslova několik kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:
20. Odkazy na Internetu
- The Go Programming Language (home page)
https://golang.org/ - GoDoc
https://godoc.org/ - Go (programming language), Wikipedia
https://en.wikipedia.org/wiki/Go_(programming_language) - Go Books (kniha o jazyku Go)
https://github.com/dariubs/GoBooks - The Go Programming Language Specification
https://golang.org/ref/spec - Go: the Good, the Bad and the Ugly
https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/ - Package builtin
https://golang.org/pkg/builtin/ - Package fmt
https://golang.org/pkg/fmt/ - The Little Go Book (další kniha)
https://github.com/dariubs/GoBooks - The Go Programming Language by Brian W. Kernighan, Alan A. A. Donovan
https://www.safaribooksonline.com/library/view/the-go-programming/9780134190570/ebook_split010.html - Learning Go
https://www.miek.nl/go/ - Go Bootcamp
http://www.golangbootcamp.com/ - Programming in Go: Creating Applications for the 21st Century (další kniha o jazyku Go)
http://www.informit.com/store/programming-in-go-creating-applications-for-the-21st-9780321774637 - Introducing Go (Build Reliable, Scalable Programs)
http://shop.oreilly.com/product/0636920046516.do - Learning Go Programming
https://www.packtpub.com/application-development/learning-go-programming - The Go Blog
https://blog.golang.org/ - Getting to Go: The Journey of Go's Garbage Collector
https://blog.golang.org/ismmkeynote - Go (programovací jazyk, Wikipedia)
https://cs.wikipedia.org/wiki/Go_(programovac%C3%AD_jazyk) - Rychle, rychleji až úplně nejrychleji s jazykem Go
https://www.root.cz/clanky/rychle-rychleji-az-uplne-nejrychleji-s-jazykem-go/ - Installing Go on the Raspberry Pi
https://dave.cheney.net/2012/09/25/installing-go-on-the-raspberry-pi - How the Go runtime implements maps efficiently (without generics)
https://dave.cheney.net/2018/05/29/how-the-go-runtime-implements-maps-efficiently-without-generics - Niečo málo o Go – Golang (slovensky)
http://golangsk.logdown.com/ - How Many Go Developers Are There?
https://research.swtch.com/gophercount - Most Popular Technologies (Stack Overflow Survery 2018)
https://insights.stackoverflow.com/survey/2018/#most-popular-technologies - Most Popular Technologies (Stack Overflow Survery 2017)
https://insights.stackoverflow.com/survey/2017#technology - The Go Programming Language: Release History
https://golang.org/doc/devel/release.html - Go 1.11 Release Notes
https://golang.org/doc/go1.11 - Go 1.10 Release Notes
https://golang.org/doc/go1.10 - Go 1.9 Release Notes (tato verze je stále používána)
https://golang.org/doc/go1.9 - Go 1.8 Release Notes (i tato verze je stále používána)
https://golang.org/doc/go1.8 - Go on Fedora
https://developer.fedoraproject.org/tech/languages/go/go-installation.html - Writing Go programs
https://developer.fedoraproject.org/tech/languages/go/go-programs.html - The GOPATH environment variable
https://tip.golang.org/doc/code.html#GOPATH - Command gofmt
https://tip.golang.org/cmd/gofmt/ - The Go Blog: go fmt your code
https://blog.golang.org/go-fmt-your-code - C? Go? Cgo!
https://blog.golang.org/c-go-cgo - Spaces vs. Tabs: A 20-Year Debate Reignited by Google’s Golang
https://thenewstack.io/spaces-vs-tabs-a-20-year-debate-and-now-this-what-the-hell-is-wrong-with-go/ - 400,000 GitHub repositories, 1 billion files, 14 terabytes of code: Spaces or Tabs?
https://medium.com/@hoffa/400–000-github-repositories-1-billion-files-14-terabytes-of-code-spaces-or-tabs-7cfe0b5dd7fd - Gofmt No Longer Allows Spaces. Tabs Only
https://news.ycombinator.com/item?id=7914523 - Why does Go „go fmt“ uses tabs instead of whitespaces?
https://www.quora.com/Why-does-Go-go-fmt-uses-tabs-instead-of-whitespaces - Interactive: The Top Programming Languages 2018
https://spectrum.ieee.org/static/interactive-the-top-programming-languages-2018 - Go vs. Python
https://www.peterbe.com/plog/govspy - PackageManagementTools
https://github.com/golang/go/wiki/PackageManagementTools - A Tour of Go: Type inference
https://tour.golang.org/basics/14 - Go Slices: usage and internals
https://blog.golang.org/go-slices-usage-and-internals - Go by Example: Slices
https://gobyexample.com/slices - What is the point of slice type in Go?
https://stackoverflow.com/questions/2098874/what-is-the-point-of-slice-type-in-go - The curious case of Golang array and slices
https://medium.com/@hackintoshrao/the-curious-case-of-golang-array-and-slices-2565491d4335 - Introduction to Slices in Golang
https://www.callicoder.com/golang-slices/ - Golang: Understanding ‚null‘ and nil
https://newfivefour.com/golang-null-nil.html - What does nil mean in golang?
https://stackoverflow.com/questions/35983118/what-does-nil-mean-in-golang - nils In Go
https://go101.org/article/nil.html - Go slices are not dynamic arrays
https://appliedgo.net/slices/ - Go-is-no-good (nelze brát doslova)
https://github.com/ksimka/go-is-not-good - Rust vs. Go
https://news.ycombinator.com/item?id=13430108 - Seriál Programovací jazyk Rust
https://www.root.cz/serialy/programovaci-jazyk-rust/ - Modern garbage collection: A look at the Go GC strategy
https://blog.plan99.net/modern-garbage-collection-911ef4f8bd8e - Go GC: Prioritizing low latency and simplicity
https://blog.golang.org/go15gc - Is Golang a good language for embedded systems?
https://www.quora.com/Is-Golang-a-good-language-for-embedded-systems - Running GoLang on an STM32 MCU. A quick tutorial.
https://www.mickmake.com/post/running-golang-on-an-mcu-a-quick-tutorial - Go, Robot, Go! Golang Powered Robotics
https://gobot.io/ - Emgo: Bare metal Go (language for programming embedded systems)
https://github.com/ziutek/emgo - UTF-8 history
https://www.cl.cam.ac.uk/~mgk25/ucs/utf-8-history.txt - Less is exponentially more
https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html - Should I Rust, or Should I Go
https://codeburst.io/should-i-rust-or-should-i-go-59a298e00ea9 - Setting up and using gccgo
https://golang.org/doc/install/gccgo - Elastic Tabstops
http://nickgravgaard.com/elastic-tabstops/ - Strings, bytes, runes and characters in Go
https://blog.golang.org/strings - Datový typ
https://cs.wikipedia.org/wiki/Datov%C3%BD_typ - Seriál o programovacím jazyku Rust: Základní (primitivní) datové typy
https://www.root.cz/clanky/programovaci-jazyk-rust-nahrada-c-nebo-slepa-cesta/#k09 - Seriál o programovacím jazyku Rust: Vytvoření „řezu“ z pole
https://www.root.cz/clanky/prace-s-poli-v-programovacim-jazyku-rust/#k06 - Seriál o programovacím jazyku Rust: Řezy (slice) vektoru
https://www.root.cz/clanky/prace-s-vektory-v-programovacim-jazyku-rust/#k05 - Printf Format Strings
https://www.cprogramming.com/tutorial/printf-format-strings.html - Java: String.format
https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#format-java.lang.String-java.lang.Object…- - Java: format string syntax
https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html#syntax - Selectors
https://golang.org/ref/spec#Selectors - Calling Go code from Python code
http://savorywatt.com/2015/09/18/calling-go-code-from-python-code/ - Go Data Structures: Interfaces
https://research.swtch.com/interfaces - How to use interfaces in Go
http://jordanorelli.com/post/32665860244/how-to-use-interfaces-in-go - Interfaces in Go (part I)
https://medium.com/golangspec/interfaces-in-go-part-i-4ae53a97479c - Part 21: Goroutines
https://golangbot.com/goroutines/ - Part 22: Channels
https://golangbot.com/channels/ - [Go] Lightweight eventbus with async compatibility for Go
https://github.com/asaskevich/EventBus - What about Trait support in Golang?
https://www.reddit.com/r/golang/comments/8mfykl/what_about_trait_support_in_golang/ - Don't Get Bitten by Pointer vs Non-Pointer Method Receivers in Golang
https://nathanleclaire.com/blog/2014/08/09/dont-get-bitten-by-pointer-vs-non-pointer-method-receivers-in-golang/