Obsah
1. Všechna klíčová slova jazyka Go s odkazy na podrobnější popis
2. Další standardní identifikátory, jména datových typů a funkcí
5. Vytvoření vlastního balíčku, export objektů
7. Adresářová struktura, na níž ukazuje proměnná GOPATH
8. Balíčky se spustitelným kódem
9. Chybějící datový typ enum a způsob jeho náhrady jinou konstrukcí
10. První krok: nový celočíselný typ použitý s identifikátorem iota
11. Převod zvolených celočíselných konstant na řetězec
12. Využití struktury/záznamu pro typově zabezpečené operace s výčtem
13. Kanály s předdefinovanou maximální kapacitou fronty
14. Použití konstrukce range při čtení dat z kanálu
15. Chování uzavřeného kanálu při čtení hodnot
16. Uzavření kanálu jako součást synchronizační konstrukce
17. Konstrukce select s definicí maximální doby čekání na data
18. Definice čekání na data a větev default
19. Repositář s demonstračními příklady
1. Všechna klíčová slova jazyka Go s odkazy na podrobnější popis
V předchozím článku jsme prakticky dokončili popis všech důležitých vlastností programovacího jazyka Go (minimálně všech syntaktických konstrukcí dostupných v současné stabilní verzi), takže si pro úplnost ještě jednou ukažme tabulku se všemi klíčovými slovy tohoto jazyka společně s odkazy na články a kapitoly, ve kterých jsme se popisu těchto klíčových slov věnovali podobněji:
break | default | func | interface | select |
case | defer | go | map | struct |
chan | else | goto | package | switch |
const | fallthrough | if | range | type |
continue | for | import | return | var |
2. Další standardní identifikátory, jména datových typů a funkcí
Kromě těchto klíčových slov se v Go setkáme s několika identifikátory, které mají pevný význam. Typicky se jedná o konstanty, v jednom případě o „automaticky měněnou konstantu“ a o pojmenování standardních datových typů. Jedná se o následující slova:
Identifikátor | Typ | Stručný popis |
---|---|---|
true | konstanta | pravdivostní hodnota |
false | konstanta | pravdivostní hodnota |
iota | konstanta | celočíselný automaticky zvyšovaný čítač |
nil | konstanta | prázdná hodnota, prázdné rozhraní |
bool | datový typ | logický/pravdivostní typ |
byte | datový typ | alias pro typ uint8 |
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 |
float32 | datový typ | číslo s jednoduchou přesností podle IEEE 754 |
float64 | datový typ | číslo s dvojitou přesností podle IEEE 754 |
complex64 | datový typ | dvojice hodnot s jednoduchou přesností |
complex128 | datový typ | dvojice hodnot s dvojitou přesností |
error | datový typ | rozhraní s předpisem metody Error |
rune | datový typ | alias pro typ int32 |
string | datový typ | |
uintptr | datový typ | používáno pro uložení adresy (ukazatele) |
Dále mají všechny moduly vytvářené v programovacím jazyku Go k dispozici několik standardních funkcí, které není zapotřebí žádným způsobem importovat. Jména těchto funkcí sice nejsou striktně rezervována, ovšem většinou není vhodné deklarovat funkci stejného jména s odlišným chováním:
Funkce | Funkce | Funkce |
---|---|---|
append | cap | close |
complex | copy | delete |
imag | len | make |
new | panic | |
println | real | recover |
3. Klíčové slovo package
S klíčovým slovem package jsme se setkali již v úvodní části tohoto seriálu. Toto slovo musí být použito v každém zdrojovém kódu, protože slouží ke specifikaci balíčku, ke kterému daný zdrojový soubor přísluší. Za tímto klíčovým slovem se zapisuje název balíčku, pro nějž platí prakticky stejné jmenné konvence, jako pro jakýkoli jiný identifikátor (teoreticky lze použít znaky z Unicode, prakticky se využívá jen podmnožina ASCII), ovšem s jednou malou výjimkou – nelze zde použít prázdný identifikátor představovaný podtržítkem „_“. Další striktní požadavky nejsou na názvy balíčků kladeny, ovšem s ohledem na konzistentnost se doporučuje, aby název balíčku byl tvořen jediným podstatným jménem zapisovaným malými písmeny. Vzhledem k tomu, že by v názvu balíčku mělo být jen jediné slovo, není zapotřebí řešit další pravidla – zda použít camelCase, snake_case atd.
Na tomto místě se asi ptáte, jak a zda vůbec je možné toto pravidlo dodržet i pro rozsáhlejší aplikace s desítkami či stovkami balíčků – zde přece musí dojít k nějaké kolizi jmen. Ve skutečnosti je ovšem název balíčku (většinou ono jediné podstatné jméno) spojeno s cestou (path) k balíčku, která slouží k přesnějšímu rozlišení. Příkladem může být například standardní balíček nazvaný httputil, jehož zdrojové kódy naleznete na stránce https://golang.org/src/net/http/httputil/. Ve zdrojových kódech, do nichž se tento balíček importuje, budeme psát cestu k balíčku a teprve po ní jeho název, což nám již umožní lepší návrh aplikace a dodržení jmenných konvencí jazyka Go. Jako oddělovač je použito lomítko:
import "net/http/httputil"
Navíc to znamená, že jméno balíčku nemusí být unikátní; samozřejmě ovšem není možné mít stejně pojmenovaný balíček se stejnou cestou. Příklad balíčků se shodným jménem, ale s odlišnou cestou (a s odlišnou funkcionalitou):
- github.com/Workiva/go-datastructures/queue
- github.com/eapache/queue
- github.com/openfaas/faas/gateway/queue
- github.com/jaegertracing/jaeger/pkg/queue
atd. (stejně pojmenovaných balíčků je ve skutečnosti několik desítek).
Další informace o jmenných konvencích při pojmenování balíčků naleznete na stránkách:
- Style guideline for Go packages
https://rakyll.org/style-packages/ - The Go Blog: Package names
https://blog.golang.org/package-names - Seznam standardních balíčků
https://golang.org/pkg/ - Seznam dalších (vybraných) balíčků pro Go
https://github.com/avelino/awesome-go - Vyhledávač balíčků pro jazyk Go
https://go-search.org/ - Everything you need to know about Packages in Go
https://medium.com/rungo/everything-you-need-to-know-about-packages-in-go-b8bac62b74cc
4. Klíčové slovo import
S balíčky úzce souvisí i další klíčové slovo nazvané import. Jak již název tohoto slova naznačuje, slouží k importu balíčků do zdrojového kódu, který je právě vyvíjen a který vyžaduje použití konstant, proměnných, datových typů, funkcí či metod z tohoto balíčku. Za klíčovým slovem import je možné v tom nejjednodušším případě uvést cestu k balíčku s jeho názvem. V programovacím jazyce Go se přitom nejedná o nějaký speciální identifikátor, ale o řetězec:
import "fmt"
Taktéž je možné provést import několika balíčků současně. V tomto případě se názvy těchto balíčků zapisují do kulatých závorek za slovem import, přičemž by se měl dodržet následující formát zápisu (namísto mezer se ovšem v reálných zdrojových kódech používají taby).
Jeden balíček:
import ( "fmt" )
Více balíčků:
import ( "fmt" "time" )
Pokud jsou balíčky importovány výše uvedeným způsobem, je přístup k importovaným objektům (konstanty, …) provádět s využitím tečkové notace, například:
fmt.Printf("Reservation for %d\n", day) time.Sleep(1 * time.Second)
Alternativně je ovšem možné před jméno importovaného balíčku zapsat tečku:
import . "fmt"
V takovém případě se tečková notace nepoužívá a objekty z balíčku se zapisují pouze svým jménem:
Printf("Reservation for %d\n", day)
Tento způsob je vhodné používat s rozvahou, protože se nám vlastně pomíchají lokální identifikátory objektů s identifikátory objektů naimportovaných, což může způsobit pozdější zmatky.
Další možností je vytvoření aliasu jména balíčku:
import formatter "fmt"
resp.:
import ( formatter "fmt" )
Ve zdrojových kódech se v tomto případě použije alias:
formatter.Printf("Reservation for %d\n", day)
Zajímavé je, že alias není zapisován jako řetězec, ale jako běžný identifikátor. Důvod, proč tomu tak je, si řekneme v navazující kapitole.
5. Vytvoření vlastního balíčku, export objektů
Nyní si ukažme, jakým způsobem je možné vytvořit vlastní balíček, který pojmenujeme hello1. Je to snadné – do adresáře ~/go/src, který jsme si vytvořili už na začátku seriálu (a na který ukazuje proměnná GOPATH), přidáme podadresář nazvaný „hello1“ a vytvoříme v něm soubor nazvaný „hello1.go“ s následujícím obsahem:
package hello1 func Hello_world() { println("Hello world!") }
V jakémkoli jiném adresáři nyní můžeme tento balíček naimportovat a zavolat z něho funkci Hello_world:
package main import ( "hello1" ) func main() { hello.Hello_world() }
Nyní vytvořme nový balíček se jménem hello2, ovšem tentokrát bude následující zdrojový kód uložen v souboru ~/go/src/hello2/hello1.go (ne – nespletli jsme se, skutečně zde použijeme jedničku a nikoli dvojku):
package hello2 func Hello_world() { println("Hello world #2!") }
Takový balíček bude stále bez potíží použitelný! V jazyku Go je totiž možné mít v jednom balíčku více zdrojových souborů, přičemž důležitý je název adresáře a řádek package, nikoli jména souborů:
package main import ( "hello2" ) func main() { hello2.Hello_world() }
Třetí balíček obsahuje deklaraci funkce hello_world, jejíž název začíná malým písmenem:
package hello3 func hello_world() { println("Hello world #3!") }
Tato zdánlivě nepatrná změna způsobí, že funkci nebude možné z jiného balíčku/modulu zavolat, protože symboly začínající malým písmenem by neměly být viditelné (přesněji řečeno – nejsou exportovány):
package main import ( "hello3" ) func main() { hello3.hello_world() }
O této vlastnosti programovacího jazyka Go nás přesvědčí pokus o překlad:
./21_use_package_hello3.go:15:2: cannot refer to unexported name hello3.hello_world ./21_use_package_hello3.go:15:2: undefined: hello3.hello_world
6. Inicializace balíčků
V případě, že je v balíčku deklarována funkce init(), bude tato funkce automaticky zavolána při importu balíčků. Totéž rekurzivně platí i pro případné balíčky importované do našeho balíčku atd. atd. Zkusme si nyní vytvořit nový balíček nazvaný hello4, který bude umístěn do adresáře ~/go/src/hello4:
package hello4 func init() { println("hello4.init() called") } func Hello_world() { println("Hello world #4!") }
Tento balíček běžným způsobem naimportujeme a zavoláme jeho funkci:
package main import ( "hello4" ) func main() { hello4.Hello_world() }
Po překladu a spuštění příkladu by se měla nejdříve zobrazit zpráva o zavolání funkce init() a teprve poté zpráva z funkce main:
hello4.init() called Hello world #4!
V některých situacích může být užitečné zavolat pouze funkci init() a balíček posléze nepoužít. To je problematické, protože init() není exportována (začíná malým písmenem) a současně není možné importovat nepoužívaný balíček. Řešení spočívá v použití již zmíněného podtržítka, které nám dovolí import balíčku bez jeho dalšího použití, což nám ovšem nevadí, protože init() se zavolá při importu:
package main import ( _ "hello4" ) func main() { }
7. Adresářová struktura, na níž ukazuje proměnná GOPATH
Vraťme se nyní k závěrečné části předchozího článku, v níž jsme se věnovali problematice adresářové struktury, na niž by měla ukazovat proměnná prostředí GOPATH. Připomeňme si, že se tento adresář (může se jmenovat jakkoli) nazývá workspace a měl by obsahovat tři podadresáře pojmenované bin, pkg a src. V těchto adresářích se postupně budou vytvářet další podadresáře, ale základní struktura by zpočátku měla vypadat následovně:
. ├── bin │ ├── │ └── ├── pkg │ ├── │ └── └── src ├── ├── └──
Obsah podadresáře src vlastně již do značné míry známe, protože ten obsahuje zdrojové kódy jednotlivých balíčků, přičemž každý balíček je umístěn ve vlastním podadresáři. Vzhledem k operacím, které jsme provedli v rámci předchozích kapitol by tedy aktuální adresářová struktura workspace, na níž ukazuje proměnná GOPATH, měla vypadat takto:
. ├── bin ├── pkg └── src ├── hello1 │ └── hello1.go ├── hello2 │ └── hello1.go ├── hello3 │ └── hello1.go └── hello4 └── hello4.go
Zajímavá je funkce ostatních dvou podadresářů bin a pkg. Jejich použití je vysvětleno v navazující kapitole.
8. Balíčky se spustitelným kódem
Nyní v adresáři src vytvoříme podadresář s balíčkem nazvaným say_hello1. Tento balíček bude obsahovat jediný soubor hello.go s tímto obsahem:
package main func main() { println("Hello world #1!") }
Povšimněte si, že v tomto případě je na prvním řádku zapsáno package main a nikoli package say_hello1. Je tomu tak z toho důvodu, že tento balíček ve skutečnosti použijeme pro vytvoření spustitelného souboru a nebudeme ho používat jako knihovnu.
V tento okamžik můžeme provést instalaci balíčku
go install say_hello_1
Balíček by se měl přeložit a jeho spustitelná nativní podoba by měla být uložena do adresáře bin:
. ├── bin │ └── say_hello_1 ├── pkg └── src ├── hello1 │ └── hello1.go ├── hello2 │ └── hello1.go ├── hello3 │ └── hello1.go ├── hello4 │ └── hello4.go └── say_hello_1 └── hello.go
Podobně můžeme vytvořit balíček say_hello2, opět se souborem hello.go. To, že se soubory v obou balíčcích jmenují stejně, vůbec nevadí, protože výsledná spustitelná aplikace bude pojmenována stejně jako balíček:
├── bin │ ├── say_hello_1 │ └── say_hello_2 ├── pkg └── src ├── hello1 │ └── hello1.go ├── hello2 │ └── hello1.go ├── hello3 │ └── hello1.go ├── hello4 │ └── hello4.go ├── say_hello_1 │ └── hello.go └── say_hello_2 └── hello.go
Další možnosti si ukážeme příště při popisu práce s repositáři, instalací externích knihoven apod.
9. Chybějící datový typ enum a způsob jeho náhrady jinou konstrukcí
V předchozím článku jsme si ukázali způsob použití identifikátoru iota. Při té příležitosti jsme si řekli, že současná verze programovacího jazyka Go neobsahuje plnohodnotný datový typ „výčet“ neboli enumeration resp. enum. Ve skutečnosti však máme k dispozici několik způsobů, jak se k výčtovému typu alespoň přiblížit. Jednotlivé způsoby si postupně popíšeme.
Nejvíce přímočará je pouhá deklarace celočíselných konstant v bloku const s využitím identifikátoru iota, který zde vystupuje v roli celočíselného čítače:
const ( Pondeli = iota Utery Streda Ctvrtek Patek Sobota Nedele )
Při takto přímočarém použití však nastávají minimálně dva problémy:
- Konstanty jsou typu int a takto se tedy musí předávat do funkcí.
- Může se stát, že některé operace změní hodnotu, která má představovat pořadí dne v týdnu, na neplatnou hodnotu.
Ostatně si můžeme vyzkoušet, jak se bude chovat následující demonstrační příklad po svém překladu a spuštění:
package main import "fmt" const ( Pondeli = iota Utery Streda Ctvrtek Patek Sobota Nedele ) func reservation(day int) { fmt.Printf("Reservation for %d\n", day) } func main() { reservation(Pondeli) reservation(Sobota) reservation(Nedele) reservation(3) day := Pondeli day-- reservation(day) }
Ze zpráv vypsaných na terminál je patrné, že můžeme použít obyčejnou celočíselnou konstantu (3) namísto jména dne (Ctvrtek) a navíc se operací day– získala hodnota, která neleží v očekávaném rozsahu:
Reservation for 0 Reservation for 5 Reservation for 6 Reservation for 3 Reservation for -1
Toto řešení je tedy nejvíce přímočaré, ale nemusí nám vyhovovat.
10. První krok: nový celočíselný typ použitý s identifikátorem iota
Existuje jeden velmi jednoduchý a elegantní způsob, jak zabránit tomu, aby celočíselné konstanty vytvořené identifikátorem iota byly typu int. Nejprve nadeklarujeme nový datový typ odvozený od int:
type Enum int
Posléze všechny konstanty vytvoříme, ovšem u první konstanty explicitně zapíšeme její datový typ:
const ( Pondeli Enum = iota Utery Streda Ctvrtek Patek Sobota Nedele )
Výsledkem bude, že se opět vytvoří sedm konstant s hodnotami 0, 1, 2, atd., ovšem tyto konstanty budou typu Enum a nikoli typu int. Interně se sice jedná o naprosto stejnou reprezentaci (běžné celé číslo), ovšem z předchozích článků již víme, že Go striktně vyžaduje explicitní konverze. To znamená, že následující funkci:
func reservation(day int) { fmt.Printf("Reservation for %d\n", day) }
již nebude možné předat přímo nějakou konstantu:
reservation(Pondeli)
Můžeme se o tom snadno přesvědčit při pokusu o překlad tohoto příkladu:
package main import "fmt" type Enum int const ( Pondeli Enum = iota Utery Streda Ctvrtek Patek Sobota Nedele ) func reservation(day int) { fmt.Printf("Reservation for %d\n", day) } func main() { reservation(Pondeli) reservation(Sobota) reservation(Nedele) reservation(3) day := Pondeli day-- reservation(day) }
Překladač jazyka Go vypíše chyby při pokusu o volání funkce reservartion s konstantou či proměnnou špatného typu:
./02_enum_with_iota_type_check.go:29:13: cannot use Pondeli (type Enum) as type int in argument to reservation ./02_enum_with_iota_type_check.go:30:13: cannot use Sobota (type Enum) as type int in argument to reservation ./02_enum_with_iota_type_check.go:31:13: cannot use Nedele (type Enum) as type int in argument to reservation ./02_enum_with_iota_type_check.go:37:13: cannot use day (type Enum) as type int in argument to reservation
Úprava příkladu je snadná, pouze přepíšeme hlavičku funkce:
func reservation(day Enum) { fmt.Printf("Reservation for %d\n", day) }
package main import "fmt" type Enum int const ( Pondeli Enum = iota Utery Streda Ctvrtek Patek Sobota Nedele ) func reservation(day Enum) { fmt.Printf("Reservation for %d\n", day) } func main() { reservation(Pondeli) reservation(Sobota) reservation(Nedele) reservation(3) day := Pondeli day-- reservation(day) }
S výsledky:
Reservation for 0 Reservation for 5 Reservation for 6 Reservation for 3 Reservation for -1
Zde můžeme vidět, že se nám sice zdrojový kód podařilo nepatrně vylepšit, ale stále je možné proměnnou typu Enum změnit (například operací –) tak, že nebude obsahovat korektní hodnotu, takže se o typově bezpečný datový typ výčet stále nejedná.
11. Převod zvolených celočíselných konstant na řetězec
V některých případech, například pro účely logování, je zapotřebí převádět konstanty představující hodnoty výčtového typu na řetězce. K tomuto účelu samozřejmě můžeme použít „obyčejnou“ funkci, ovšem elegantnější je deklarace metody:
func (day Enum) String() string { days := []string{ "Pondeli", "Utery", "Streda", "Ctvrtek", "Patek", "Sobota", "Nedele"} if day < Pondeli || day > Nedele { return "Unknown day" } return days[day] }
Opět si ukažme úplný zdrojový kód tohoto příkladu, v němž se provádí základní kontroly, zda předaná hodnota leží v očekávaném rozsahu:
package main import "fmt" type Enum int const ( Pondeli Enum = iota Utery Streda Ctvrtek Patek Sobota Nedele ) func (day Enum) String() string { days := []string{ "Pondeli", "Utery", "Streda", "Ctvrtek", "Patek", "Sobota", "Nedele"} if day < Pondeli || day > Nedele { return "Unknown day" } return days[day] } func reservation(day Enum) { fmt.Printf("Reservation for %s\n", day.String()) } func main() { reservation(Pondeli) reservation(Sobota) reservation(Nedele) reservation(3) day := Pondeli day-- reservation(day) }
Po překladu a spuštění tohoto příkladu získáme tyto řádky:
Reservation for Pondeli Reservation for Sobota Reservation for Nedele Reservation for Ctvrtek Reservation for Unknown day
12. Využití struktury/záznamu pro typově zabezpečené operace s výčtem
Při tvorbě datového typu podobného typu výčtovému můžeme jít ještě o jeden krok dále a použít tuto deklaraci:
type Enum int type Den struct { X Enum }
Touto deklarací vlastně říkáme, že typ Den je datová struktura s jediným členem typu Enum pojmenovaným X. Díky tomu se vlastně interní hodnoty „uschovají“ do struktury, což je dobře, protože zápisy typu:
day := Den{Pondeli} day++
přestanou být korektní a nebude možné použít jiné hodnoty než definované (pokud přímo nepřistoupíme k prvku struktury).
Opět se podívejme na upravený demonstrační příklad, který nyní vypadá následovně:
package main import "fmt" type Enum int const ( Pondeli Enum = iota Utery Streda Ctvrtek Patek Sobota Nedele ) type Den struct { X Enum } func (day Enum) String() string { days := []string{ "Pondeli", "Utery", "Streda", "Ctvrtek", "Patek", "Sobota", "Nedele"} if day < Pondeli || day > Nedele { return "Unknown day" } return days[day] } func reservation(day Den) { fmt.Printf("Reservation for %s\n", day) } func main() { reservation(Den{Pondeli}) reservation(Den{Sobota}) reservation(Den{Nedele}) day := Den{Pondeli} reservation(day) }
Příklad nyní pracuje správně (případné složené závorky lze snadno odstranit – což ponechám váženému čtenáři za domácí úkol):
Reservation for {Pondeli} Reservation for {Sobota} Reservation for {Nedele} Reservation for {Pondeli}
Nyní si otestujeme, že skutečně nebude možné měnit hodnotu našeho výčtového typu:
package main import "fmt" type Enum int const ( Pondeli Enum = iota Utery Streda Ctvrtek Patek Sobota Nedele ) type Den struct { X Enum } func (day Enum) String() string { days := []string{ "Pondeli", "Utery", "Streda", "Ctvrtek", "Patek", "Sobota", "Nedele"} if day < Pondeli || day > Nedele { return "Unknown day" } return days[day] } func reservation(day Den) { fmt.Printf("Reservation for %s\n", day) } func main() { reservation(Den{Pondeli}) reservation(Den{Sobota}) reservation(Den{Nedele}) day := Den{Pondeli} day++ reservation(day) }
Při pokusu o překlad day++ překladač jazyka Go zahlásí chybu:
./06_enum_as_type.go:53:5: invalid operation: day++ (non-numeric type Den)
13. Kanály s předdefinovanou maximální kapacitou fronty
Dalším doplňujícím tématem dnešního článku je zmínka o kanálech s nakonfigurovanou kapacitou fronty (FIFO, v kontextu kanálů se mluví o bufferu). Připomeňme si, že pokud je kanál vytvořen následujícím způsobem:
channel := make(chan rune)
bude mít jeho fronta/buffer nulovou kapacitu a tudíž se kanál bude chovat podobně jako mailbox. Ovšem kapacitu kanálu (resp. jeho FIFA) můžeme zvýšit zápisem druhého (nepovinného) parametru do volání funkce make:
channel := make(chan rune, 1234)
Teoreticky je kapacita omezena tím, že je reprezentována typem int, prakticky však pochopitelně narazíme na limity, které nejvíce souvisí s maximálním množstvím paměti, která může být procesu přidělena.
Podívejme se na příklad, v němž je vytvořen kanál s kapacitou bufferu/FIFA pro tři znaky. V tomto příkladu se nevytváří ani nevolá žádná nová gorutina – všechny operace s kanálem jsou součástí jediné (hlavní) gorutiny, která používá kanál pro implementaci fronty (queue):
channel := make(chan rune)
package main import ( "fmt" ) func main() { channel := make(chan rune, 3) channel <- 'A' channel <- 'B' channel <- 'C' for i := 0; i < 3; i++ { fmt.Printf("%c ", <-channel) } }
Po překladu a spuštění tohoto příkladu by se měl vypsat tento řádek:
A B C
14. Použití konstrukce range při čtení dat z kanálu
Dalším užitečným trikem, který je možné využít při práci s kanály, je čtení hodnot z kanálu v programové smyčce for s konstrukcí range. Celý zápis vypadá následovně:
for msg := range channel { fmt.Printf("%c ", msg) }
Tato programová smyčka skončí až ve chvíli, kdy dojde k uzavření kanálu! Pokud kanál není uzavřen a jeho fronta je prázdná, smyčka bude buď čekat na data nebo runtime programovacího jazyka Go bude detekovat stav, kdy neběží žádná gorutina a program bude násilně ukončen. Tuto vlastnost si opět můžeme velmi snadno odzkoušet:
package main import ( "fmt" ) func fill_in_channel(channel chan rune) { channel <- 'A' channel <- 'B' channel <- 'C' } func main() { channel := make(chan rune, 3) go fill_in_channel(channel) for msg := range channel { fmt.Printf("%c ", msg) } }
Pokud tento program spustíme, dojde k postupnému naplnění kanálu třemi prvky, které budou (opět postupně) čteny v programové smyčce. Následně runtime jazyka Go zjistí, že existuje jen jediná gorutina (hlavní) a ta čeká na další data – ty evidentně nemá kde získat a proto bude program násilně ukončen:
A B C fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: main.main() /home/tester/temp/out/go-root/article_08/08_channel_and_range.go:25 +0xe7 exit status 2
Úprava je ve skutečnosti velmi snadná – v námi vytvořené gorutině (v ní se volá funkce fill_in_channel) nejprve do kanálu pošleme tři prvky a posléze kanál uzavřeme voláním:
close(channel)
Upravený zdrojový kód demonstračního příkladu vypadá následovně:
package main import ( "fmt" ) func fill_in_channel(channel chan rune) { channel <- 'A' channel <- 'B' channel <- 'C' close(channel) } func main() { channel := make(chan rune, 3) go fill_in_channel(channel) for msg := range channel { fmt.Printf("%c ", msg) } }
Po spuštění zjistíme, že se program skutečně chová tak, jak je očekáváno: k žádným pádům nedochází a hlavní gorutina je po příjmu tří prvků korektně ukončena:
A B C
15. Chování uzavřeného kanálu při čtení hodnot
Pro úplnost si ukažme, jak se chová uzavřený kanál ve chvíli, kdy z něj čteme další hodnoty. V tomto případě se nejedná o blokující operace (kanál je uzavřen, žádná data do něj již nelze poslat), proto je chování upraveno takovým způsobem, že pokus o přečtení dat z uzavřeného kanálu vrátí výchozí hodnotu platnou pro ten datový typ, jaký odpovídá typu prvků kanálu. V praxi to znamená, že pro uzavřený kanál chan int získáme po přečtení sadu nul:
package main import ( "fmt" ) func main() { channel := make(chan int, 3) close(channel) fmt.Printf("%d\n", <-channel) fmt.Printf("%d\n", <-channel) fmt.Printf("%d\n", <-channel) }
Chování příkladu po spuštění:
0 0 0
16. Uzavření kanálu jako součást synchronizační konstrukce
Výše popsané chování:
- Čtení z prázdného kanálu je blokující operace.
- Čtení z uzavřeného kanálu skončí ihned (a vrátí se nula)
je možné využít k tomu, aby kanál sloužil jako synchronizační konstrukce, a to bez toho, aby se do něj provedl byť i jediný zápis. V následujícím příkladu je vytvořena nová gorutina představovaná anonymní funkcí. Na dokončení běhu této gorutiny čekáme příkazem:
<-done
Vzhledem k tomu, že do kanálu nebyl proveden zápis, je výše uvedený příkaz blokován až do doby, kdy je kanál uzavřen:
package main func main() { done := make(chan bool) go func() { println("async block") close(done) }() println("wait for async block") <-done }
Po spuštění příkladu je patrné, že se skutečně čeká na dokončení gorutiny (v opačném případě je program ukončen již při opuštění funkce main):
wait for async block async block
17. Konstrukce select s definicí maximální doby čekání na data
Další užitečnou konstrukcí, která souvisí s kanály a komunikací mezi gorutinami, je čekání na data v příkazu select. Pokud čekáme pouze na data z libovolného množství kanálů, jedná se o blokující operaci, která může trvat libovolně dlouho:
select { case <-ch1: fmt.Println("Data z kanálu 1") case <-ch2: fmt.Println("Data z kanálu 2")
V případě, že potřebujeme, aby byla celá programová konstrukce select po nějaké době ukončena, nezávisle na tom, zda jsme data přečetli či nikoli, použijeme tuto větev (přidanou k ostatním větvím):
case <-time.After(2 * time.Second): fmt.Println("Timeout!") }
Ve skutečnosti se nejedná o žádnou magii, protože funkce time.After() vrací kanál typu chan Time, do něhož je proveden jediný zápis, a to po uběhnutí specifikované doby.
Celý příklad, v němž se snažíme o přečtení dat z kanálu ch1 nebo ch2, ovšem s definovanou maximální dobou čekání, může vypadat následovně:
package main import ( "fmt" "time" ) func worker(channel chan int, worker int) { fmt.Printf("Worker %d spuštěn\n", worker) time.Sleep(1 * time.Second) channel <- 1 fmt.Printf("Worker %d ukončen\n", worker) } func main() { ch1 := make(chan int) ch2 := make(chan int) go worker(ch1, 1) go worker(ch2, 2) select { case <-ch1: fmt.Println("Data z kanálu 1") case <-ch2: fmt.Println("Data z kanálu 2") case <-time.After(2 * time.Second): fmt.Println("Timeout!") } }
Chování po spuštění – timeout nebyl použit:
Worker 2 spuštěn Worker 1 spuštěn Worker 1 ukončen Data z kanálu 1
Nepatrnou úpravou zdrojového kódu můžeme dosáhnout toho, aby byla větev s timeoutem skutečně použita; postačuje pouze zvýšit čas ve volání time.Sleep() v implementaci gorutiny:
package main import ( "fmt" "time" ) func worker(channel chan int, worker int) { fmt.Printf("Worker %d spuštěn\n", worker) time.Sleep(5 * time.Second) channel <- 1 fmt.Printf("Worker %d ukončen\n", worker) } func main() { ch1 := make(chan int) ch2 := make(chan int) go worker(ch1, 1) go worker(ch2, 2) select { case <-ch1: fmt.Println("Data z kanálu 1") case <-ch2: fmt.Println("Data z kanálu 2") case <-time.After(2 * time.Second): fmt.Println("Timeout!") } }
Nyní bude chování příkladu po jeho spuštění odlišné:
Worker 1 spuštěn Worker 2 spuštěn Timeout!
18. Definice čekání na data a větev default
Jen pro úplnost si ukažme nesmyslný příklad (syntakticky je správně, sémanticky už nikoli), v němž v programové konstrukci select použijeme jak větev se čtením z kanálu zkonstruovaného pomocí time.After(), tak i větev default. V tomto případě se větev time.After() nikdy nepoužije, protože se ve skutečnosti neprovádí žádné čekání, pouze výběr případných dat z jednoho dostupného kanálu. V případě, že data k dispozici nejsou, provede se ihned kód ve větvi default:
package main import ( "fmt" "time" ) func worker(channel chan int, worker int) { fmt.Printf("Worker %d spuštěn\n", worker) time.Sleep(5 * time.Second) channel <- 1 fmt.Printf("Worker %d ukončen\n", worker) } func main() { ch1 := make(chan int) ch2 := make(chan int) go worker(ch1, 1) go worker(ch2, 2) select { case <-ch1: fmt.Println("Data z kanálu 1") case <-ch2: fmt.Println("Data z kanálu 2") case <-time.After(2 * time.Second): fmt.Println("Timeout!") default: fmt.Println("No data!") } }
Worker 1 spuštěn No data!
19. Repositář s demonstračními příklady
Zdrojové kódy všech dnes popsaných demonstračních příkladů byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má doslova několik kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:
20. Odkazy na Internetu
- Select waits on a group of channels
https://yourbasic.org/golang/select-explained/ - Rob Pike: Simplicity is Complicated (video)
http://www.golang.to/posts/dotgo-2015-rob-pike-simplicity-is-complicated-youtube-16893 - Algorithms to Go
https://yourbasic.org/ - Využití knihovny Pygments (nejenom) pro obarvení zdrojových kódů
https://www.root.cz/clanky/vyuziti-knihovny-pygments-nejenom-pro-obarveni-zdrojovych-kodu/ - 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/ - Go Defer Simplified with Practical Visuals
https://blog.learngoprogramming.com/golang-defer-simplified-77d3b2b817ff - 5 More Gotchas of Defer in Go — Part II
https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-ii-cc550f6ad9aa - The Go Blog: Defer, Panic, and Recover
https://blog.golang.org/defer-panic-and-recover - The defer keyword in Swift 2: try/finally done right
https://www.hackingwithswift.com/new-syntax-swift-2-defer - Swift Defer Statement
https://andybargh.com/swift-defer-statement/ - Modulo operation (Wikipedia)
https://en.wikipedia.org/wiki/Modulo_operation - Node.js vs Golang: Battle of the Next-Gen Languages
https://www.hostingadvice.com/blog/nodejs-vs-golang/ - The Go Programming Language (home page)
https://golang.org/ - GoDoc
https://godoc.org/ - Go (programming language), Wikipedia
https://en.wikipedia.org/wiki/Go_(programming_language) - Go Books (kniha o jazyku Go)
https://github.com/dariubs/GoBooks - The Go Programming Language Specification
https://golang.org/ref/spec - Go: the Good, the Bad and the Ugly
https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/ - Package builtin
https://golang.org/pkg/builtin/ - Package fmt
https://golang.org/pkg/fmt/ - The Little Go Book (další kniha)
https://github.com/dariubs/GoBooks - The Go Programming Language by Brian W. Kernighan, Alan A. A. Donovan
https://www.safaribooksonline.com/library/view/the-go-programming/9780134190570/ebook_split010.html - Learning Go
https://www.miek.nl/go/ - Go Bootcamp
http://www.golangbootcamp.com/ - Programming in Go: Creating Applications for the 21st Century (další kniha o jazyku Go)
http://www.informit.com/store/programming-in-go-creating-applications-for-the-21st-9780321774637 - Introducing Go (Build Reliable, Scalable Programs)
http://shop.oreilly.com/product/0636920046516.do - Learning Go Programming
https://www.packtpub.com/application-development/learning-go-programming - The Go Blog
https://blog.golang.org/ - Getting to Go: The Journey of Go's Garbage Collector
https://blog.golang.org/ismmkeynote - Go (programovací jazyk, Wikipedia)
https://cs.wikipedia.org/wiki/Go_(programovac%C3%AD_jazyk) - Rychle, rychleji až úplně nejrychleji s jazykem Go
https://www.root.cz/clanky/rychle-rychleji-az-uplne-nejrychleji-s-jazykem-go/ - Installing Go on the Raspberry Pi
https://dave.cheney.net/2012/09/25/installing-go-on-the-raspberry-pi - How the Go runtime implements maps efficiently (without generics)
https://dave.cheney.net/2018/05/29/how-the-go-runtime-implements-maps-efficiently-without-generics - Niečo málo o Go – Golang (slovensky)
http://golangsk.logdown.com/ - How Many Go Developers Are There?
https://research.swtch.com/gophercount - Most Popular Technologies (Stack Overflow Survery 2018)
https://insights.stackoverflow.com/survey/2018/#most-popular-technologies - Most Popular Technologies (Stack Overflow Survery 2017)
https://insights.stackoverflow.com/survey/2017#technology - The Go Programming Language: Release History
https://golang.org/doc/devel/release.html - Go 1.11 Release Notes
https://golang.org/doc/go1.11 - Go 1.10 Release Notes
https://golang.org/doc/go1.10 - Go 1.9 Release Notes (tato verze je stále používána)
https://golang.org/doc/go1.9 - Go 1.8 Release Notes (i tato verze je stále používána)
https://golang.org/doc/go1.8 - Go on Fedora
https://developer.fedoraproject.org/tech/languages/go/go-installation.html - Writing Go programs
https://developer.fedoraproject.org/tech/languages/go/go-programs.html - The GOPATH environment variable
https://tip.golang.org/doc/code.html#GOPATH - Command gofmt
https://tip.golang.org/cmd/gofmt/ - The Go Blog: go fmt your code
https://blog.golang.org/go-fmt-your-code - C? Go? Cgo!
https://blog.golang.org/c-go-cgo - Spaces vs. Tabs: A 20-Year Debate Reignited by Google’s Golang
https://thenewstack.io/spaces-vs-tabs-a-20-year-debate-and-now-this-what-the-hell-is-wrong-with-go/ - 400,000 GitHub repositories, 1 billion files, 14 terabytes of code: Spaces or Tabs?
https://medium.com/@hoffa/400–000-github-repositories-1-billion-files-14-terabytes-of-code-spaces-or-tabs-7cfe0b5dd7fd - Gofmt No Longer Allows Spaces. Tabs Only
https://news.ycombinator.com/item?id=7914523 - Why does Go „go fmt“ uses tabs instead of whitespaces?
https://www.quora.com/Why-does-Go-go-fmt-uses-tabs-instead-of-whitespaces - Interactive: The Top Programming Languages 2018
https://spectrum.ieee.org/static/interactive-the-top-programming-languages-2018 - Go vs. Python
https://www.peterbe.com/plog/govspy - PackageManagementTools
https://github.com/golang/go/wiki/PackageManagementTools - A Tour of Go: Type inference
https://tour.golang.org/basics/14 - Go Slices: usage and internals
https://blog.golang.org/go-slices-usage-and-internals - Go by Example: Slices
https://gobyexample.com/slices - What is the point of slice type in Go?
https://stackoverflow.com/questions/2098874/what-is-the-point-of-slice-type-in-go - The curious case of Golang array and slices
https://medium.com/@hackintoshrao/the-curious-case-of-golang-array-and-slices-2565491d4335 - Introduction to Slices in Golang
https://www.callicoder.com/golang-slices/ - Golang: Understanding ‚null‘ and nil
https://newfivefour.com/golang-null-nil.html - What does nil mean in golang?
https://stackoverflow.com/questions/35983118/what-does-nil-mean-in-golang - nils In Go
https://go101.org/article/nil.html - Go slices are not dynamic arrays
https://appliedgo.net/slices/ - Go-is-no-good (nelze brát doslova)
https://github.com/ksimka/go-is-not-good - Rust vs. Go
https://news.ycombinator.com/item?id=13430108 - Seriál Programovací jazyk Rust
https://www.root.cz/serialy/programovaci-jazyk-rust/ - Modern garbage collection: A look at the Go GC strategy
https://blog.plan99.net/modern-garbage-collection-911ef4f8bd8e - Go GC: Prioritizing low latency and simplicity
https://blog.golang.org/go15gc - Is Golang a good language for embedded systems?
https://www.quora.com/Is-Golang-a-good-language-for-embedded-systems - Running GoLang on an STM32 MCU. A quick tutorial.
https://www.mickmake.com/post/running-golang-on-an-mcu-a-quick-tutorial - Go, Robot, Go! Golang Powered Robotics
https://gobot.io/ - Emgo: Bare metal Go (language for programming embedded systems)
https://github.com/ziutek/emgo - UTF-8 history
https://www.cl.cam.ac.uk/~mgk25/ucs/utf-8-history.txt - Less is exponentially more
https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html - Should I Rust, or Should I Go
https://codeburst.io/should-i-rust-or-should-i-go-59a298e00ea9 - Setting up and using gccgo
https://golang.org/doc/install/gccgo - Elastic Tabstops
http://nickgravgaard.com/elastic-tabstops/ - Strings, bytes, runes and characters in Go
https://blog.golang.org/strings - Datový typ
https://cs.wikipedia.org/wiki/Datov%C3%BD_typ - Seriál o programovacím jazyku Rust: Základní (primitivní) datové typy
https://www.root.cz/clanky/programovaci-jazyk-rust-nahrada-c-nebo-slepa-cesta/#k09 - Seriál o programovacím jazyku Rust: Vytvoření „řezu“ z pole
https://www.root.cz/clanky/prace-s-poli-v-programovacim-jazyku-rust/#k06 - Seriál o programovacím jazyku Rust: Řezy (slice) vektoru
https://www.root.cz/clanky/prace-s-vektory-v-programovacim-jazyku-rust/#k05 - Printf Format Strings
https://www.cprogramming.com/tutorial/printf-format-strings.html - Java: String.format
https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#format-java.lang.String-java.lang.Object…- - Java: format string syntax
https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html#syntax - Selectors
https://golang.org/ref/spec#Selectors - Calling Go code from Python code
http://savorywatt.com/2015/09/18/calling-go-code-from-python-code/ - Go Data Structures: Interfaces
https://research.swtch.com/interfaces - How to use interfaces in Go
http://jordanorelli.com/post/32665860244/how-to-use-interfaces-in-go - Interfaces in Go (part I)
https://medium.com/golangspec/interfaces-in-go-part-i-4ae53a97479c - Part 21: Goroutines
https://golangbot.com/goroutines/ - Part 22: Channels
https://golangbot.com/channels/ - [Go] Lightweight eventbus with async compatibility for Go
https://github.com/asaskevich/EventBus - What about Trait support in Golang?
https://www.reddit.com/r/golang/comments/8mfykl/what_about_trait_support_in_golang/ - Don't Get Bitten by Pointer vs Non-Pointer Method Receivers in Golang
https://nathanleclaire.com/blog/2014/08/09/dont-get-bitten-by-pointer-vs-non-pointer-method-receivers-in-golang/ - Control Flow
https://en.wikipedia.org/wiki/Control_flow - Structured programming
https://en.wikipedia.org/wiki/Structured_programming - Control Structures
https://www.golang-book.com/books/intro/5 - Control structures – Go if else statement
http://golangtutorials.blogspot.com/2011/06/control-structures-if-else-statement.html - Control structures – Go switch case statement
http://golangtutorials.blogspot.com/2011/06/control-structures-go-switch-case.html - Control structures – Go for loop, break, continue, range
http://golangtutorials.blogspot.com/2011/06/control-structures-go-for-loop-break.html - Single Function Exit Point
http://wiki.c2.com/?SingleFunctionExitPoint - Entry point
https://en.wikipedia.org/wiki/Entry_point - Why does Go have a GOTO statement?!
https://www.reddit.com/r/golang/comments/kag5q/why_does_go_have_a_goto_statement/ - Effective Go
https://golang.org/doc/effective_go.html - GoClipse: an Eclipse IDE for the Go programming language
http://goclipse.github.io/ - GoClipse Installation
https://github.com/GoClipse/goclipse/blob/latest/documentation/Installation.md#installation