V dnešním článku o programovacím jazyce Go se seznámíme s některými užitečnými knihovnami. Kromě základních knihoven (práce s řetězci atd.) si ukážeme některé možnosti nabízené knihovnou nazvanou GoDS neboli Go Data Structures.
V dnešním článku o programovacím jazyce Go se seznámíme s některými užitečnými knihovnami. Kromě základních knihoven (práce s řetězci atd.) si ukážeme některé možnosti nabízené knihovnou nazvanou GoDS neboli Go Data Structures.
1. Užitečné balíčky pro každodenní použití jazyka Go
2. Získání seznamu nainstalovaných balíčků
3. Použití webového prohlížeče pro získání podrobnějších informací o balíčcích
4. Instalace externího balíčku
5. Použití nově nainstalovaného balíčku
6. Funkce pro konverzi řetězců na pravdivostní hodnotu
7. Konverze řetězců na číselné hodnoty
8. Převod popř. naformátování celočíselné hodnoty na řetězec
9. Převod hodnoty s plovoucí čárkou na řetězec
10. Obecné formátování hodnot s jejich uložením do řetězce (Sprintf)
11. Kontejnery implementované ve standardní knihovně
12. Základní operace se seznamy
13. Praktičtější příklad: použití seznamů ve funkci zásobníku pro implementaci RPN
14. Externí knihovna GoDS (Go Data Structures)
15. Datová struktura arraylist, jednosměrně a obousměrně vázané seznamy
16. Implementace zásobníků, přepis a vylepšení příkladu s implementací RPN
17. Množiny, implementace Eratosthenova síta
18. Stromy, struktura Red-Black tree
19. Repositář s demonstračními příklady
V předchozích částech tohoto seriálu jsme se zaměřili především na popis samotného jazyka Go a později i na způsob vytváření a použití nových balíčků (packages). Mnoho demonstračních příkladů používalo některé základní balíčky, které jsou součástí instalace samotného jazyka Go a jeho základní sady nástrojů. Jednalo se především o balíčky fmt, math a time (připomeňme si, že je zvykem, aby názvy balíčků byly jednoslovní a aby byly zapisovány malými písmeny). V praxi je ovšem většinou nutné použít i mnohé další balíčky, a to jak balíčky ze standardní knihovny programovacího jazyka Go, tak i externí balíčky, které implementují další funkcionalitu. Dnes se s některými užitečnými a často používanými balíčky seznámíme. Již na úvod je nutné poznamenat, že se (alespoň prozatím) zaměříme na instalaci a správu balíčků s využitím základních nástrojů jazyka Go. Použitím pokročilejších správců balíčků se budeme zabývat v samostatném článku.
Seznam základních balíčků patřících do standardní knihovny jazyka Go nalezneme i s jejich popisem na stránce https://golang.org/pkg/. Povšimněte si, že balíčky jsou rozděleny do několika kategorií, ovšem prakticky vždy se jedná o základní funkcionalitu (práce se sítí, balíčky umožňující vytvoření HTTP serveru, zpracování datových struktur, volání funkcí operačního systému, kryptografické a hešovací funkce, podpora pro práci s několika rastrovými formáty apod.). Nenajdeme zde tedy například knihovny pro tvorbu grafického uživatelského rozhraní, multimediální funkce, či zpracování XML (kromě jednoduchého parseru představovaného balíčkem encoding/xml). V případě, že budete potřebovat získat seznam aktuálně nainstalovaných balíčků na vašem systému, je pro tento účel možné použít prostředky nabízené vlastním jazykem Go a jeho základní sady nástrojů.
Základním příkazem, který můžeme pro tento účel použít, je příkaz go list. Nápovědu k tomuto příkazu získáme následujícím způsobem:
$ go help list usage: go list [-f format] [-json] [-m] [list flags] [build flags] [packages] ... ... ...
Ukažme si nyní několik praktických příkladů. Pro výpis všech balíčků použijte následující příkaz:
$ go list ...
popř. alternativně:
$ go list all
V našem případě (čistá instalace Go + několik testovacích balíčků) by měl výstup vypadat takto – nejdříve se vypíšou balíčky ze standardní knihovny a po nich šest balíčků, které jsme vytvořili minule:
archive/tar archive/zip bufio bytes cmd/addr2line cmd/api cmd/asm ... ... ... hello1 hello2 hello3 hello4 say_hello_1 say_hello_2
U tohoto příkazu je dokonce možné specifikovat formát výstupu a tím i ovlivnit, jaké informace se zobrazí na standardním výstupu. Následující příkaz zobrazí napřed název balíčku (i s cestou) a poté seznam všech dalších balíčků, které jsou explicitně importovány:
$ go list -f "{{.ImportPath}} {{.Imports}}" all archive/tar [bytes errors fmt io io/ioutil math os os/user path reflect runtime sort strconv strings sync syscall time] archive/zip [bufio compress/flate encoding/binary errors fmt hash hash/crc32 io io/ioutil os path strings sync time unicode/utf8] bufio [bytes errors io unicode/utf8] ... ... ...
V případě, že upravíme balíček say_hello takovým způsobem, aby obsahoval:
package main import "fmt" func main() { fmt.Println("Hello world #1!") }
Projeví se tato změna ihned i ve výpisu balíčků:
$ go list -f "{{.ImportPath}} {{.Imports}}" say_hello_... say_hello_1 [fmt] say_hello_2 []
Všechny závislosti (tedy nikoli jen přímo importované balíčky), lze vypsat například takto:
$ go list -f "{{.ImportPath}} {{.Deps}}" say_hello_... say_hello_1 [errors fmt internal/bytealg internal/cpu internal/poll internal/race internal/syscall/unix internal/testlog io math math/bits os reflect runtime runtime/internal/atomic runtime/internal/sys strconv sync sync/atomic syscall time unicode unicode/utf8 unsafe] say_hello_2 [internal/bytealg internal/cpu runtime runtime/internal/atomic runtime/internal/sys unsafe]
Existuje ještě jedna možnost, jak se dozvědět podrobnější informace o nainstalovaných balíčcích. Můžeme si totiž lokálně spustit webový (HTTP) server, který nám zpřístupní veškerou automaticky generovanou dokumentaci. Samotné spuštění serveru je jednoduché:
$ godoc --http :8080
2019/01/22 21:21:40 ListenAndServe :8080: listen tcp :8080: bind: address already in use
Následně ve webovém prohlížeči otevřete adresu http://localhost:8080/pkg. Pro jednoduchost můžeme zůstat v terminálu a použít Lynx nebo Links:
The Go Programming Language Go ▽ Documents Packages The Project Help Blog ____________________ (BUTTON) Packages Standard library Third party Other packages Sub-repositories Community Standard library ▹ Standard library ▾ Name Synopsis archive tar Package tar implements access to tar archives. zip Package zip provides support for reading and writing ZIP archives.
Obrázek 1: Zobrazení automaticky vygenerované stránky se seznamem balíčků.
Na vygenerované stránce nalezneme i seznam balíčků, které jsme vytvořili v rámci předchozího článku. Ovšem jak je patrné, nejsou tyto balíčky rozumně zdokumentovány:
Third party ▾ Name Synopsis hello1 hello2 hello3 hello4 say_hello_1 say_hello_2 Other packages Sub-repositories These packages are part of the Go Project but outside the main Go tree. They are developed under looser compatibility requirements than the Go core. Install them with "go get".
Obrázek 2: Zobrazení balíčků „třetích stran“, což jsou v tomto okamžiku balíčky, které jsme vytvořili v rámci předchozího článku.
Nyní si ukážeme, jakým způsobem je vlastně možné nainstalovat nějaký externí balíček. Použijeme přitom pouze základní nástroje, které byly nainstalovány společně s překladačem a základními knihovnami programovacího jazyka Go. Balíčky se budou instalovat do adresářové struktury, na níž ukazuje proměnná $GOPATH, což je typicky adresář ~/go. Tento adresář by měl vypadat zhruba následovně:
├── bin ├── pkg └── src ├── hello1 │ └── hello1.go ├── hello2 │ └── hello1.go ├── hello3 │ └── hello1.go ├── hello4 │ └── hello4.go ├── say_hello_1 │ └── hello.go └── say_hello_2 └── hello.go 9 directories, 6 files
Vidíme, že se v adresáři – alespoň prozatím – nachází pouze balíčky vytvořené z demonstračních příkladů popsaných v předchozí části tohoto seriálu.
Pokud například budeme potřebovat vypočítat Levenštejnovu vzdálenost dvou řetězců (což se provádí poměrně často například při implementaci uživatelsky přívětivé funkce Search), můžeme pro tento účel použít knihovnu/balíček s názvem levenshtein a s cestou github.com/agext/levenshtein, která je dostupná na GitHubu, konkrétně na adrese https://github.com/agext/levenshtein (součástí plné cesty balíčku je skutečně i „github.com“).
Balíček agext/levenshtein se instaluje velmi snadno, a to příkazem go get, kterému předáme jméno repositáře s balíčkem (ovšem vynechá se protokol!):
$ go get github.com/agext/levenshtein
Pokud chcete vidět, jaké operace se provádí, přidejte přepínač -v:
$ go get -v github.com/agext/levenshtein github.com/agext/levenshtein (download)
Nyní by měla adresářová struktura ~/go (přesněji řečeno adresář, na který ukazuje proměnná prostředí $GOPATH) vypadat zhruba následovně:
. ├── bin ├── pkg │ └── linux_amd64 │ └── github.com │ └── agext │ └── levenshtein.a └── src ├── github.com │ └── agext │ └── levenshtein │ ├── DCO │ ├── levenshtein.go │ ├── levenshtein_test.go │ ├── LICENSE │ ├── MAINTAINERS │ ├── NOTICE │ ├── params.go │ ├── params_test.go │ └── README.md ├── hello1 │ └── hello1.go ├── hello2 │ └── hello1.go ├── hello3 │ └── hello1.go ├── hello4 │ └── hello4.go ├── say_hello_1 │ └── hello.go └── say_hello_2 └── hello.go 15 directories, 16 files
Povšimněte si, že se balíček nainstaloval jak do podadresáře src (vlastní zdrojové kódy, testy, licence, další dokumentace), tak i do podadresáře pkg (binární knihovna určená pro slinkování s kódem výsledných aplikací). Po instalaci je součástí cesty k balíčku skutečně i prefix github.com, protože zdrojové kódy balíčku leží v podadresáři src/github.com/agext/levenshtein.
Příkaz go list by nyní měl ukázat informace i o nově nainstalované knihovně agext/levenshtein:
$ go list ... ... ... ... github.com/agext/levenshtein ... ... ...
Podobně uvidíme základní informace o balíčku i na dynamicky generovaných stránkách s dokumentací:
Obrázek 3: Automaticky vygenerované stránky s dokumentací k balíčku github.com/agext/levenshtein.
Po instalaci balíčku si můžeme zobrazit jeho nápovědu, a to opět přímo na příkazovém řádku:
$ go doc levenshtein
Začátek nápovědy pro celý balíček:
package levenshtein // import "github.com/agext/levenshtein" Package levenshtein implements distance and similarity metrics for strings, based on the Levenshtein measure. The underlying `Calculate` function is also exported, to allow the building of other derivative metrics, if needed. func Calculate(str1, str2 []rune, maxCost, insCost, subCost, delCost int) (dist, prefixLen, suffixLen int) func Distance(str1, str2 string, p *Params) int func Match(str1, str2 string, p *Params) float64 func Similarity(str1, str2 string, p *Params) float64 type Params struct{ ... } func NewParams() *Params
Nápovědu si můžeme zobrazit i pro libovolnou exportovanou funkci popř. pro datový typ (exportují se pouze ty objekty, které začínají velkým písmenem):
$ go doc levenshtein.Distance package levenshtein // import "github.com/agext/levenshtein" func Distance(str1, str2 string, p *Params) int Distance returns the Levenshtein distance between str1 and str2, using the default or provided cost values. Pass nil for the third argument to use the default cost of 1 for all three operations, with no maximum.
V testovacím příkladu se balíček naimportuje, přičemž při importu musíme použít celou cestu, a to včetně prefixu „github.com“. Po importu můžeme zavolat funkci levenshtein.Distance() a zjistit vzdálenost mezi dvěma řetězci. Posledním parametrem této funkce jsou případné další parametry, jejichž význam si můžete přečíst v nápovědě (my je ovšem nebudeme potřebovat ani nastavovat, takže v posledním parametru předáme pouze nil):
package main import ( "github.com/agext/levenshtein" ) func main() { s1 := "Hello" s2 := "hello" println(levenshtein.Distance(s1, s2, nil)) s1 = "Marta" s2 = "Markéta" println(levenshtein.Distance(s1, s2, nil)) s1 = " foo" s2 = "jiný naprosto odlišný text nesouvisející s foo" println(levenshtein.Distance(s1, s2, nil)) }
Po překladu a spuštění tohoto příkladu by se na standardní výstup měly vypsat tyto tři Levenštejnovy vzdálenosti:
1 2 42
Nyní si popíšeme některé často používané funkce, které jsou používány prakticky dennodenně a které nalezneme ve standardní knihovně programovacího jazyka Go. Mnohokrát se setkáme s potřebou konvertovat řetězec obsahující pravdivostní hodnotu zapsanou ve formě textu na skutečnou pravdivostní hodnotu typu bool. K tomuto účelu se používá funkce nazvaná ParseBool, kterou nalezneme v balíčku strconv. Této funkci se předá řetězec a po konverzi se vrátí dvě hodnoty – vlastní pravdivostní hodnota typu bool a hodnota typu error, která může nést informace o případné chybě (jak je zvykem, pokud k chybě nedošlo, vrací se nil). Zbývá doplnit informaci o tom, jak se konkrétně provádí konverze:
Řetězec | Výsledek |
---|---|
„true“ | true, error==nil |
„True“ | true, error==nil |
„TRUE“ | true, error==nil |
„t“ | true, error==nil |
„T“ | true, error==nil |
„1“ | true, error==nil |
„false“ | false, error==nil |
„False“ | false, error==nil |
„FALSE“ | false, error==nil |
„f“ | false, error==nil |
„F“ | false, error==nil |
„0“ | false, error==nil |
jiná hodnota | chyba, error!=nil |
Chování této funkce je tedy snadno pochopitelné, takže si ji odzkoušíme na tomto demonstračním příkladu:
package main import ( "fmt" "strconv" ) func tryToParseBool(s string) { i, err := strconv.ParseBool(s) if err == nil { fmt.Printf("%t\n", i) } else { fmt.Printf("%v\n", err) } } func main() { tryToParseBool("true") tryToParseBool("True") tryToParseBool("TRUE") tryToParseBool("T") tryToParseBool("t") tryToParseBool("false") tryToParseBool("False") tryToParseBool("FALSE") tryToParseBool("F") tryToParseBool("f") tryToParseBool("Foobar") tryToParseBool("0") tryToParseBool("1") tryToParseBool("no") tryToParseBool("") }
Po překladu a spuštění by se na standardním výstupu měly objevit následující řádky s výsledky konverze:
true true true true true false false false false false strconv.ParseBool: parsing "Foobar": invalid syntax false true strconv.ParseBool: parsing "no": invalid syntax strconv.ParseBool: parsing "": invalid syntax
Velmi často se v praxi setkáme s nutností získat číselné hodnoty uložené v řetězcích. Může se například jednat o zpracování vstupů z příkazové řádky, zpracování zpráv předaných přes sockety atd. K tomuto účelu nám standardní knihovna programovacího jazyka Go nabízí několik funkcí, které jsou vypsány v tabulce. Až na poslední funkci nalezneme ostatní funkce v balíčku strconv, podobně jako výše popsanou funkci ParseBool:
Funkce | Stručný popis |
---|---|
ParseInt | převod řetězce na celé číslo se znaménkem, je možné specifikovat základ číselné soustavy |
ParseUint | převod řetězce na celé číslo bez znaménka, opět je možné specifikovat základ číselné soustavy |
ParseFloat | převod řetězce na číslo s plovoucí řádovou čárkou |
Atoi | odpovídá funkci ParseInt se základem 10 a bitovou šířkou typu int |
Scanf | na základě řetězce se specifikací formátu vstupu se snaží ve vstupním řetězci nalézt číselné popř. i další hodnoty |
První funkcí, kterou si popíšeme, je funkce ParseInt sloužící pro převod řetězce na celé číslo se znaménkem, přesněji řečeno na hodnotu typu int64 (nikoli int). Této funkci se předávají tři parametry:
Tato funkce opět vrací dvě hodnoty – vlastní číslo a informaci o chybě. Pokud se konverze podařila, bude druhá vrácená hodnota obsahovat nil.
Podívejme se nyní na jednoduchý demonstrační příklad s konverzí různých typů řetězců:
package main import ( "fmt" "strconv" ) func tryToParseInteger(s string, base int) { i, err := strconv.ParseInt(s, base, 32) if err == nil { fmt.Printf("%d\n", i) } else { fmt.Printf("%v\n", err) } } func main() { tryToParseInteger("42", 10) tryToParseInteger("42", 0) tryToParseInteger("42", 16) tryToParseInteger("42", 2) tryToParseInteger("42x", 10) println() tryToParseInteger("-42", 10) tryToParseInteger("-42", 0) tryToParseInteger("-42", 16) tryToParseInteger("-42", 2) tryToParseInteger("-42x", 10) println() }
Po spuštění a překladu získáme tyto číselné hodnoty (popř. informace o chybě):
42 42 66 strconv.ParseInt: parsing "42": invalid syntax strconv.ParseInt: parsing "42x": invalid syntax -42 -42 -66 strconv.ParseInt: parsing "-42": invalid syntax strconv.ParseInt: parsing "-42x": invalid syntax
K funkci ParseInt existuje i její alternativa pojmenovaná ParseUint určená pro převod celých čísel bez znaménka, konkrétně se vrací hodnoty typu uint64. Opět se podívejme na jednoduchý demonstrační příklad, ve kterém se tato funkce použije:
package main import ( "fmt" "strconv" ) func tryToParseUnsignedInteger(s string, base int) { i, err := strconv.ParseUint(s, base, 32) if err == nil { fmt.Printf("%d\n", i) } else { fmt.Printf("%v\n", err) } } func main() { tryToParseUnsignedInteger("42", 10) tryToParseUnsignedInteger("42", 0) tryToParseUnsignedInteger("42", 16) tryToParseUnsignedInteger("42", 2) tryToParseUnsignedInteger("42x", 10) println() tryToParseUnsignedInteger("-42", 10) tryToParseUnsignedInteger("-42", 0) tryToParseUnsignedInteger("-42", 16) tryToParseUnsignedInteger("-42", 2) tryToParseUnsignedInteger("-42x", 10) println() }
Výsledky získané po spuštění tohtoo příklad
42 42 66 strconv.ParseUint: parsing "42": invalid syntax strconv.ParseUint: parsing "42x": invalid syntax strconv.ParseUint: parsing "-42": invalid syntax strconv.ParseUint: parsing "-42": invalid syntax strconv.ParseUint: parsing "-42": invalid syntax strconv.ParseUint: parsing "-42": invalid syntax strconv.ParseUint: parsing "-42x": invalid syntax
Třetí funkce určená pro získání celočíselné hodnoty z řetězce se jmenuje Atoi. Její chování se podobá podobně pojmenované céčkovské funkci atoi(3): vstupem je pouze řetězec, protože jak základ číselné soustavy, tak i bitová šířka výsledku jsou pevně nastavené (základ je odvozen od prefixu čísla v řetězci, šířka odpovídá typu int). Funkce je to tedy velmi jednoduchá na použití, což je ostatně patrné i při pohledu na dnešní pátý demonstrační příklad:
package main import ( "fmt" "strconv" ) func tryToParseInteger(s string) { i, err := strconv.Atoi(s) if err == nil { fmt.Printf("%d\n", i) } else { fmt.Printf("%v\n", err) } } func main() { tryToParseInteger("42") tryToParseInteger("42x") tryToParseInteger("") println() tryToParseInteger("-42") tryToParseInteger("-42x") tryToParseInteger("-") println() }
Samozřejmě se opět podíváme na výsledky tohoto příkladu:
42 strconv.Atoi: parsing "42x": invalid syntax strconv.Atoi: parsing "": invalid syntax -42 strconv.Atoi: parsing "-42x": invalid syntax strconv.Atoi: parsing "-": invalid syntax
Předposlední funkcí z tabulky uvedené na začátku této kapitoly je funkce ParseFloat, která se snaží v řetězci nalézt textovou reprezentaci číselné hodnoty s desetinnou tečkou popř. s desítkovým exponentem. Prvním parametrem této funkce je vstupní řetězec, druhým parametrem pak určení, zda se má provádět konverze odpovídající typu float32 nebo float64:
package main import ( "fmt" "strconv" ) func tryToParseFloat(s string) { i, err := strconv.ParseFloat(s, 32) if err == nil { fmt.Printf("%f\n", i) } else { fmt.Printf("%v\n", err) } } func main() { tryToParseFloat("42") tryToParseFloat("42.0") tryToParseFloat(".5") tryToParseFloat("0.5") tryToParseFloat("5e10") tryToParseFloat(".5e10") tryToParseFloat(".5e-5") tryToParseFloat("-.5e-5") tryToParseFloat("5E10") tryToParseFloat(".5E10") tryToParseFloat(".5E-5") tryToParseFloat("-.5E-5") }
Po spuštění předchozího příkladu se můžeme přesvědčit, že všechny vstupní řetězce skutečně reprezentují hodnoty odpovídající IEEE 754:
42.000000 42.000000 0.500000 0.500000 49999998976.000000 5000000000.000000 0.000005 -0.000005 49999998976.000000 5000000000.000000 0.000005 -0.000005
Pro zpětný převod celého čísla na řetězec můžeme v tom nejjednodušším případě (žádné speciální požadavky na výsledné naformátování) použít funkci Itoa, kterou opět nalezneme v balíčku strconv. Použití této funkce je velmi přímočaré, jak je to ostatně patrné i při pohledu na další demonstrační příklad:
package main import ( . "strconv" ) func main() { println(Itoa(42)) println(Itoa(0)) println(Itoa(-1)) }
Výsledkem jsou skutečně řetězce vypsané na standardní výstup (i když nutno říci, že stejně by se vypsaly i přímo celočíselné hodnoty):
42 0 -1
Poněkud více možností nám nabízí funkce FormatInt, taktéž z balíčku strconv. V této funkci můžeme specifikovat základ číselné soustavy použité pro konverzi čísla na řetězec. Podporovány jsou základy od 2 (klasická binární soustava) až po 36, což je opět omezení dané tím, že jednotlivé cifry budou reprezentovány znaky 0–9 a poté a-z. V případě, že budete potřebovat použít velká písmena, postačuje výstup z funkce FormatInt předat do funkce ToUpper z balíčku strings. Opět se podívejme na příklad, který funkci FormatInt použije:
package main import ( . "strconv" ) func main() { value := int64(0xcafebabe) for base := 2; base < 36; base++ { println(base, FormatInt(value, base)) } }
Výsledkem běhu tohoto programu jsou všechny možné reprezentace desítkové hodnoty 3405691582 (neboli 0×cafebabe šestnáctkově):
2 11001010111111101011101010111110 3 22210100102001120021 4 3022333223222332 5 23433324112312 6 1321535442354 7 150252620656 8 31277535276 9 8710361507 10 3405691582 11 1498473547 12 7b06863ba 13 423769627 14 24444d366 15 14decdc07 16 cafebabe 17 851a7gfg 18 5a26a5dg 19 3f781gd7 20 2d45b8j2 21 1ieiebdd 22 180i76ii 23 10032471 24 hjh0inm 25 dnie6d7 26 b0ghb7k 27 8l9b1f7 28 71omia6 29 5l15dnm 30 4k4glmm 31 3ptmepo 32 35ftelu 33 2l0pcr7 34 26whxxg 35 1tti1dr
Pro převod hodnot typu float32 nebo float64 na řetězec je možné použít funkci nazvanou FormatFloat. Této funkci se předávají čtyři parametry:
Specifikace formátu:
Znak | Výsledek |
---|---|
‚b‘ | -ddddp±ddd (dvojkový exponent) |
‚e‘ | -d.dddde±dd (desítkový exponent) |
‚E‘ | -d.ddddE±dd (desítkový exponent) |
‚f‘ | -ddd.dddd |
‚g‘ | buď ‚e‘ nebo ‚f‘ |
‚G‘ | buď ‚E‘ nebo ‚F‘ |
Funkci FormatFloat si samozřejmě opět otestujeme, a to v následujícím demonstračním příkladu:
package main import ( "math" . "strconv" ) func main() { value := math.Pi println(FormatFloat(value, 'f', 5, 64)) println(FormatFloat(value, 'f', 10, 64)) println(FormatFloat(value, 'e', 10, 64)) println(FormatFloat(value, 'g', 10, 64)) println(FormatFloat(value, 'b', 10, 64)) println() value = 1e20 println(FormatFloat(value, 'f', 5, 64)) println(FormatFloat(value, 'f', 10, 64)) println(FormatFloat(value, 'e', 10, 64)) println(FormatFloat(value, 'g', 10, 64)) println(FormatFloat(value, 'b', 10, 64)) println() value = 0 println(FormatFloat(value, 'f', 5, 64)) println(FormatFloat(value, 'f', 10, 64)) println(FormatFloat(value, 'e', 10, 64)) println(FormatFloat(value, 'g', 10, 64)) println(FormatFloat(value, 'b', 5, 64)) }
Výsledkem budou následující řádky vypsané na standardní výstup:
3.14159 3.1415926536 3.1415926536e+00 3.141592654 7074237752028440p-51 100000000000000000000.00000 100000000000000000000.0000000000 1.0000000000e+20 1e+20 6103515625000000p+14 0.00000 0.0000000000 0.0000000000e+00 0 0p-1074
Již mnohokrát jsme se setkali s funkcí fmt.Printf, která slouží pro naformátování hodnot s určením jejich šířky, zarovnání, umístění znaménka atd. a pro následný výpis těchto hodnot na standardní výstup. Ovšem jakou funkci máme použít ve chvíli, kdy sice potřebujeme hodnoty naformátovat, ale potom je například odeslat na webovou stránku, uložit do souboru atd.? Namísto fmt.Printf se v takovém případě použije funkce fmt.Sprintf, což je možná trošku překvapivé, protože ostatní konverzní funkce nalezneme v balíčku strconv a nikoli fmt. Funkce fmt.Printf a fmt.Sprintf mají podobné vlastnosti a stejná pravidla pro formátovací řetězce, takže si dnes bez větších podrobností pouze ukážeme příklady jejího použití.
Následuje demonstrační příklad ukazující použití funkce Sprintf:
package main import ( . "fmt" "math" ) func main() { value := 42 s := Sprintf("%10d", value) println(s) s = Sprintf("%10.5f", math.Pi) println(s) s = Sprintf("%10.9f", math.Pi) println(s) a := []int{10, 20, 30} s = Sprintf("%v", a) println(s) }
Po spuštění tohoto příkladu by se měly zobrazit přesně tyto řádky:
42 3.14159 3.141592654 [10 20 30]
Se základními datovými strukturami programovacího jazyka Go jsme se již setkali v předchozích částech tohoto seriálu. Připomeňme si, že Go podporuje jeden typ lineární kolekce (neboli kontejneru) a tím je pole (array) tvořící základ pro řezy (slice). Teoreticky je možné do této skupiny počítat i kanály, které při vhodném použití nahradí frontu (queue), ovšem další typy kolekcí/kontejnerů přímo v programovacím jazyku nenalezneme. Některé alespoň základní typy kontejnerů je však možné najít v základní knihovně jazyka Go. Jedná se o tyto balíčky:
Balíček | Implementovaná datová struktura/kontejner |
---|---|
container/list | obousměrně vázaný seznam |
container/heap | halda (lze použít například pro implementaci prioritní fronty) |
container/ring | cyklická fronta |
Do těchto kontejnerů se ukládají prvky typu interface{}, což v programovacím jazyku Go de facto znamená „hodnoty neznámého typu“. Pokud z kontejneru nějakou hodnotu přečteme, musí se většinou explicitně specifikovat, jaký typ prvků očekáváme. K tomuto účelu se v Go používají takzvané typové aserce, které se zapisují následujícím způsobem:
i := x.(T) i, ok := x.(T)
kde se za T doplní konkrétní datový typ, například int nebo string. Ve druhém případě se do nové proměnné ok zapíše pravdivostní hodnota značící, zda se operace provedla či nikoli.
Příklad použití, zde konkrétně ukázaný na příkladu obousměrně vázaného seznamu:
l := list.New() l.PushBack(42) i := l.Front().Value.(int)
Ukažme si nyní jednoduchý demonstrační příklad, na němž si vysvětlíme základní operace se seznamy. V tomto příkladu se seznam zkonstruuje takto:
l := list.New()
Povšimněte si, že není nutné (a vlastně ani možné) specifikovat typ položek seznamu!
l.PushBack("foo") l.PushBack("bar") l.PushBack("baz")
Dále se podívejme na způsob implementace průchodu všemi prvky seznamu. Nejprve získáme ukazatel na první prvek s využitím metody List.Front a následně voláme metodu Element.Next (nikoli List.Next!) tak dlouho, až dojdeme na konec seznamu. Datový typ Element je definován jako struktura obalující hodnotu Value typu interface{}. A hodnotu tohoto typu můžeme snadno vytisknout s využitím fmt.Println:
func printList(l *list.List) { for e := l.Front(); e != nil; e = e.Next() { fmt.Println(e.Value) } }
Úplný zdrojový kód tohoto příkladu vypadá následovně:
package main import ( "container/list" "fmt" ) func printList(l *list.List) { for e := l.Front(); e != nil; e = e.Next() { fmt.Println(e.Value) } } func main() { l := list.New() l.PushBack("foo") l.PushBack("bar") l.PushBack("baz") printList(l) }
Výsledky:
foo bar baz
Obousměrně vázané seznamy je možné využít i ve funkci zásobníku. Stačí se pouze rozhodnout, který konec seznamu bude sloužit jako vrchol zásobníku (TOS – Top of Stack). Pokud zvolíme, že TOS bude ležet na konci seznamu, můžeme si vytvořit dvě pomocné funkce pro dvě základní operace se zásobníkem – push a pop. V případě, že zásobník bude obsahovat pouze celá čísla, může implementace obou zmíněných funkcí vypadat následovně.
func push(l *list.List, number int) { l.PushBack(number) }
func pop(l *list.List) int { tos := l.Back() l.Remove(tos) return tos.Value.(int) }
Tento zásobník nyní použijeme pro implementaci jednoduchého kalkulátoru, který bude pracovat s výrazy zapsanými v obrácené polské notaci (RPN), kterou jsme se zabývali například v článku Programovací jazyky z vývojářského pekla++. Samotná implementace vyhodnocení RPN výrazů je relativně jednoduchá a spočívá v tom, že na zásobník umisťujeme hodnoty a ve chvíli, kdy se ve výrazu nachází operátor, přečteme ze zásobníku dvě hodnoty, provedeme příslušnou operaci a na zásobník uložíme výsledek:
expression := "1 2 + 2 3 * 8 + *" terms := strings.Split(expression, " ") stack := list.New() for _, term := range terms { switch term { case "+": operand1 := pop(stack) operand2 := pop(stack) push(stack, operand1+operand2) ... ... ... default: number, err := strconv.Atoi(term) if err == nil { push(stack, number) } }
Pokud je zapsaný výraz korektní, bude po jeho vyhodnocení na zásobníku uložena jediná hodnota, a to výsledek celého výrazu:
printStack(stack)
Následuje výpis úplného zdrojového kódu tohoto demonstračního příkladu:
package main import ( "container/list" "fmt" "strconv" "strings" ) type Stack list.List func printStack(l *list.List) { for e := l.Front(); e != nil; e = e.Next() { fmt.Println(e.Value) } } func push(l *list.List, number int) { l.PushBack(number) } func pop(l *list.List) int { tos := l.Back() l.Remove(tos) return tos.Value.(int) } func main() { expression := "1 2 + 2 3 * 8 + *" terms := strings.Split(expression, " ") stack := list.New() for _, term := range terms { switch term { case "+": operand1 := pop(stack) operand2 := pop(stack) push(stack, operand1+operand2) case "-": operand1 := pop(stack) operand2 := pop(stack) push(stack, operand2-operand1) case "*": operand1 := pop(stack) operand2 := pop(stack) push(stack, operand1*operand2) case "/": operand1 := pop(stack) operand2 := pop(stack) push(stack, operand2/operand1) default: number, err := strconv.Atoi(term) if err == nil { push(stack, number) } } } printStack(stack) }
Výsledkem by měla být hodnota 42:
42
Nepatrnou úpravou tohoto programu získáme nástroj, který nejenže daný RPN výraz vypočítá, ale bude v průběhu výpočtu vypisovat i aktuální obsah zásobníku. Úprava spočívá v přidání zvýrazněných řádků:
package main import ( "container/list" "fmt" "strconv" "strings" ) type Stack list.List func printStack(l *list.List) { for e := l.Front(); e != nil; e = e.Next() { fmt.Printf("%2d ", e.Value) } println() } func push(l *list.List, number int) { l.PushBack(number) } func pop(l *list.List) int { tos := l.Back() l.Remove(tos) return tos.Value.(int) } func main() { expression := "1 2 + 2 3 * 8 + *" terms := strings.Split(expression, " ") stack := list.New() for _, term := range terms { switch term { case "+": operand1 := pop(stack) operand2 := pop(stack) push(stack, operand1+operand2) print("+ : ") printStack(stack) case "-": operand1 := pop(stack) operand2 := pop(stack) push(stack, operand2-operand1) print("- : ") printStack(stack) case "*": operand1 := pop(stack) operand2 := pop(stack) push(stack, operand1*operand2) print("* : ") printStack(stack) case "/": operand1 := pop(stack) operand2 := pop(stack) push(stack, operand2/operand1) print("/ : ") printStack(stack) default: number, err := strconv.Atoi(term) if err == nil { push(stack, number) } fmt.Printf("%-2d: ", number) printStack(stack) } } print("Result: ") printStack(stack) }
Nyní se na standardní výstup vypisuje stav zásobníku při průběžném vyhodnocování výrazu. Opět můžeme vidět, že u korektního výrazu se nakonec na zásobníku objeví jen jediná hodnota:
1 : 1 2 : 1 2 + : 3 2 : 3 2 3 : 3 2 3 * : 3 6 8 : 3 6 8 + : 3 14 * : 42 Result: 42
Kontejnery (kolekce) nabízené samotným jazykem Go popř. jeho standardní knihovnou nám v praxi většinou nebudou postačovat, takže bude nutné sáhnout po externí knihovně. Těch existuje větší množství, ovšem pravděpodobně nejúplnější sadu kontejnerů nalezneme v knihovně pojmenované GoDS neboli plným jménem Go Data Structures. Dnes si ukážeme jen základní možnosti nabízené touto knihovnou, podrobnosti budou uvedeny příště.
Tuto knihovnu nainstalujeme příkazem:
$ go get github.com/emirpasic/gods
Knihovna by se ihned poté měla objevit v adresářové struktuře odkazované proměnnou $GOPATH:
. ├── github.com │ ├── agext │ │ └── levenshtein ... ... ... │ └── emirpasic │ └── gods │ ├── containers │ │ ├── containers.go │ │ ├── containers_test.go │ │ ├── enumerable.go │ │ ├── iterator.go │ │ └── serialization.go │ ├── examples │ │ ├── arraylist │ │ │ └── arraylist.go ... ... ... │ └── utils │ ├── comparator.go │ ├── comparator_test.go │ ├── sort.go │ ├── sort_test.go │ ├── utils.go │ └── utils_test.go
Pro jistotu si ještě vypišme všechny balíčky obsahující „gods“:
$ go list ...gods... github.com/emirpasic/gods/containers github.com/emirpasic/gods/examples/arraylist github.com/emirpasic/gods/examples/arraystack ... ... ... github.com/emirpasic/gods/lists github.com/emirpasic/gods/lists/arraylist github.com/emirpasic/gods/lists/doublylinkedlist github.com/emirpasic/gods/lists/singlylinkedlist github.com/emirpasic/gods/maps github.com/emirpasic/gods/maps/hashbidimap github.com/emirpasic/gods/maps/hashmap github.com/emirpasic/gods/maps/linkedhashmap github.com/emirpasic/gods/maps/treebidimap github.com/emirpasic/gods/maps/treemap github.com/emirpasic/gods/sets github.com/emirpasic/gods/sets/hashset github.com/emirpasic/gods/sets/linkedhashset github.com/emirpasic/gods/sets/treeset github.com/emirpasic/gods/stacks github.com/emirpasic/gods/stacks/arraystack github.com/emirpasic/gods/stacks/linkedliststack github.com/emirpasic/gods/trees github.com/emirpasic/gods/trees/avltree github.com/emirpasic/gods/trees/binaryheap github.com/emirpasic/gods/trees/btree github.com/emirpasic/gods/trees/redblacktree github.com/emirpasic/gods/utils
Seznamy popsané rozhraním List existují v knihovně GoDS ve třech implementacích:
U všech seznamů máme k dispozici iterátor (List.Iterator), který se používá takto:
iterator := list.Iterator() for iterator.Next() { index, value := iterator.Index(), iterator.Value() fmt.Printf("item #%d == %s\n", index, value) }
Použití seznamů všech tří typů je snadné, pouze nesmíme zapomenout na případnou typovou aserci. Příklad použití operací Add, Remove a Swap:
package main import ( "fmt" "github.com/emirpasic/gods/lists/arraylist" ) func printList(list *arraylist.List) { iterator := list.Iterator() for iterator.Next() { index, value := iterator.Index(), iterator.Value() fmt.Printf("item #%d == %s\n", index, value) } fmt.Println() } func main() { list := arraylist.New() list.Add("a") list.Add("c", "b") printList(list) list.Swap(0, 1) list.Swap(1, 2) printList(list) list.Remove(2) printList(list) list.Remove(1) printList(list) }
Výsledek po spuštění příkladu:
item #0 == a item #1 == c item #2 == b item #0 == c item #1 == b item #2 == a item #0 == c item #1 == b item #0 == c
Prakticky stejným způsobem můžeme napsat program používající jednosměrně vázaný seznam:
package main import ( "fmt" sll "github.com/emirpasic/gods/lists/singlylinkedlist" ) func printList(list *sll.List) { iterator := list.Iterator() for iterator.Next() { index, value := iterator.Index(), iterator.Value() fmt.Printf("item #%d == %s\n", index, value) } fmt.Println() } func main() { list := sll.New() list.Add("a") list.Add("c", "b") printList(list) list.Swap(0, 1) list.Swap(1, 2) printList(list) list.Remove(2) printList(list) list.Remove(1) printList(list) }
Výsledek činnosti tohoto programu je odlišný:
item #0 == a item #1 == c item #2 == b item #0 == c item #1 == b item #2 == a item #0 == c item #1 == b item #0 == c
První variantu RPN kalkulačky, s níž jsme se seznámili ve třinácté kapitole můžeme snadno přepsat s využitím kontejneru nazvaného arraystack, což je implementace zásobníku realizovaná nad polem (které se v případě potřeby realokuje). Povšimněte si triku u příkazu import, v níž se na arraystack budeme odkazovat přes obecnější alias stack:
package main import ( "fmt" stack "github.com/emirpasic/gods/stacks/arraystack" "strconv" "strings" ) func printStack(s *stack.Stack) { it := s.Iterator() for it.Next() { value := it.Value() fmt.Printf("%3d ", value) } println() } func main() { expression := "1 2 + 2 3 * 8 + *" terms := strings.Split(expression, " ") stack := stack.New() for _, term := range terms { switch term { case "+": operand1, _ := stack.Pop() operand2, _ := stack.Pop() stack.Push(operand1.(int) + operand2.(int)) print("+ :\t") printStack(stack) case "-": operand1, _ := stack.Pop() operand2, _ := stack.Pop() stack.Push(operand2.(int) - operand1.(int)) print("- :\t") printStack(stack) case "*": operand1, _ := stack.Pop() operand2, _ := stack.Pop() stack.Push(operand1.(int) * operand2.(int)) print("* :\t") printStack(stack) case "/": operand1, _ := stack.Pop() operand2, _ := stack.Pop() stack.Push(operand2.(int) / operand1.(int)) print("/ :\t") printStack(stack) default: number, err := strconv.Atoi(term) if err == nil { stack.Push(number) } fmt.Printf("%-2d:\t", number) printStack(stack) } } print("Result: ") printStack(stack) }
Při spuštění tohoto příkladu se bude postupně vypisovat obsah zásobníku operandů:
1 : 1 2 : 2 1 + : 3 2 : 2 3 3 : 3 2 3 * : 6 3 8 : 8 6 3 + : 14 3 * : 42 Result: 42
Další často používanou datovou strukturou jsou množiny přestavované rozhraním Set. Pro toto rozhraní existují tři implementace:
Jako příklad praktičtějšího použití množin jsem vybral známý algoritmus pro hledání prvočísel s využitím Eratosthenova síta. Samozřejmě existuje mnoho více či méně optimalizovaných implementací tohoto algoritmu; některé jsou zmíněny na stránce Rosetta Code. My si ukážeme tu nejprimitivnější implementaci spočívající v postupném odstraňování celočíselných násobků prvočísel z množiny, která původně obsahovala všechna celá čísla od 2 do 1000.
Následuje výpis zdrojového kódu tohoto demonstračního příkladu:
package main import ( "fmt" "github.com/emirpasic/gods/sets/linkedhashset" ) const maxPrime = 1000 func printSet(set *linkedhashset.Set) { iterator := set.Iterator() for iterator.Next() { index, value := iterator.Index(), iterator.Value() fmt.Printf("%3d ", value) if index%10 == 9 { fmt.Println() } } fmt.Println() } func main() { primes := linkedhashset.New() for n := 2; n < maxPrime; n++ { primes.Add(n) } for n := 2; n < maxPrime/2; n++ { if primes.Contains(n) { for k := 2 * n; k < maxPrime; k += n { primes.Remove(k) } } } printSet(primes) }
Tento příklad by měl po svém spuštění vypsat všechna prvočísla nalezená v intervalu 0 až 1000:
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331 337 347 349 353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449 457 461 463 467 479 487 491 499 503 509 521 523 541 547 557 563 569 571 577 587 593 599 601 607 613 617 619 631 641 643 647 653 659 661 673 677 683 691 701 709 719 727 733 739 743 751 757 761 769 773 787 797 809 811 821 823 827 829 839 853 857 859 863 877 881 883 887 907 911 919 929 937 941 947 953 967 971 977 983 991 997
Jako malá reklama na další pokračování tohoto seriálu bude sloužit příklad, v němž je použita datová struktura pojmenovaná červeno-černý strom neboli Red-Black Tree, popř. RB-Tree. V příkladu vložíme do stromu několik dvojic klíč-hodnota a pokaždé si necháme vypsat strukturu stromu:
package main import ( "fmt" rbt "github.com/emirpasic/gods/trees/redblacktree" ) func main() { tree := rbt.NewWithIntComparator() fmt.Println(tree) tree.Put(1, "G") fmt.Println(tree) tree.Put(2, "a") tree.Put(3, "b") fmt.Println(tree) tree.Put(4, "a") tree.Put(5, "b") fmt.Println(tree) tree.Put(6, "a") tree.Put(7, "b") fmt.Println(tree) tree.Put(8, "a") tree.Put(9, "b") fmt.Println(tree) }
Po spuštění příkladu se bude postupně vykreslovat tvar stromu, přičemž kořen je zde umístěn v levém sloupci (stromy se ovšem v informatice obecně kreslí s kořenem nahoře :-):
RedBlackTree RedBlackTree └── 1 RedBlackTree │ ┌── 3 └── 2 └── 1 RedBlackTree │ ┌── 5 │ ┌── 4 │ │ └── 3 └── 2 └── 1 RedBlackTree │ ┌── 7 │ ┌── 6 │ │ └── 5 │ ┌── 4 │ │ └── 3 └── 2 └── 1 RedBlackTree │ ┌── 9 │ ┌── 8 │ │ └── 7 │ ┌── 6 │ │ └── 5 └── 4 │ ┌── 3 └── 2 └── 1
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:
Pavel Tišnovský vystudoval VUT FIT a v současné době pracuje ve společnosti Red Hat, kde vyvíjí nástroje pro OpenShift.io.
Internet Info Root.cz (www.root.cz)
Informace nejen ze světa Linuxu. ISSN 1212-8309
Copyright © 1998 – 2019 Internet Info, s.r.o. Všechna práva vyhrazena.