Hlavní navigace

Užitečné balíčky pro každodenní použití Go (2), porovnání výkonnosti Go s céčkem

Pavel Tišnovský

Dnes si popíšeme další užitečné funkce a metody ze standardních balíčků. Ve druhé části článku porovnáme kvalitu překladače Go v porovnání s ANSI C (překladač GCC) a vliv paralelizace kódu pomocí gorutin.

Doba čtení: 38 minut

Sdílet

11. Kvalita překladače jazyka Go při optimalizacích

12. Vzorový benchmark naprogramovaný v ANSI C

13. První varianta benchmarku přímo přepsaná do Go

14. Vliv bufferu na rychlost dokončení benchmarku

15. Druhá verze benchmarku: použití bufferovaného výstupu

16. Třetí verze benchmarku: výpočet po jednotlivých obrazových řádcích

17. Finální verze benchmarku: využití kanálů a gorutin pro paralelní výpočty

18. Porovnání výsledků všech benchmarků

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Užitečné balíčky pro každodenní použití Go (2), porovnání výkonnosti Go s céčkem

Dnešní díl seriálu o programovacím jazyku Go je rozdělen na dvě části. V první polovině se budeme převážně věnovat dalším balíčkům, které se vývojářům mohou hodit v každodenní praxi. Ve druhé části si ukážeme jeden benchmark zaměřený jak na optimalizaci počítaných programových smyček a výpočtů s hodnotami typu float64, tak i na základní vstupně-výstupní operace. Porovnáme si dvě varianty benchmarku naprogramovaných v ANSI C s několika implementacemi vytvořenými přímo v programovacím jazyku Go. Na závěr si ukážeme, jakým způsobem lze benchmark vytvořený v Go relativně snadno přepsat do paralelní podoby s využitím nám již známých gorutin a kanálů (což samozřejmě není příliš férové v porovnání s jednovláknovou céčkovskou implementací, ovšem „paralelizace“ v Go je – na rozdíl od programovacího jazyka C – velmi snadná a, což je neméně důležité, i poměrně přímočará).

2. Zpracování argumentů předaných na příkazové řádce

Programovací jazyk Go se mj. používá i pro tvorbu různých nástrojů spouštěných z příkazového řádku. Typicky se těmto nástrojům musí předávat nějaké argumenty, takže si dnes ukážeme, jakým způsobem se tyto argumenty přečtou a zpracují. Jen pro připomenutí si nejprve ukažme, jak se s argumenty předávanými na příkazovém řádku pracuje v programovacím jazyku C. Předpokládejme, že se aplikaci mají předat tři celočíselné argumenty:

int main(int argc, char **argv)
{
    if (argc < 4) {
        puts("usage: ./mandelbrot width height maxiter");
        return 1;
    }
    int width = atoi(argv[1]);
    int height = atoi(argv[2]);
    int maxiter = atoi(argv[3]);
    ...
    ...
    ...

Z předchozího příkladu je patrné, že v programovacím jazyku C se informace o případných argumentech zapsaných na příkazovém řádku předávají přímo do funkce main, která je vstupním bodem aplikace (je automaticky zavolána runtime systémem). V prvním parametru nazvaném argc je uložen celkový počet argumentů a druhý parametr pojmenovaný argv obsahuje ukazatel na pole řetězců s argumenty (řetězce jsou v poli taktéž představovány ukazateli typu char*, což je v céčku obvyklý způsob reprezentace řetězců). Důležité je, že v poli argv se v prvním prvku (s indexem 0) nachází název spuštěného programu. Díky tomu je možné „simulovat“ větší množství programů s různým chováním, které jsou ovšem uloženy ve stejném binárním souboru – postačuje spustit symbolický ukazatel na tento soubor, protože řetězec argv[0] bude obsahovat právě jméno symlinku (takto se například rozlišují příkazy vi, vim a vimdiff reprezentované jediným binárním souborem a dvěma symlinky).

V programovacím jazyku Go se s argumenty zapsanými na příkazový řádek aplikace pracuje poněkud odlišným způsobem. Všechny argumenty (včetně jména spuštěného programu) jsou uloženy do pole os.Args, což mj. znamená, že nejdříve musíme importovat balíček os. Pole os.Args je typu:

var Args []string

což z pohledu vývojáře značí, že s jeho prvky můžeme pracovat jako s běžnými řetězci. To je ostatně ukázáno i v dnešním prvním demonstračním příkladu, jehož zdrojový kód naleznete na adrese https://github.com/tisnik/go-fedora/blob/master/article10/01_cmdli­ne_params.go. Po spuštění tohoto příkladu se nejdříve vypíše celkový počet argumentů a posléze i jejich hodnoty:

package main
 
import (
        "fmt"
        "os"
)
 
func main() {
        fmt.Printf("Arguments: %d\n", len(os.Args))
 
        for index, element := range os.Args {
                fmt.Printf("Argument #%d = %s\n", index, element)
        }
}

Samozřejmě si ukážeme použití tohoto jednoduchého příkladu v praxi.

Spuštění bez argumentů:

$ go run 01_cmdline_params.go
 
Arguments: 1
Argument #0 = /tmp/go-build447690084/b001/exe/01_cmdline_params

Spuštění s předáním tří argumentů:

$ go run 01_cmdline_params.go foo bar baz
 
Arguments: 4
Argument #0 = /tmp/go-build652687078/b001/exe/01_cmdline_params
Argument #1 = foo
Argument #2 = bar
Argument #3 = baz
Poznámka: v tomto případě je v prvním prvku pole os.Args uloženo jméno dočasně vytvořeného spustitelného souboru, protože jsme použili příkaz go run a nikoli go build.

3. Standardní balíček flag

Přímá práce s výše popsaným polem os.Args je v praxi vhodná jen ve chvíli, kdy aplikace akceptuje pouze několik argumentů, které jsou navíc umístěny na pevné pozici (například může první argument obsahovat jméno vstupního souboru a druhý argument jméno souboru výstupního). Pokud však má aplikace podporovat různé přepínače, pojmenované argumenty, argumenty s krátkou a dlouhou variantou zápisu (-h, –help) apod., tak je lepší namísto ručního procházení polem os.Args využít možností poskytovaných standardním balíčkem nazvaným flag. S tímto balíčkem se pracuje následujícím způsobem:

  1. Nejdříve se s využitím funkcí flag.InvVar, flag.UintVar, flag.BoolVar, flag.Float64Var atd. zaregistrují jména argumentů očekávaných na příkazové řádce, jejich typy (řetězec, celé číslo, logický přepínač, …), výchozí hodnoty a taktéž ukazatele na proměnné, které se mají naplnit skutečně zapsanými hodnotami argumentů.
  2. Posléze se zavolá funkce flag.Parse, která argumenty zadané na příkazové řádce zpracuje a naplní příslušné proměnné (z tohoto důvodu se registrují ukazatele na proměnné).

Ukažme si nyní jednoduché základní použití balíčku flag i s příslušným komentářem:

// deklarace běžné proměnné typu int
var width int
 
// specifikace, že na příkazovém řádku očekáváme argument se jménem width
// hodnotou tohoto argumentu má být celé číslo s výchozí hodnotou 0
// po zpracování a převodu na celé číslo se má výsledek uložit do proměnné width
flag.IntVar(&width, "width", 0, "image width")
 
// vlastní zpracování argumentů předaných na příkazové řádce
flag.Parse()
 
// výpis výsledku
fmt.Printf("width: %d\n", width)

Následuje poněkud obsáhlejší demonstrační příklad, v němž jsou specifikovány čtyři argumenty, z toho dva celočíselné, jeden je typu boolean (buď je použit či nikoli) a poslední argument může obsahovat libovolný řetězec:

package main
 
import (
        "flag"
        "fmt"
)
 
func main() {
        var width int
        var height int
        var aa bool
        var output string
 
        flag.IntVar(&width, "width", 0, "image width")
        flag.IntVar(&height, "height", 0, "image height")
        flag.BoolVar(&aa, "aa", false, "enable antialiasing")
        flag.StringVar(&output, "output", "", "output file name")
 
        flag.Parse()
 
        fmt.Printf("width: %d\n", width)
        fmt.Printf("height: %d\n", height)
        fmt.Printf("antialiasing: %t\n", aa)
        fmt.Printf("output file name: %s\n", output)
}

Spuštění tohoto příkladu bez argumentů:

$ go run 02_flags.go 
 
width: 0
height: 0
antialiasing: false
output file name:

Spuštění příkladu s argumenty:

$ go run 02_flags.go -width=320 -height=200 -aa -output=xyzzy
 
width: 320
height: 200
antialiasing: true
output file name: xyzzy

Balíček flag dokonce dokáže automaticky vygenerovat nápovědu na základě zaregistrovaných argumentů:

$ go run 02_flags.go -h
 
Usage of /tmp/go-build202978013/b001/exe/02_flags:
  -aa
        enable antialiasing
  -height int
        image height
  -output string
        output file name
  -width int
        image width
exit status 2

Popř. můžeme použít dlouhou variantu –help:

$ go run 02_flags.go --help
 
Usage of /tmp/go-build109677334/b001/exe/02_flags:
  -aa
        enable antialiasing
  -height int
        image height
  -output string
        output file name
  -width int
        image width
exit status 2

Samozřejmě nám nic nebrání ve specifikaci argumentů s krátkým a současně i dlouhým jménem – prostě u obou argumentů uvedeme stejnou proměnnou, což je ukázáno v dnešním třetím demonstračním příkladu:

package main
 
import (
        "flag"
        "fmt"
)
 
func main() {
        var width int
        var height int
        var aa bool
        var output string
 
        flag.IntVar(&width, "w", 0, "image width (shorthand)")
        flag.IntVar(&width, "width", 0, "image width")
 
        flag.IntVar(&height, "h", 0, "image height (shorthand)")
        flag.IntVar(&height, "height", 0, "image height")
 
        flag.BoolVar(&aa, "a", false, "enable antialiasing (shorthand)")
        flag.BoolVar(&aa, "antialias", false, "enable antialiasing")
 
        flag.StringVar(&output, "o", "", "output file name (shorthand)")
        flag.StringVar(&output, "output", "", "output file name")
 
        flag.Parse()
 
        fmt.Printf("width: %d\n", width)
        fmt.Printf("height: %d\n", height)
        fmt.Printf("antialiasing: %t\n", aa)
        fmt.Printf("output file name: %s\n", output)
}

Krátké otestování možností:

$ go run 03_flag_shorthands.go 
 
width: 0
height: 0
antialiasing: false
output file name:

Vygenerovaná nápověda:

$ go run 03_flag_shorthands.go --help
 
Usage of /tmp/go-build543408348/b001/exe/03_flag_shorthands:
  -a    enable antialiasing (shorthand)
  -antialias
        enable antialiasing
  -h int
        image height (shorthand)
  -height int
        image height
  -o string
        output file name (shorthand)
  -output string
        output file name
  -w int
        image width (shorthand)
  -width int
        image width
exit status 2

Specifikace zkrácených parametrů:

$ go run 03_flag_shorthands.go -w=320 -h=240 -o=xyzzy -a
 
width: 320
height: 240
antialiasing: true
output file name: xyzzy

Alternativní způsob volání:

$ go run 03_flag_shorthands.go -w 320 -h 240 -o xyzzy -a
 
width: 320
height: 240
antialiasing: true
output file name: xyzzy

4. Celá čísla s neomezeným rozsahem

Připomeňme si, že v programovacím jazyku Go mají vývojáři k dispozici několik základních datových typů určených pro práci s celými čísly, ať již se znaménkem, nebo bez znaménka. Jedná se o tyto typy, přičemž int a uint jsou systémově závislé aliasy:

Identifikátor Typ Stručný popis
int datový typ odpovídá buď typu int32 nebo int64
int8 datový typ osmibitové celé číslo se znaménkem
int16 datový typ šestnáctibitové celé číslo se znaménkem
int32 datový typ 32bitové celé číslo se znaménkem
int64 datový typ 64bitové celé číslo se znaménkem
uint datový typ odpovídá buď typu uint32 nebo uint64
uint8 datový typ osmibitové celé číslo bez znaménka
uint16 datový typ 16bitové celé číslo bez znaménka
uint32 datový typ 32bitové celé číslo bez znaménka
uint64 datový typ 64bitové celé číslo bez znaménka

Práce s hodnotami těchto typů je většinou velmi rychlá, a to z toho důvodu, že současné mikroprocesory operaci s celými čísly typicky provedou přímo v aritmeticko-logické jednotce v několika strojových cyklech (které se navíc překrývají s dalšími instrukcemi díky instrukční pipeline). Ovšem mohou nastat situace, kdy nám ani rozsah typu int64 nebo uint64 nebude dostačovat. V tomto případě je možné využít možnosti poskytované standardním balíčkem math/big, v němž se mj. nachází i specifikace nového typu Int. Podívejme se nyní, jakým způsobem se budou počítat druhé mocniny dvojky, a to v libovolném rozsahu (ve skutečnosti jsme omezeni pamětí přiřazenou procesu popř. maximální velikosti zásobníku gorutiny, ovšem z praktického hlediska se o žádné reálné omezení nejedná). Povšimněte si, že namísto běžných operací = a * je nutné použít metody SetInt64 a Mul, protože současná verze programovacího jazyka Go neumožňuje přetížení operátorů:

package main
 
import (
        "fmt"
        . "math/big"
)
 
func main() {
        var x Int
        var y Int
        x.SetInt64(1)
        y.SetInt64(2)
 
        for i := 1; i < 200; i++ {
                fmt.Println(x.Text(10))
                x.Mul(&x, &y)
        }
}

Výsledek po spuštění bude vypadat následovně:

1
2
4
8
16
32
64
128
256
512
1024
2048
4096
8192
16384
32768
65536
131072
262144
524288
1048576
...
...
...
6277101735386680763835789423207666416102355444464034512896
12554203470773361527671578846415332832204710888928069025792
25108406941546723055343157692830665664409421777856138051584
50216813883093446110686315385661331328818843555712276103168
100433627766186892221372630771322662657637687111424552206336
200867255532373784442745261542645325315275374222849104412672
401734511064747568885490523085290650630550748445698208825344

Z těchto výsledků můžeme vidět, že skutečně nejsme omezeni „pouze“ rozsahem 64bitových slov.

5. Výpočet faktoriálu pro téměř libovolné n

S celými čísly o prakticky neomezeném rozsahu můžeme provádět všechny základní aritmetické operace; pouze nesmíme zapomenout na to, že se nezapisují s využitím operátorů +, -, *, / a %, ale příslušnými metodami popsanými na stránce https://golang.org/pkg/math/big/#Int. Taktéž porovnání dvou hodnot se neprovádí standardní šesticí relačních operátorů, ale metodou Int.Cmp Pro úplnost si ukažme, jakým způsobem je možné implementovat funkci pro výpočet faktoriálu pro libovolné kladné n:

package main
 
import (
        "fmt"
        . "math/big"
)
 
func factorial(n *Int) *Int {
        one := NewInt(1)
        if n.Cmp(NewInt(0)) <= 0 {
                return one
        } else {
                return one.Mul(n, factorial(one.Sub(n, one)))
        }
}
 
func main() {
        for n := int64(1); n < 80; n++ {
                f := factorial(NewInt(n))
                fmt.Printf("%3d! = %s\n", n, f.Text(10))
        }
}

Podívejme se nyní na faktoriály od 1! do 79! (samozřejmě můžeme spočítat i faktoriál pro vyšší n, výstup je však již příliš dlouhý a nevleze se do šířky vyhrazené textu článku):

  1! = 1
  2! = 2
  3! = 6
  4! = 24
  5! = 120
  6! = 720
  7! = 5040
  8! = 40320
  9! = 362880
 10! = 3628800
 11! = 39916800
 12! = 479001600
 13! = 6227020800
 14! = 87178291200
 15! = 1307674368000
 16! = 20922789888000
 17! = 355687428096000
 18! = 6402373705728000
 19! = 121645100408832000
 20! = 2432902008176640000
 21! = 51090942171709440000
 22! = 1124000727777607680000
 23! = 25852016738884976640000
 24! = 620448401733239439360000
 25! = 15511210043330985984000000
 26! = 403291461126605635584000000
 27! = 10888869450418352160768000000
 28! = 304888344611713860501504000000
 29! = 8841761993739701954543616000000
 30! = 265252859812191058636308480000000
 31! = 8222838654177922817725562880000000
 32! = 263130836933693530167218012160000000
 33! = 8683317618811886495518194401280000000
 34! = 295232799039604140847618609643520000000
 35! = 10333147966386144929666651337523200000000
 36! = 371993326789901217467999448150835200000000
 37! = 13763753091226345046315979581580902400000000
 38! = 523022617466601111760007224100074291200000000
 39! = 20397882081197443358640281739902897356800000000
 40! = 815915283247897734345611269596115894272000000000
 41! = 33452526613163807108170062053440751665152000000000
 42! = 1405006117752879898543142606244511569936384000000000
 43! = 60415263063373835637355132068513997507264512000000000
 44! = 2658271574788448768043625811014615890319638528000000000
 45! = 119622220865480194561963161495657715064383733760000000000
 46! = 5502622159812088949850305428800254892961651752960000000000
 47! = 258623241511168180642964355153611979969197632389120000000000
 48! = 12413915592536072670862289047373375038521486354677760000000000
 49! = 608281864034267560872252163321295376887552831379210240000000000
 50! = 30414093201713378043612608166064768844377641568960512000000000000
 51! = 1551118753287382280224243016469303211063259720016986112000000000000
 52! = 80658175170943878571660636856403766975289505440883277824000000000000
 53! = 4274883284060025564298013753389399649690343788366813724672000000000000
 54! = 230843697339241380472092742683027581083278564571807941132288000000000000
 55! = 12696403353658275925965100847566516959580321051449436762275840000000000000
 56! = 710998587804863451854045647463724949736497978881168458687447040000000000000
 57! = 40526919504877216755680601905432322134980384796226602145184481280000000000000
 58! = 2350561331282878571829474910515074683828862318181142924420699914240000000000000
 59! = 138683118545689835737939019720389406345902876772687432540821294940160000000000000
 60! = 8320987112741390144276341183223364380754172606361245952449277696409600000000000000
 61! = 507580213877224798800856812176625227226004528988036003099405939480985600000000000000
 62! = 31469973260387937525653122354950764088012280797258232192163168247821107200000000000000
 63! = 1982608315404440064116146708361898137544773690227268628106279599612729753600000000000000
 64! = 126886932185884164103433389335161480802865516174545192198801894375214704230400000000000000
 65! = 8247650592082470666723170306785496252186258551345437492922123134388955774976000000000000000
 66! = 544344939077443064003729240247842752644293064388798874532860126869671081148416000000000000000
 67! = 36471110918188685288249859096605464427167635314049524593701628500267962436943872000000000000000
 68! = 2480035542436830599600990418569171581047399201355367672371710738018221445712183296000000000000000
 69! = 171122452428141311372468338881272839092270544893520369393648040923257279754140647424000000000000000
 70! = 11978571669969891796072783721689098736458938142546425857555362864628009582789845319680000000000000000
 71! = 850478588567862317521167644239926010288584608120796235886430763388588680378079017697280000000000000000
 72! = 61234458376886086861524070385274672740778091784697328983823014963978384987221689274204160000000000000000
 73! = 4470115461512684340891257138125051110076800700282905015819080092370422104067183317016903680000000000000000
 74! = 330788544151938641225953028221253782145683251820934971170611926835411235700971565459250872320000000000000000
 75! = 24809140811395398091946477116594033660926243886570122837795894512655842677572867409443815424000000000000000000
 76! = 1885494701666050254987932260861146558230394535379329335672487982961844043495537923117729972224000000000000000000
 77! = 145183092028285869634070784086308284983740379224208358846781574688061991349156420080065207861248000000000000000000
 78! = 11324281178206297831457521158732046228731749579488251990048962825668835325234200766245086213177344000000000000000000
 79! = 894618213078297528685144171539831652069808216779571907213868063227837990693501860533361810841010176000000000000000000

6. Čísla s plovoucí řádovou čárkou s neomezeným rozsahem a přesností

Opět si připomeňme, že v programovacím jazyku Go existují dva základní datové typy určené pro reprezentaci hodnot s plovoucí řádovou čárkou. Tyto typy se jmenují float32 a float64, což odpovídá céčkovským typům float a double. Typy float32float64 svými vlastnostmi odpovídají normě IEEE 754, z níž si uvedeme jen krátký výňatek:

Čísla s plovoucí řádovou čárkou jsou reprezentována třemi bitovými poli určenými pro uložení znaménka, exponentu a mantisy.

Podle bitové šířky čísel exp, bias a m se rozlišují základní (basic) a rozšířené (extended) formáty FP čísel; norma IEEE 754 (její původní verze) přitom explicitně zmiňuje dva základní formáty: jednoduchá přesnost (single, v Go pak float32) a dvojitá přesnost (double, v Go float64). Druhá verze normy IEEE 754–2008 již obsahuje specifikaci většího množství formátů; navíc došlo k přejmenování typů single a double na binary32 a binary64:

Oficiální jméno Základní Známo též jako Znaménko Exponent Mantisa Celkem Decimálních číslic
binary16 × half precision 1b 5b 10b 16b cca 3,3
binary32 single precision/float/float32 1b 8b 23b 32b cca 7,2
binary64 double precision/float64 1b 11b 52b 64b cca 15,9
binary128 quadruple precision 1b 15b 112b 128b cca 34,0
binary256 × octuple precision 1b 19b 236b 256b cca 71,3

Zajímat nás nyní bude typ označený v Go jménem float32. Jeho 32 bitů je rozděleno takto:

  1. 1 bit pro znaménko
  2. 8 bitů pro exponent
  3. 23 bitů pro mantisu

Exponent je přitom posunutý o hodnotu bias, která je nastavena na 127, protože je použit vztah:

bias=2eb-1-1

a po dosazení eb=8 (bitů) dostaneme:

bias=28–1-1=27-1=128–1=127

Vzorec pro vyjádření reálné hodnoty vypadá následovně:

Xsingle=(-1)s × 2exp-127 × m

Naproti tomu u typu float64 je každá hodnota reprezentována šedesáti čtyřmi bity rozdělenými následujícím způsobem:

  1. 1 bit pro znaménko
  2. 11 bitů pro exponent
  3. 52 bitů pro mantisu

Bitově vypadá rozdělení následovně:

bit 63 62 … 52 51 … 0
význam s exponent (11 bitů) mantisa (52 bitů)

Exponent je v tomto případě posunutý o hodnotu bias=2047 a vzorec pro výpočet reálné hodnoty vypadá takto:

Xdouble=(-1)s × 2exp-2047 × m

Přičemž hodnotu mantisy je možné pro normalizované hodnoty získat pomocí vztahu:

m=1+m51-1+m50-2+m49-3+…+m0-52

(mx představuje x-tý bit mantisy)

Rozsah hodnot ukládaných ve dvojité přesnosti je –1,7×10308..1,7×10308, nejmenší možná nenulová hodnota je rovna 2,2×10-308.

V případě, že nám nebude rozsah hodnot (tj. v podstatě počet bitů exponentu) či přesnost (de facto počet bitů mantisy) dostačovat, můžeme namísto toho použít datový typ Float z balíčku math/big. Způsob jeho použití je velmi podobný nám již známému typu Int, takže si ukažme způsob postupného výpočtu prvků řady 2-n. Povšimněte si, jakým způsobem se převádí hodnota Float na řetězec s využitím metody Text, které se předává jak požadovaný formát, tak i šířka výpisu:

package main
 
import (
        "fmt"
        . "math/big"
)
 
func main() {
        x := NewFloat(1.0)
        y := NewFloat(0.5)

        for i := 1; i < 82; i++ {
                fmt.Println(x.Text('f', 80))
                x.Mul(x, y)
        }
}

Z výsledků je patrné, že nedochází k žádným zaokrouhlovacím ani jiným chybám (stačí odseknout část před desetinnou tečkou a spočítat výsledky s typem Int pro kontrolu):

1.00000000000000000000000000000000000000000000000000000000000000000000000000000000
0.50000000000000000000000000000000000000000000000000000000000000000000000000000000
0.25000000000000000000000000000000000000000000000000000000000000000000000000000000
0.12500000000000000000000000000000000000000000000000000000000000000000000000000000
0.06250000000000000000000000000000000000000000000000000000000000000000000000000000
0.03125000000000000000000000000000000000000000000000000000000000000000000000000000
0.01562500000000000000000000000000000000000000000000000000000000000000000000000000
0.00781250000000000000000000000000000000000000000000000000000000000000000000000000
...
...
...
0.00000000000000000000010587911840678754238354031258495524525642395019531250000000
0.00000000000000000000005293955920339377119177015629247762262821197509765625000000
0.00000000000000000000002646977960169688559588507814623881131410598754882812500000
0.00000000000000000000001323488980084844279794253907311940565705299377441406250000
0.00000000000000000000000661744490042422139897126953655970282852649688720703125000
0.00000000000000000000000330872245021211069948563476827985141426324844360351562500
0.00000000000000000000000165436122510605534974281738413992570713162422180175781250
0.00000000000000000000000082718061255302767487140869206996285356581211090087890625

7. Spuštění externích utilit

Poměrně často se setkáme s požadavkem na to, aby se s aplikace naprogramované v jazyku Go spustila nějaká externí utilita. K tomuto účelu je možné použít funkci Command, kterou nalezneme v balíčku os/exec. Této funkci lze předat proměnný počet parametrů, přičemž prvním parametrem je jméno spouštěné utility a v dalších parametrech pak argumenty předávané na příkazovém řádku. V dalším příkladu je ukázáno, jakým způsobem je možné spustit známou utilitku date a získat její výstup; a to s kontrolou, zda případně nedošlo k nějaké chybě při spouštění či při běhu utility:

package main
 
import (
        "fmt"
        "os/exec"
)
 
func main() {
        cmd := exec.Command("date", "--date=next Mon")
        out, err := cmd.Output()
        if err != nil {
                fmt.Println(err)
        } else {
                fmt.Printf("Next Monday: %s\n", out)
        }
}
$ go run 07_exec.go
 
Next Monday: Po úno  4 00:00:00 CET 2019

8. Předání dat na standardní vstup externích utilit

Poněkud složitější je situace ve chvíli, kdy potřebujeme externí utilitě předat nějaké informace přes standardní vstup. Příkladem může být požadavek na spuštění nástroje sort, který dokáže seřadit řádky ze standardního vstupu. Pro spuštění utility sort nyní použijeme příkaz StdinPipe, který by měl vrátit handle nově otevřeného standardního vstupu utility (určeného pro zápis z pohledu programu v Go) a případný kód chyby. Do otevřeného standardního vstupu se zapisují data přes funkci io.WriteString a nesmíme zapomenout na jeho uzavření. Následně již výstup z utility zpracujeme nám již známým způsobem:

package main
 
import (
        "fmt"
        "io"
        "os/exec"
)
 
func main() {
        cmd := exec.Command("sort")
 
        stdin, err := cmd.StdinPipe()
        if err != nil {
                fmt.Println(err)
        }
 
        io.WriteString(stdin, "zzz\n")
        io.WriteString(stdin, "xyz\n")
        io.WriteString(stdin, "aaa\n")
        stdin.Close()
 
        out, err := cmd.Output()
        if err != nil {
                fmt.Println(err)
        } else {
                fmt.Printf("sorted input:\n%s\n", out)
        }
}

Příklad výstupu programu:

$ go run 08_exec_stdin.go 
 
sorted input:
aaa
xyz
zzz

V případě, že se volá externí utilita zpracovávající rozsáhlejší data, je korektnější použít jiný způsob zápisu, v němž se data utilitě předávají v paralelně běžící gorutině a současně se čte výstup produkovaný utilitou. Tímto způsobem lze utilitě předat prakticky libovolné množství dat. Jedno z nejjednodušších řešení tohoto problému je ukázáno v následujícím demonstračním příkladu:

package main
 
import (
        "fmt"
        "io"
        "os/exec"
)
 
func main() {
        cmd := exec.Command("sort")
 
        stdin, err := cmd.StdinPipe()
        if err != nil {
                fmt.Println(err)
        }
 
        go func() {
                defer stdin.Close()
                io.WriteString(stdin, "zzz\n")
                io.WriteString(stdin, "xyz\n")
                io.WriteString(stdin, "aaa\n")
        }()
 
        out, err := cmd.Output()
        if err != nil {
                fmt.Println(err)
        } else {
                fmt.Printf("sorted input:\n%s\n", out)
        }
}

Výstup, který získáme z předchozího příkladu, bude vypadat následovně:

$ go run 09_exec_stdin.go 
 
sorted input:
aaa
xyz
zzz

9. Čtení a vyhledávání proměnných prostředí

Dalším často prováděnou operací je čtení a popř. i modifikace proměnných prostředí (environment variables). Pro přečtení nějaké proměnné použijte funkci os.LookupEnv, která vrací hodnotu proměnné a příznak, zda byla proměnná nalezena. Pokud budete potřebovat vypsat všechny proměnné prostředí, použijte funkci os.Environ – výslednou sekvencí lze iterovat s využitím konstrukce for-range:

package main
 
import (
        "fmt"
        "os"
)
 
func main() {
        env_vars := os.Environ()
 
        for i, env_var := range env_vars {
                fmt.Printf("%02d\t%s\n", i, env_var)
        }
 
        term, found := os.LookupEnv("TERM")
        if found {
                fmt.Printf("\n\n\nSelected TERM = %s", term)
        } else {
                fmt.Printf("\n\n\nThe TERM environment variable is not set")
        }
}

10. Získání základních informací o běžícím procesu a jeho prostředí

V závěru první části článku si ještě ukažme několik funkcí z balíčku os, které slouží pro získání základních informací o běžícím procesu (PID) a o jeho prostředí (PID rodičovského procesu, adresář pro uložení dočasných souborů, aktuální adresář):

package main
 
import (
        "fmt"
        "os"
)
 
func main() {
        fmt.Printf("PID = %d\n", os.Getpid())
        fmt.Printf("Parent PID = %d\n", os.Getppid())
        fmt.Printf("Temp. directory = %s\n", os.TempDir())
        cwd, err := os.Getwd()
        if err == nil {
                fmt.Printf("CWD = %s\n", cwd)
        } else {
                fmt.Printf("can not get CWD")
        }
}

Výstup může vypadat následovně:

PID = 8841
Parent PID = 8782
Temp. directory = /tmp
CWD = /home/tester/article_10

11. Kvalita překladače jazyka Go při optimalizacích

Ve druhé části článku se pokusíme porovnat kvalitu překladače programovacího jazyka Go (jeho standardní verze) s překladačem ANSI C. Jak sami uvidíte, je nutné při psaní benchmarků brát v úvahu i vlastnosti standardních knihoven obou jazyků, které mohou mít dosti odlišné chování.

Dnešní benchmark bude provádět výpočty s výpisem výsledku výpočtů na standardní výstup. Ten bude přesměrován do souboru, protože výsledkem výpočtů budou bitmapy v jednoduchém a současně i přenositelném formátu Portable Pixel Map (viz [1]). Samozřejmě je nutné si uvědomit, že i výpis hodnot na standardní výstup znamená nutnost volání knihovních funkcí a bude ovlivňovat čas výpočtu (jak uvidíme dále, tak mnohdy dosti význačně). Celý benchmark spočívá ve výpočtu barev pixelů Mandelbrotovy množiny, přičemž rozlišení výsledného rastrového obrázku i maximální počet iterací bude možné zvolit z příkazového řádku.

Obrázek 1: Výsledek benchmarku pro fraktál s rozlišením 512×512 pixelů.

12. Vzorový benchmark naprogramovaný v ANSI C

První varianta příkladu naprogramovaného v jazyku C vypadá následovně:

#include <stdlib.h>
#include <stdio.h>
 
#include "palette_mandmap.h"
 
void calc_mandelbrot(unsigned int width, unsigned int height, unsigned int maxiter, unsigned char palette[][3])
{
    puts("P3");
    printf("%d %d\n", width, height);
    puts("255");
 
    double cy = -1.5;
    int y;
    for (y=0; y<height; y++) {
        double cx = -2.0;
        int x;
        for (x=0; x<width; x++) {
            double zx = 0.0;
            double zy = 0.0;
            unsigned int i = 0;
            while (i < maxiter) {
                double zx2 = zx * zx;
                double zy2 = zy * zy;
                if (zx2 + zy2 > 4.0) {
                    break;
                }
                zy = 2.0 * zx * zy + cy;
                zx = zx2 - zy2 + cx;
                i++;
            }
            unsigned char *color = palette[i];
            unsigned char r = *color++;
            unsigned char g = *color++;
            unsigned char b = *color;
            printf("%d %d %d\n", r, g, b);
            cx += 3.0/width;
        }
        cy += 3.0/height;
    }
}
 
 
int main(int argc, char **argv)
{
    if (argc < 4) {
        puts("usage: ./mandelbrot width height maxiter");
        return 1;
    }
    int width = atoi(argv[1]);
    int height = atoi(argv[2]);
    int maxiter = atoi(argv[3]);
    calc_mandelbrot(width, height, maxiter, palette);
    return 0;
}

Překlad provedeme takto pomocí Makefile:

# Parametry prekladace.
CFLAGS=-Wall -ansi -O9 -funroll-loops -march=native
 
PROGNAME=mandelbrot
 
all:    $(PROGNAME)
 
clean:
        rm *.o
        rm $(PROGNAME)
 
# Pravidlo pro slinkovani vsech objektovych souboru a vytvoreni
# vysledne spustitelne aplikace.
$(PROGNAME):    $(PROGNAME).o
        $(CC) -o $@ $(LDFLAGS) $<
 
# Pravidlo pro preklad kazdeho zdrojoveho souboru do prislusneho
# objektoveho souboru.
%.o:    %.c
        $(CC) $(CFLAGS) -c $< -o $@

Benchmark spustíme několikrát a budeme přitom měnit požadované rozlišení výsledné bitmapy s fraktálem:

sizes="16 24 32 48 64 96 128 192 256 384 512 768 1024 1536 2048 3072 4096"
 
OUTFILE="c.times"
PREFIX="mandelbrot"
 
rm $OUTFILE
 
for size in $sizes
do
    echo $size
    echo -n "$size " >> $OUTFILE
    /usr/bin/time --output $OUTFILE --append --format "%e %M" ./mandelbrot $size $size 255 > "${PREFIX}_${size}_${size}.ppm"
done

Výsledky po spuštění na stroji s procesorem i5 se čtyřmi jádry:

# Rozlišení Čas (s)
1 16×16 0.00
2 24×24 0.00
3 32×32 0.00
4 48×48 0.00
5 64×64 0.00
6 96×96 0.00
7 128×128 0.00
8 192×192 0.01
9 256×256 0.03
10 384×384 0.06
11 512×512 0.11
12 768×768 0.25
13 1024×1024 0.44
14 1536×1536 1.01
15 2048×2048 1.78
16 3072×3072 4.03
17 4096×4096 7.11

Výsledky po spuštění na stroji s procesorem i7 se šesti jádry:

# Rozlišení Čas (s)
1 16×16 0.00
2 24×24 0.00
3 32×32 0.00
4 48×48 0.00
5 64×64 0.00
6 96×96 0.00
7 128×128 0.00
8 192×192 0.01
9 256×256 0.02
10 384×384 0.04
11 512×512 0.07
12 768×768 0.17
13 1024×1024 0.30
14 1536×1536 0.67
15 2048×2048 1.20
16 3072×3072 2.70
17 4096×4096 4.83

13. První varianta benchmarku přímo přepsaná do Go

Předchozí benchmark si nejdříve přepíšeme do jazyka Go, a to bez jakýchkoli zásadnějších úprav. Výsledek by mohl vypadat například takto:

 
import (
        "fmt"
        "os"
        "strconv"
)
 
func calcMandelbrot(width uint, height uint, maxiter uint, palette [][3]byte) {
        fmt.Println("P3")
        fmt.Printf("%d %d\n", width, height)
        fmt.Println("255")
 
        var cy float64 = -1.5
 
        for y := uint(0); y < height; y++ {
                var cx float64 = -2.0
                for x := uint(0); x < width; x++ {
                        var zx float64 = 0.0
                        var zy float64 = 0.0
                        var i uint = 0
                        for i < maxiter {
                                zx2 := zx * zx
                                zy2 := zy * zy
                                if zx2+zy2 > 4.0 {
                                        break
                                }
                                zy = 2.0*zx*zy + cy
                                zx = zx2 - zy2 + cx
                                i++
                        }
                        color := palette[i]
                        r := color[0]
                        g := color[1]
                        b := color[2]
                        fmt.Printf("%d %d %d\n", r, g, b)
                        cx += 3.0 / float64(width)
                }
                cy += 3.0 / float64(height)
        }
 
}
 
func main() {
        if len(os.Args) < 4 {
                println("usage: ./mandelbrot width height maxiter")
                os.Exit(1)
        }
 
        width, err := strconv.Atoi(os.Args[1])
        if err != nil {
                fmt.Printf("Improper width parameter: '%s'\n", os.Args[1])
                os.Exit(1)
        }
 
        height, err := strconv.Atoi(os.Args[2])
        if err != nil {
                fmt.Printf("Improper height parameter: '%s'\n", os.Args[2])
                os.Exit(1)
        }
 
        maxiter, err := strconv.Atoi(os.Args[3])
        if err != nil {
                fmt.Printf("Improper maxiter parameter: '%s'\n", os.Args[3])
                os.Exit(1)
        }
        calcMandelbrot(uint(width), uint(height), uint(maxiter), mandmap[:])
}

Časy běhu na počítači s procesorem i5 jsou znatelně horší, než v případě céčkové varianty:

# Rozlišení Čas (s)
1 16×16 0.00
2 24×24 0.00
3 32×32 0.00
4 48×48 0.00
5 64×64 0.01
6 96×96 0.02
7 128×128 0.04
8 192×192 0.09
9 256×256 0.16
10 384×384 0.36
11 512×512 0.64
12 768×768 1.41
13 1024×1024 2.54
14 1536×1536 5.66
15 2048×2048 10.08
16 3072×3072 23.61
17 4096×4096 40.53

Totéž zhoršení uvidíme i u počítače s procesorem i7:

# Rozlišení Čas (s)
1 16×16 0.00
2 24×24 0.00
3 32×32 0.00
4 48×48 0.00
5 64×64 0.00
6 96×96 0.01
7 128×128 0.02
8 192×192 0.05
9 256×256 0.08
10 384×384 0.19
11 512×512 0.34
12 768×768 0.77
13 1024×1024 1.36
14 1536×1536 3.08
15 2048×2048 5.55
16 3072×3072 12.41
17 4096×4096 21.95

14. Vliv bufferu na rychlost dokončení benchmarku

Při porovnání výsledků benchmarku naprogramovaného v céčku a přeloženého s povolením optimalizací překladače s benchmarkem vytvořeným v Go by se mohlo zdát, že překladač Go nedokáže provádět příliš dobré optimalizace. To sice může být pravda (přesvědčíme se o tom v dalších kapitolách), ovšem benchmark ve skutečnosti provádí i export vypočteného obrázku na standardní výstup a ukazuje se, že právě tato část je dosti kritická operace, protože v případě Go se používá jiná metoda bufferování, než je tomu v céčku.

V jazyku C se při použití standardního výstupu používá buffer, který ovšem můžeme s využitím funkce setvbuf přenastavit a dokonce i zakázat. Ukažme si vypnutí bufferingu. Nejprve vytvoříme buffer s nulovou kapacitou (což je nyní ve skutečnosti zbytečné, ovšem později si můžete sami vyzkoušet změnit konstantu BUFFER_SIZE):

#define BUFFER_SIZE 0
char buffer[BUFFER_SIZE];

A posléze funkcí setvbuf řekneme, že se má buffer zcela vypnout (_IONBF), a to konkrétně pro soubor stdout (protože z pohledu céčka je standardní výstup běžným souborem, který je automaticky otevřený již při vstupu do funkce main):

setvbuf(stdout, buffer, _IONBF, BUFFER_SIZE);

Po této nepatrné úpravě se časy běhu céčkovského programu zcela změní. Výsledky běhu benchmarku ve chvíli, kdy je buffer vypnutý a má navíc nulovou délku:

# Rozlišení Čas (s)
1 16×16 0.00
2 24×24 0.00
3 32×32 0.00
4 48×48 0.00
5 64×64 0.01
6 96×96 0.02
7 128×128 0.03
8 192×192 0.08
9 256×256 0.13
10 384×384 0.29
11 512×512 0.52
12 768×768 1.18
13 1024×1024 2.14
14 1536×1536 4.91
15 2048×2048 8.52
16 3072×3072 19.66
17 4096×4096 34.40

Nic nám samozřejmě nebrání provést i opačné nastavení – deklarovat obrovský buffer o kapacitě jednoho megabajtu a nastavit takzvané plné bufferování, bez závislosti na tom, zda se na výstup posílají znaky pro konce řádků či nikoli (tento typ bufferování by nám vadil při sledování standardního výstupu aplikace, my ovšem provádíme přesměrování do souboru):

#define BUFFER_SIZE 1*1024*1024
char buffer[BUFFER_SIZE];

Pro plné bufferování se použije konstanta _IOFBF:

setvbuf(stdout, buffer, _IOFBF, BUFFER_SIZE);

Nyní již budou výsledky benchmarku markantně odlišné od výsledků předchozích, což je ostatně patrné i z následující tabulky:

# Rozlišení Čas (s)
1 16×16 0.00
2 24×24 0.00
3 32×32 0.00
4 48×48 0.00
5 64×64 0.00
6 96×96 0.01
7 128×128 0.01
8 192×192 0.01
9 256×256 0.03
10 384×384 0.06
11 512×512 0.11
12 768×768 0.25
13 1024×1024 0.44
14 1536×1536 1.00
15 2048×2048 1.75
16 3072×3072 3.99
17 4096×4096 7.10

15. Druhá verze benchmarku: použití bufferovaného výstupu

V programovacím jazyku Go se režim bufferování při zápisu do souborů řídí jiným způsobem, než je tomu v programovacím jazyku C. Musíme použít standardní balíček pojmenovaný bufio a v něm vytvořit novou instanci tzv. writeru, kterému se předá reference na otevřený soubor, u něhož chceme bufferování použít. Tímto souborem bude v našem případě os.Stdout. Nesmíme zapomenout na to, aby se na konci zápisu provedla operace Writer.Flush(), která zapíše celý zbytek bufferu na konec souboru:

w := bufio.NewWriter(os.Stdout)
defer w.Flush()

Zápisy nyní nebudou prováděny přímo na os.Stdout funkcemi fmt.Println či fmt.Printf, ale budeme muset explicitně specifikovat soubor, do něhož se zápis má provést pomocí fmt.Fprintln a fmt.Fprintf:

fmt.Fprintln(w, "P3")
fmt.Fprintf(w, "%d %d\n", width, height)
fmt.Fprintln(w, "255")
...
...
...
r := color[0]
g := color[1]
b := color[2]
fmt.Fprintf(w, "%d %d %d\n", r, g, b)

Druhá varianta zdrojového kódu benchmarku bude vypadat takto:

package main
 
import (
        "bufio"
        "fmt"
        "os"
        "strconv"
)
 
func calcMandelbrot(width uint, height uint, maxiter uint, palette [][3]byte) {
        w := bufio.NewWriter(os.Stdout)
        defer w.Flush()
 
        fmt.Fprintln(w, "P3")
        fmt.Fprintf(w, "%d %d\n", width, height)
        fmt.Fprintln(w, "255")
 
        var cy float64 = -1.5
 
        for y := uint(0); y < height; y++ {
                var cx float64 = -2.0
                for x := uint(0); x < width; x++ {
                        var zx float64 = 0.0
                        var zy float64 = 0.0
                        var i uint = 0
                        for i < maxiter {
                                zx2 := zx * zx
                                zy2 := zy * zy
                                if zx2+zy2 > 4.0 {
                                        break
                                }
                                zy = 2.0*zx*zy + cy
                                zx = zx2 - zy2 + cx
                                i++
                        }
                        color := palette[i]
                        r := color[0]
                        g := color[1]
                        b := color[2]
                        fmt.Fprintf(w, "%d %d %d\n", r, g, b)
                        cx += 3.0 / float64(width)
                }
                cy += 3.0 / float64(height)
        }
}
 
func main() {
        if len(os.Args) < 4 {
                println("usage: ./mandelbrot width height maxiter")
                os.Exit(1)
        }
 
        width, err := strconv.Atoi(os.Args[1])
        if err != nil {
                fmt.Printf("Improper width parameter: '%s'\n", os.Args[1])
                os.Exit(1)
        }
 
        height, err := strconv.Atoi(os.Args[2])
        if err != nil {
                fmt.Printf("Improper height parameter: '%s'\n", os.Args[2])
                os.Exit(1)
        }
 
        maxiter, err := strconv.Atoi(os.Args[3])
        if err != nil {
                fmt.Printf("Improper maxiter parameter: '%s'\n", os.Args[3])
                os.Exit(1)
        }
        calcMandelbrot(uint(width), uint(height), uint(maxiter), mandmap[:])
}

Při pohledu na následující tabulku je zřejmé, že se nám podařilo přiblížení k výsledkům, které jsme získali z céčkovského kódu.

Průběh na počítači s procesorem i5:

# Rozlišení Čas (s)
1 16×16 0.00
2 24×24 0.00
3 32×32 0.00
4 48×48 0.00
5 64×64 0.00
6 96×96 0.00
7 128×128 0.01
8 192×192 0.02
9 256×256 0.03
10 384×384 0.07
11 512×512 0.13
12 768×768 0.29
13 1024×1024 0.50
14 1536×1536 1.14
15 2048×2048 2.03
16 3072×3072 4.56
17 4096×4096 8.07

Průběh na počítači s procesorem i7:

# Rozlišení Čas (s)
1 16×16 0.00
2 24×24 0.00
3 32×32 0.00
4 48×48 0.00
5 64×64 0.00
6 96×96 0.00
7 128×128 0.00
8 192×192 0.01
9 256×256 0.02
10 384×384 0.05
11 512×512 0.09
12 768×768 0.20
13 1024×1024 0.35
14 1536×1536 0.80
15 2048×2048 1.42
16 3072×3072 3.21
17 4096×4096 5.78

16. Třetí verze benchmarku: výpočet po jednotlivých obrazových řádcích

Předchozí benchmark si ještě dále upravíme, a to takovým způsobem, že dojde k oddělení kódu určeného pro zápis výsledného obrázku na standardní výstup od kódu pro výpočet. To ovšem není všechno, protože funkci pro výpočet Mandelbrotovy množiny změníme takovým způsobem, že se vypočte pouze jediný obrazový řádek (důvod pro tuto na první pohled možná podivnou změnu je vysvětlen v navazující kapitole). Změny budou vypadat takto:

Zápis (či lépe řečeno export) výsledné bitmapy je realizován v samostatné funkci pojmenované writeImage, které se předají rozměry bitmapy a hodnoty jednotlivých pixelů v poli typu []byte:

func writeImage(width uint, height uint, image []byte) {
}

Funkce pro výpočet jediného řádku obrázku Mandelbrotovy množiny bude mít hlavičku:

func calcMandelbrot(width uint, height uint, maxiter uint, palette [][3]byte, image []byte, cy float64) {
}

Povšimněte si, že v posledním parametru předáváme hodnotu cy, která určuje imaginární složku komplexního čísla C, které vstupuje do iterativního výpočtu Mandelbrotovy množiny (viz odkazovaný článek s podrobnějším vysvětlením).

Volání této funkce a postupné skládání bitmapy tedy může vypadat například takto:

image := make([]byte, width*height*3)
offset := 0
delta := width * 3
 
var cy float64 = -1.5
for y := 0; y < height; y++ {
        calcMandelbrot(uint(width), uint(height), uint(maxiter), mandmap[:], image[offset:offset+delta], cy)
        offset += delta
        cy += 3.0 / float64(height)
}

Úplný zdrojový kód benchmarku se tedy modifikuje následujícím způsobem:

package main
 
import (
        "bufio"
        "fmt"
        "os"
        "strconv"
)
 
func writeImage(width uint, height uint, image []byte) {
        w := bufio.NewWriter(os.Stdout)
        defer w.Flush()
 
        fmt.Fprintln(w, "P3")
        fmt.Fprintf(w, "%d %d\n", width, height)
        fmt.Fprintln(w, "255")
 
        for i := 0; i < len(image); {
                r := image[i]
                i++
                g := image[i]
                i++
                b := image[i]
                i++
                fmt.Fprintf(w, "%d %d %d\n", r, g, b)
        }
}
 
func calcMandelbrot(width uint, height uint, maxiter uint, palette [][3]byte, image []byte, cy float64) {
        var cx float64 = -2.0
        for x := uint(0); x < width; x++ {
                var zx float64 = 0.0
                var zy float64 = 0.0
                var i uint = 0
                for i < maxiter {
                        zx2 := zx * zx
                        zy2 := zy * zy
                        if zx2+zy2 > 4.0 {
                                break
                        }
                        zy = 2.0*zx*zy + cy
                        zx = zx2 - zy2 + cx
                        i++
                }
                color := palette[i]
                image[3*x] = color[0]
                image[3*x+1] = color[1]
                image[3*x+2] = color[2]
                cx += 3.0 / float64(width)
        }
}
 
func main() {
        if len(os.Args) < 4 {
                println("usage: ./mandelbrot width height maxiter")
                os.Exit(1)
        }
 
        width, err := strconv.Atoi(os.Args[1])
        if err != nil {
                fmt.Printf("Improper width parameter: '%s'\n", os.Args[1])
                os.Exit(1)
        }
 
        height, err := strconv.Atoi(os.Args[2])
        if err != nil {
                fmt.Printf("Improper height parameter: '%s'\n", os.Args[2])
                os.Exit(1)
        }
 
        maxiter, err := strconv.Atoi(os.Args[3])
        if err != nil {
                fmt.Printf("Improper maxiter parameter: '%s'\n", os.Args[3])
                os.Exit(1)
        }
 
        image := make([]byte, width*height*3)
        offset := 0
        delta := width * 3
 
        var cy float64 = -1.5
        for y := 0; y < height; y++ {
                calcMandelbrot(uint(width), uint(height), uint(maxiter), mandmap[:], image[offset:offset+delta], cy)
                offset += delta
                cy += 3.0 / float64(height)
        }
        writeImage(uint(width), uint(height), image)
}

Povšimněte si, že se tyto úpravy – které zvyšují složitost programu – vlastně nijak zásadně neprojevily na výsledcích benchmarku:

Průběh na počítači s procesorem i5:

# Rozlišení Čas (s)
1 16×16 0.00
2 24×24 0.00
3 32×32 0.00
4 48×48 0.00
5 64×64 0.00
6 96×96 0.00
7 128×128 0.01
8 192×192 0.02
9 256×256 0.04
10 384×384 0.07
11 512×512 0.13
12 768×768 0.29
13 1024×1024 0.50
14 1536×1536 1.19
15 2048×2048 2.09
16 3072×3072 4.73
17 4096×4096 8.44

Průběh na počítači s procesorem i7:

# Rozlišení Čas (s)
1 16×16 0.00
2 24×24 0.00
3 32×32 0.00
4 48×48 0.00
5 64×64 0.00
6 96×96 0.00
7 128×128 0.00
8 192×192 0.01
9 256×256 0.02
10 384×384 0.05
11 512×512 0.09
12 768×768 0.20
13 1024×1024 0.35
14 1536×1536 0.82
15 2048×2048 1.49
16 3072×3072 3.25
17 4096×4096 5.74

17. Finální verze benchmarku: využití kanálů a gorutin pro paralelní výpočty

Nyní konečně nastal čas, abychom si vysvětlili, proč jsme vlastně upravili předchozí variantu benchmarku takovým způsobem, že je možné počítat jednotlivé obrazové řádky explicitním zavoláním funkce calcMandelbrot. Celý výpočet nyní budeme paralelizovat – každý obrazový řádek bude vypočten v samostatné gorutině. Vzhledem k tomu, že gorutiny jsou interně reprezentovány úsporným způsobem, je možné tuto optimalizaci bez problémů provést a vytvořit jich tak i několik tisíc (v závislosti na rozlišení bitmapy).

O to, aby se počkalo na dokončení všech gorutin, se postará kanál pojmenovaný done, jehož kapacita přesně odpovídá počtu gorutin. Nejprve se kanál vytvoří, následně se všechny gorutiny spustí a na konci čtením z kanálu počkáme na dokončení všech height gorutin:

done := make(chan bool, height)
 
// na tomto místě bude umístěn vlastní výpočet
 
for i := 0; i < height; i++ {
        <-done
}

Jedinou další úpravou bude modifikace funkce calcMandelbrot, které se musí předat reference na kanál a do kterého se na konci výpočtu zapíše hodnota true (důležitý je zápis, nikoli vlastní hodnota):

func calcMandelbrot(width uint, height uint, maxiter uint, palette [][3]byte, image []byte, cy float64, done chan bool) {
        ...
        ...
        ...
        done <- true
}

Poslední „paralelizovaná“ varianta benchmarku bude vypadat takto:

package main
 
import (
        "bufio"
        "fmt"
        "os"
        "strconv"
)
 
func writeImage(width uint, height uint, image []byte) {
        w := bufio.NewWriter(os.Stdout)
        defer w.Flush()
 
        fmt.Fprintln(w, "P3")
        fmt.Fprintf(w, "%d %d\n", width, height)
        fmt.Fprintln(w, "255")
 
        for i := 0; i < len(image); {
                r := image[i]
                i++
                g := image[i]
                i++
                b := image[i]
                i++
                fmt.Fprintf(w, "%d %d %d\n", r, g, b)
        }
}
 
func calcMandelbrot(width uint, height uint, maxiter uint, palette [][3]byte, image []byte, cy float64, done chan bool) {
        var cx float64 = -2.0
        for x := uint(0); x < width; x++ {
                var zx float64 = 0.0
                var zy float64 = 0.0
                var i uint = 0
                for i < maxiter {
                        zx2 := zx * zx
                        zy2 := zy * zy
                        if zx2+zy2 > 4.0 {
                                break
                        }
                        zy = 2.0*zx*zy + cy
                        zx = zx2 - zy2 + cx
                        i++
                }
                color := palette[i]
                image[3*x] = color[0]
                image[3*x+1] = color[1]
                image[3*x+2] = color[2]
                cx += 3.0 / float64(width)
        }
        done <- true
}
 
func main() {
        if len(os.Args) < 4 {
                println("usage: ./mandelbrot width height maxiter")
                os.Exit(1)
        }
 
        width, err := strconv.Atoi(os.Args[1])
        if err != nil {
                fmt.Printf("Improper width parameter: '%s'\n", os.Args[1])
                os.Exit(1)
        }
 
        height, err := strconv.Atoi(os.Args[2])
        if err != nil {
                fmt.Printf("Improper height parameter: '%s'\n", os.Args[2])
                os.Exit(1)
        }
 
        maxiter, err := strconv.Atoi(os.Args[3])
        if err != nil {
                fmt.Printf("Improper maxiter parameter: '%s'\n", os.Args[3])
                os.Exit(1)
        }
 
        done := make(chan bool, height)
 
        image := make([]byte, width*height*3)
        offset := 0
        delta := width * 3
 
        var cy float64 = -1.5
        for y := 0; y < height; y++ {
                go calcMandelbrot(uint(width), uint(height), uint(maxiter), mandmap[:], image[offset:offset+delta], cy, done)
                offset += delta
                cy += 3.0 / float64(height)
        }
        for i := 0; i < height; i++ {
                <-done
        }
        writeImage(uint(width), uint(height), image)
}

Výsledky benchmarku budou podle očekávání mnohem lepší, a to z toho důvodu, že plně využijeme možnosti vícejádrových mikroprocesorů.

Průběh na počítači s procesorem i5:

# Rozlišení Čas (s)
1 16×16 0.00
2 24×24 0.00
3 32×32 0.00
4 48×48 0.00
5 64×64 0.00
6 96×96 0.00
7 128×128 0.01
8 192×192 0.01
9 256×256 0.02
10 384×384 0.05
11 512×512 0.08
12 768×768 0.19
13 1024×1024 0.34
14 1536×1536 0.76
15 2048×2048 1.33
16 3072×3072 2.98
17 4096×4096 5.31

Průběh na počítači s procesorem i7:

# Rozlišení Čas (s)
1 16×16 0.00
2 24×24 0.00
3 32×32 0.00
4 48×48 0.00
5 64×64 0.00
6 96×96 0.00
7 128×128 0.00
8 192×192 0.00
9 256×256 0.01
10 384×384 0.03
11 512×512 0.04
12 768×768 0.10
13 1024×1024 0.19
14 1536×1536 0.42
15 2048×2048 0.73
16 3072×3072 1.66
17 4096×4096 2.94
Opět připomínám, že se v posledním případě nejedná o férové porovnání, ovšem paralelizace programu v Go je mnohem snadnější, než by tomu bylo v céčku s využitím vláken.

18. Porovnání výsledků všech benchmarků

Na závěr si výsledky jednotlivých benchmarků porovnáme. Povšimněte si, v případě kódu běžícího v jednom vláknu je lepší překladač jazyka C (s optimalizacemi!), ovšem – což asi očekáváte – varianta s gorutinami je rychlejší, než nejlepší céčková varianta.

Počítač s procesorem i5 (čtyři jádra):

# Rozlišení C std. C bez bufferu C s bufferem Go std. Go s bufferem Go po řádcích Go s gorutinami
1 16×16 0,00 0,00 0,00 0,00 0,00 0,00 0,00
2 24×24 0,00 0,00 0,00 0,00 0,00 0,00 0,00
3 32×32 0,00 0,00 0,00 0,00 0,00 0,00 0,00
4 48×48 0,00 0,00 0,00 0,00 0,00 0,00 0,00
5 64×64 0,00 0,01 0,00 0,01 0,00 0,00 0,00
6 96×96 0,00 0,02 0,01 0,02 0,00 0,00 0,00
7 128×128 0,00 0,03 0,01 0,04 0,01 0,01 0,01
8 192×192 0,01 0,08 0,01 0,09 0,02 0,02 0,01
9 256×256 0,03 0,13 0,03 0,16 0,03 0,04 0,02
10 384×384 0,06 0,29 0,06 0,36 0,07 0,07 0,05
11 512×512 0,11 0,52 0,11 0,64 0,13 0,13 0,08
12 768×768 0,25 1,18 0,25 1,41 0,29 0,29 0,19
13 1024×1024 0,44 2,14 0,44 2,54 0,50 0,50 0,34
14 1536×1536 1,01 4,91 1,00 5,66 1,14 1,19 0,76
15 2048×2048 1,78 8,52 1,75 10,08 2,03 2,09 1,33
16 3072×3072 4,03 19,66 3,99 23,61 4,56 4,73 2,98
17 4096×4096 7,11 34,40 7,10 40,53 8,07 8,44 5,31

Počítač s procesorem i7 (šest jader):

# Rozlišení C std. C bez bufferu C s bufferem Go std. Go s bufferem Go po řádcích Go s gorutinami
1 16×16 0,00 0,00 0,00 0,00 0,00 0,00 0,00
2 24×24 0,00 0,00 0,00 0,00 0,00 0,00 0,00
3 32×32 0,00 0,00 0,00 0,00 0,00 0,00 0,00
4 48×48 0,00 0,00 0,00 0,00 0,00 0,00 0,00
5 64×64 0,00 0,00 0,00 0,00 0,00 0,00 0,00
6 96×96 0,00 0,01 0,00 0,01 0,00 0,00 0,00
7 128×128 0,00 0,02 0,00 0,02 0,00 0,00 0,00
8 192×192 0,01 0,04 0,01 0,05 0,01 0,01 0,00
9 256×256 0,02 0,07 0,02 0,08 0,02 0,02 0,01
10 384×384 0,04 0,16 0,04 0,19 0,05 0,05 0,03
11 512×512 0,07 0,29 0,07 0,34 0,09 0,09 0,04
12 768×768 0,17 0,65 0,17 0,77 0,20 0,20 0,10
13 1024×1024 0,30 1,20 0,30 1,36 0,35 0,35 0,19
14 1536×1536 0,67 2,64 0,67 3,08 0,80 0,82 0,42
15 2048×2048 1,20 4,67 1,19 5,55 1,42 1,49 0,73
16 3072×3072 2,70 10,67 2,68 12,41 3,21 3,25 1,66
17 4096×4096 4,83 18,84 4,89 21,95 5,78 5,74 2,94

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:

# Demonstrační příklad Popis Cesta
1 01_cmdline_params.go práce s argumenty CLI uloženými do pole os.Args https://github.com/tisnik/go-fedora/blob/master/article10/01_cmdli­ne_params.go
2 02_flags.go práce s argumenty CLI přes balíček flag https://github.com/tisnik/go-fedora/blob/master/article10/02_flag­s.go
3 03_flag_shorthands.go krátká a dlouhá jména argumentů https://github.com/tisnik/go-fedora/blob/master/article10/03_flag_shor­thands.go
4 04_bigint.go práce s celými čísly s neomezeným rozsahem https://github.com/tisnik/go-fedora/blob/master/article10/04_bi­gint.go
5 05_factorial.go výpočet faktoriálu prakticky libovolného n https://github.com/tisnik/go-fedora/blob/master/article10/05_fac­torial.go
6 06_bigfloat.go čísla s plovoucí řádovou čárkou bez omezení rozsahu a přesnosti https://github.com/tisnik/go-fedora/blob/master/article10/06_big­float.go
7 07_exec.go spuštění externího procesu https://github.com/tisnik/go-fedora/blob/master/article10/07_e­xec.go
8 08_exec_stdin.go spuštění externího procesu s předáním dat na standardní vstup https://github.com/tisnik/go-fedora/blob/master/article10/08_e­xec_stdin.go
9 09_exec_stdin.go spuštění externího procesu s předáním dat na standardní vstup https://github.com/tisnik/go-fedora/blob/master/article10/09_e­xec_stdin.go
10 10_env_var.go přístup k proměnným prostředí https://github.com/tisnik/go-fedora/blob/master/article10/10_en­v_var.go
11 11_process_info.go základní informace o spuštěném procesu https://github.com/tisnik/go-fedora/blob/master/article10/11_pro­cess_info.go
12 12_file_operations.go operace se soubory https://github.com/tisnik/go-fedora/blob/master/article10/12_fi­le_operations.go

20. Odkazy na Internetu

  1. Mandelbrot set (Rosetta code)
    https://rosettacode.org/wi­ki/Mandelbrot_set
  2. Computer language Benchmark Game: Mandelbrot set
    https://benchmarksgame-team.pages.debian.net/ben­chmarksgame/performance/man­delbrot.html
  3. How big is the pipe buffer?
    https://unix.stackexchange­.com/questions/11946/how-big-is-the-pipe-buffer
  4. How to turn off buffering of stdout in C
    https://stackoverflow.com/qu­estions/7876660/how-to-turn-off-buffering-of-stdout-in-c
  5. setbuf(3) – Linux man page
    https://linux.die.net/man/3/setbuf
  6. setvbuf(3) – Linux man page (stejný obsah jako předchozí stránka)
    https://linux.die.net/man/3/setvbuf
  7. Select waits on a group of channels
    https://yourbasic.org/golang/select-explained/
  8. Rob Pike: Simplicity is Complicated (video)
    http://www.golang.to/posts/dotgo-2015-rob-pike-simplicity-is-complicated-youtube-16893
  9. Algorithms to Go
    https://yourbasic.org/
  10. Využití knihovny Pygments (nejenom) pro obarvení zdrojových kódů
    https://www.root.cz/clanky/vyuziti-knihovny-pygments-nejenom-pro-obarveni-zdrojovych-kodu/
  11. Využití knihovny Pygments (nejenom) pro obarvení zdrojových kódů: vlastní filtry a lexery
    https://www.root.cz/clanky/vyuziti-knihovny-pygments-nejenom-pro-obarveni-zdrojovych-kodu-vlastni-filtry-a-lexery/
  12. Go Defer Simplified with Practical Visuals
    https://blog.learngoprogram­ming.com/golang-defer-simplified-77d3b2b817ff
  13. 5 More Gotchas of Defer in Go — Part II
    https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-ii-cc550f6ad9aa
  14. The Go Blog: Defer, Panic, and Recover
    https://blog.golang.org/defer-panic-and-recover
  15. The defer keyword in Swift 2: try/finally done right
    https://www.hackingwithswift.com/new-syntax-swift-2-defer
  16. Swift Defer Statement
    https://andybargh.com/swift-defer-statement/
  17. Modulo operation (Wikipedia)
    https://en.wikipedia.org/wi­ki/Modulo_operation
  18. Node.js vs Golang: Battle of the Next-Gen Languages
    https://www.hostingadvice­.com/blog/nodejs-vs-golang/
  19. The Go Programming Language (home page)
    https://golang.org/
  20. GoDoc
    https://godoc.org/
  21. Go (programming language), Wikipedia
    https://en.wikipedia.org/wi­ki/Go_(programming_langua­ge)
  22. Go Books (kniha o jazyku Go)
    https://github.com/dariubs/GoBooks
  23. The Go Programming Language Specification
    https://golang.org/ref/spec
  24. Go: the Good, the Bad and the Ugly
    https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/
  25. Package builtin
    https://golang.org/pkg/builtin/
  26. Package fmt
    https://golang.org/pkg/fmt/
  27. The Little Go Book (další kniha)
    https://github.com/dariubs/GoBooks
  28. The Go Programming Language by Brian W. Kernighan, Alan A. A. Donovan
    https://www.safaribookson­line.com/library/view/the-go-programming/9780134190570/e­book_split010.html
  29. Learning Go
    https://www.miek.nl/go/
  30. Go Bootcamp
    http://www.golangbootcamp.com/
  31. Programming in Go: Creating Applications for the 21st Century (další kniha o jazyku Go)
    http://www.informit.com/sto­re/programming-in-go-creating-applications-for-the-21st-9780321774637
  32. Introducing Go (Build Reliable, Scalable Programs)
    http://shop.oreilly.com/pro­duct/0636920046516.do
  33. Learning Go Programming
    https://www.packtpub.com/application-development/learning-go-programming
  34. The Go Blog
    https://blog.golang.org/
  35. Getting to Go: The Journey of Go's Garbage Collector
    https://blog.golang.org/ismmkeynote
  36. Go (programovací jazyk, Wikipedia)
    https://cs.wikipedia.org/wi­ki/Go_(programovac%C3%AD_ja­zyk)
  37. Rychle, rychleji až úplně nejrychleji s jazykem Go
    https://www.root.cz/clanky/rychle-rychleji-az-uplne-nejrychleji-s-jazykem-go/
  38. Installing Go on the Raspberry Pi
    https://dave.cheney.net/2012/09/25/in­stalling-go-on-the-raspberry-pi
  39. 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
  40. Niečo málo o Go – Golang (slovensky)
    http://golangsk.logdown.com/
  41. How Many Go Developers Are There?
    https://research.swtch.com/gop­hercount
  42. Most Popular Technologies (Stack Overflow Survery 2018)
    https://insights.stackover­flow.com/survey/2018/#most-popular-technologies
  43. Most Popular Technologies (Stack Overflow Survery 2017)
    https://insights.stackover­flow.com/survey/2017#techno­logy
  44. JavaScript vs. Golang for IoT: Is Gopher Winning?
    https://www.iotforall.com/javascript-vs-golang-iot/
  45. The Go Programming Language: Release History
    https://golang.org/doc/de­vel/release.html
  46. Go 1.11 Release Notes
    https://golang.org/doc/go1.11
  47. Go 1.10 Release Notes
    https://golang.org/doc/go1.10
  48. Go 1.9 Release Notes (tato verze je stále používána)
    https://golang.org/doc/go1.9
  49. Go 1.8 Release Notes (i tato verze je stále používána)
    https://golang.org/doc/go1.8
  50. Go on Fedora
    https://developer.fedorapro­ject.org/tech/languages/go/go-installation.html
  51. Writing Go programs
    https://developer.fedorapro­ject.org/tech/languages/go/go-programs.html
  52. The GOPATH environment variable
    https://tip.golang.org/doc/co­de.html#GOPATH
  53. Command gofmt
    https://tip.golang.org/cmd/gofmt/
  54. The Go Blog: go fmt your code
    https://blog.golang.org/go-fmt-your-code
  55. C? Go? Cgo!
    https://blog.golang.org/c-go-cgo
  56. 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/
  57. 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
  58. Gofmt No Longer Allows Spaces. Tabs Only
    https://news.ycombinator.com/i­tem?id=7914523
  59. Why does Go „go fmt“ uses tabs instead of whitespaces?
    https://www.quora.com/Why-does-Go-go-fmt-uses-tabs-instead-of-whitespaces
  60. Interactive: The Top Programming Languages 2018
    https://spectrum.ieee.org/sta­tic/interactive-the-top-programming-languages-2018
  61. Go vs. Python
    https://www.peterbe.com/plog/govspy
  62. PackageManagementTools
    https://github.com/golang/go/wi­ki/PackageManagementTools
  63. A Tour of Go: Type inference
    https://tour.golang.org/basics/14
  64. Go Slices: usage and internals
    https://blog.golang.org/go-slices-usage-and-internals
  65. Go by Example: Slices
    https://gobyexample.com/slices
  66. What is the point of slice type in Go?
    https://stackoverflow.com/qu­estions/2098874/what-is-the-point-of-slice-type-in-go
  67. The curious case of Golang array and slices
    https://medium.com/@hackintoshrao/the-curious-case-of-golang-array-and-slices-2565491d4335
  68. Introduction to Slices in Golang
    https://www.callicoder.com/golang-slices/
  69. Golang: Understanding ‚null‘ and nil
    https://newfivefour.com/golang-null-nil.html
  70. What does nil mean in golang?
    https://stackoverflow.com/qu­estions/35983118/what-does-nil-mean-in-golang
  71. nils In Go
    https://go101.org/article/nil.html
  72. Go slices are not dynamic arrays
    https://appliedgo.net/slices/
  73. Go-is-no-good (nelze brát doslova)
    https://github.com/ksimka/go-is-not-good
  74. Rust vs. Go
    https://news.ycombinator.com/i­tem?id=13430108
  75. Seriál Programovací jazyk Rust
    https://www.root.cz/seria­ly/programovaci-jazyk-rust/
  76. Modern garbage collection: A look at the Go GC strategy
    https://blog.plan99.net/modern-garbage-collection-911ef4f8bd8e
  77. Go GC: Prioritizing low latency and simplicity
    https://blog.golang.org/go15gc
  78. Is Golang a good language for embedded systems?
    https://www.quora.com/Is-Golang-a-good-language-for-embedded-systems
  79. Running GoLang on an STM32 MCU. A quick tutorial.
    https://www.mickmake.com/post/running-golang-on-an-mcu-a-quick-tutorial
  80. Go, Robot, Go! Golang Powered Robotics
    https://gobot.io/
  81. Emgo: Bare metal Go (language for programming embedded systems)
    https://github.com/ziutek/emgo
  82. UTF-8 history
    https://www.cl.cam.ac.uk/~mgk25/uc­s/utf-8-history.txt
  83. Less is exponentially more
    https://commandcenter.blog­spot.com/2012/06/less-is-exponentially-more.html
  84. Should I Rust, or Should I Go
    https://codeburst.io/should-i-rust-or-should-i-go-59a298e00ea9
  85. Setting up and using gccgo
    https://golang.org/doc/install/gccgo
  86. Elastic Tabstops
    http://nickgravgaard.com/elastic-tabstops/
  87. Strings, bytes, runes and characters in Go
    https://blog.golang.org/strings
  88. Datový typ
    https://cs.wikipedia.org/wi­ki/Datov%C3%BD_typ
  89. Seriál o programovacím jazyku Rust: Základní (primitivní) datové typy
    https://www.root.cz/clanky/pro­gramovaci-jazyk-rust-nahrada-c-nebo-slepa-cesta/#k09
  90. 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
  91. Seriál o programovacím jazyku Rust: Řezy (slice) vektoru
    https://www.root.cz/clanky/prace-s-vektory-v-programovacim-jazyku-rust/#k05
  92. Printf Format Strings
    https://www.cprogramming.com/tu­torial/printf-format-strings.html
  93. Java: String.format
    https://docs.oracle.com/ja­vase/8/docs/api/java/lang/Strin­g.html#format-java.lang.String-java.lang.Object…-
  94. Java: format string syntax
    https://docs.oracle.com/ja­vase/8/docs/api/java/util/For­matter.html#syntax
  95. Selectors
    https://golang.org/ref/spec#Selectors
  96. Calling Go code from Python code
    http://savorywatt.com/2015/09/18/ca­lling-go-code-from-python-code/
  97. Go Data Structures: Interfaces
    https://research.swtch.com/interfaces
  98. How to use interfaces in Go
    http://jordanorelli.com/pos­t/32665860244/how-to-use-interfaces-in-go
  99. Interfaces in Go (part I)
    https://medium.com/golangspec/in­terfaces-in-go-part-i-4ae53a97479c
  100. Part 21: Goroutines
    https://golangbot.com/goroutines/
  101. Part 22: Channels
    https://golangbot.com/channels/
  102. [Go] Lightweight eventbus with async compatibility for Go
    https://github.com/asaske­vich/EventBus
  103. What about Trait support in Golang?
    https://www.reddit.com/r/go­lang/comments/8mfykl/what_a­bout_trait_support_in_golan­g/
  104. 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/
  105. Control Flow
    https://en.wikipedia.org/wi­ki/Control_flow
  106. Structured programming
    https://en.wikipedia.org/wi­ki/Structured_programming
  107. Control Structures
    https://www.golang-book.com/books/intro/5
  108. Control structures – Go if else statement
    http://golangtutorials.blog­spot.com/2011/06/control-structures-if-else-statement.html
  109. Control structures – Go switch case statement
    http://golangtutorials.blog­spot.com/2011/06/control-structures-go-switch-case.html
  110. Control structures – Go for loop, break, continue, range
    http://golangtutorials.blog­spot.com/2011/06/control-structures-go-for-loop-break.html
  111. Single Function Exit Point
    http://wiki.c2.com/?Single­FunctionExitPoint
  112. Entry point
    https://en.wikipedia.org/wi­ki/Entry_point
  113. Why does Go have a GOTO statement?!
    https://www.reddit.com/r/go­lang/comments/kag5q/why_do­es_go_have_a_goto_statemen­t/
  114. Effective Go
    https://golang.org/doc/ef­fective_go.html
  115. GoClipse: an Eclipse IDE for the Go programming language
    http://goclipse.github.io/
  116. GoClipse Installation
    https://github.com/GoClip­se/goclipse/blob/latest/do­cumentation/Installation.md#in­stallation