Obsah
1. Datové typy v programovacím jazyku Go
3. Konverze mezi hodnotami různých typů
4. Tisk a formátování celočíselných hodnot
5. Podmnožiny reálných čísel a komplexní čísla
8. Deklarace lokálních proměnných
9. Implicitní hodnoty proměnných
12. Řezy (slices), jejich význam a rozdílné vlastnosti oproti polím
14. Pokročilejší operace s řezy
15. Vytvoření řezu z jiného řezu
16. Datové typy popsané příště
17. Speciální hodnota nil a její specifický význam v jazyku Go
18. Repositář s demonstračními příklady
1. Datové typy v programovacím jazyku Go
V dnešním článku se seznámíme s typovým systémem programovacího jazyka Go. Připomeňme si, že Go používá statický typový systém, což konkrétně znamená, že typ proměnné, parametru funkce, návratové hodnoty funkce atd. musí být známý již v době překladu (compile time) a není ho tedy nutné vyhodnocovat a kontrolovat v čase běhu programu (runtime). Datové typy, které programovací jazyk Go vývojářům nabízí, je možné rozdělit podle několika kritérií. Základní dělení je na jednoduché či primitivní datové typy (typicky se hodnoty těchto typů dají uložit do jediného registru mikroprocesoru), složené datové typy a některé zvláštní typy (zde se konkrétně jedná o ukazatel, funkci, rozhraní a taktéž kanál). Jednoduché datové typy se někdy dále dělí na ordinální a neordinální. Celá hierarchie typového systému jazyka Go je zobrazena pod tímto odstavcem:
- Jednoduché datové typy
- Ordinální
- Pravdivostní typ (boolean)
- Celočíselné typy (integer)
- Neordinální
- Hodnoty s plovoucí řádovou čárkou (float)
- Komplexní čísla (complex)
- Ordinální
- Složené datové typy
- Zvláštní datové typy
- Ukazatel (pointer)
- Funkce (function)
- Rozhraní (interface)
- Kanál (channel)
2. Celočíselné datové typy
Programovací jazyk Go je navržen takovým způsobem, aby ho bylo možné efektivně provozovat na mainstreamových mikroprocesorových architekturách, což konkrétně znamená především architektury x86 (32bit), x86–64 (64bit), ARM32 (velká rodina několika 32bitových architektur), taktéž AArch64 (64bit) popř. další architetury (MIPS, PowerPC, …). Z tohoto důvodu v Go nalezneme takové celočíselné datové typy, které jsou na zmíněných architekturách nativně zpracovávány. To je poměrně značně odlišná strategie, než kterou se kdysi vydal programovací jazyk C, v němž se formát a šířka celočíselných datových typů odvozuje od použité architektury (například různé mikrořadiče či DSP používají mnohdy zcela odlišné formáty, ostatně podobně, jako tomu bylo na minipočítačích PDP atd.).
V následující tabulce jsou vypsány všechny celočíselné datové typy poskytované jazykem Go 1.x. Každý celočíselný datový typ je jednoznačně určen jediným klíčovým slovem; nenajdeme zde tedy modifikátory signed, unsigned, short, long atd.:
Označení | Rozsah hodnot | Stručný popis |
---|---|---|
int8 | –128 až 127 | osmibitové celé číslo se znaménkem |
int16 | –32768 až 32767 | 16bitové celé číslo se znaménkem |
int32 | –2147483648 až 2147483647 | 32bitové celé číslo se znaménkem |
int64 | –9223372036854775808 až 9223372036854775807 | 64bitové celé číslo se znaménkem |
uint8 | 0 až 255 | osmibitové celé číslo bez znaménka |
uint16 | 0 až 65535 | 16bitové celé číslo bez znaménka |
uint32 | 0 až 4294967295 | 32bitové celé číslo bez znaménka |
uint64 | 0 až 18446744073709551615 | 64bitové celé číslo bez znaménka |
int | různý | odpovídá buď typu int32 nebo int64 |
uint | různý | odpovídá buď typu uint32 nebo uint64 |
byte | 0 až 255 | alias pro typ uint8 |
rune | –2147483648 až 2147483647 | alias pro typ int32 |
uintptr | různý | používáno pro uložení adresy (ukazatele) |
Dále stojí za povšimnutí existence takzvaných aliasů, které se používají ve třech významech. První aliasy int a uint odpovídají buď 32bitové nebo 64bitové šířce celočíselného datového typu v závislosti na architektuře (zde se alespoň do jisté míry přibližujeme k céčku); ovšem zajímavější jsou aliasy byte (vždy osmibitová hodnota bez znaménka) a rune (reprezentace znaku v Unicode). Poslední alias uintptr reprezentuje datový typ určený pro uložení ukazatele (pointeru) a je tedy opět závislý na konkrétní architektuře.
Ukažme si nyní příklad definice proměnných s uvedením typu a současně i s jejich inicializací:
package main import "fmt" func main() { var a int8 = -10 var b int16 = -1000 var c int32 = -10000 var d int32 = -1000000 var r1 rune = 'a' var r2 rune = '\x40' var r3 rune = '\n' var r4 rune = '\u03BB' fmt.Println(a) fmt.Println(b) fmt.Println(c) fmt.Println(d) fmt.Println(r1) fmt.Println(r2) fmt.Println(r3) fmt.Println(r4) }
Výsledek po překladu a spuštění:
-10 -1000 -10000 -1000000 97 64 10 955
Překladač jazyka Go striktně hlídá, zda se přiřazovaná hodnota skutečně vejde do proměnné daného typu:
package main import "fmt" func main() { var a int8 = -1000 var b int16 = -100000 var c int32 = -10000000000 var d int32 = -10000000000000000 fmt.Println(a) fmt.Println(b) fmt.Println(c) fmt.Println(d) }
Při pokusu o překlad předchozího příkladu získáme pouze čtveřici chybových hlášení:
./02_integer_signed_types_checks.go:13:15: constant -1000 overflows int8 ./02_integer_signed_types_checks.go:14:16: constant -100000 overflows int16 ./02_integer_signed_types_checks.go:15:16: constant -10000000000 overflows int32 ./02_integer_signed_types_checks.go:16:16: constant -10000000000000000 overflows int32
Ve třetím příkladu vytvoříme čtyři lokální proměnné nesoucí informace o hodnotách bez znaménka:
package main import "fmt" func main() { var a uint8 = 10 var b uint16 = 1000 var c uint32 = 10000 var d uint32 = 1000000 fmt.Println(a) fmt.Println(b) fmt.Println(c) fmt.Println(d) }
Výsledek po překladu a spuštění:
10 1000 10000 1000000
Celočíselné konstanty je možné zapisovat v desítkové soustavě, v soustavě osmičkové (prefixem je zde nula) nebo v soustavě šestnáctkové (prefixem je 0×). Způsob použití různých číselných soustav je ukázán ve čtvrtém demonstračním příkladu:
package main import "fmt" func main() { var a uint8 = 10 var b uint8 = 010 var c uint8 = 0x10 fmt.Println(a) fmt.Println(b) fmt.Println(c) var d int8 = -10 var e int8 = -010 var f int8 = -0x10 fmt.Println(d) fmt.Println(e) fmt.Println(f) }
Výsledek po překladu a spuštění:
10 8 16 -10 -8 -16
3. Konverze mezi hodnotami různých typů
V mnoha programovacích jazycích nalezneme různá více či méně složitá pravidla, která se týkají konverzí mezi hodnotami odlišných datových typů. V jazyce Go se podobná pravidla používají taktéž a nutno říci, že jsou poměrně silná. Tato pravidla například neumožní ani implicitní převody mezi typy uint8 a uint16, což si můžeme ukázat na pátém demonstračním příkladu:
package main import "fmt" func main() { var a uint8 = 255 var b uint16 = a fmt.Println(a) fmt.Println(b) }
Teoreticky by měla být konverze mezi osmibitovým číslem bez znaménka na 16bitové číslo (taktéž bez znaménka) bezproblémová. Přesto překladač jazyka Go tuto implicitní konverzi nepovolí, protože se řídí filozofií, že explicitní přetypování je z dlouhodobého hlediska výhodnější než sada implicitních pravidel:
./05_improper_conversion.go:14:6: cannot use a (type uint8) as type uint16 in assignment
Podobně je tomu i při opačné konverzi, kde samozřejmě dochází ke ztrátě informace:
package main import "fmt" func main() { var a uint16 = 255 var b uint8 = a fmt.Println(a) fmt.Println(b) }
Výsledek pokusu o překlad:
./06_improper_conversion.go:14:6: cannot use a (type uint16) as type uint8 in assignment
Taktéž implicitní převody mezi celočíselnými typy se stejnou bitovou šířkou, ovšem s odlišným rozsahem (se znaménkem, bez znaménka), nejsou povoleny, což si vyzkoušíme v sedmém demonstračním příkladu:
package main import "fmt" func main() { var a int8 = 100 var b uint8 = a fmt.Println(a) fmt.Println(b) var c uint8 = 100 var d int8 = c fmt.Println(c) fmt.Println(d) }
Výsledek pokusu o překlad tohoto příkladu:
./07_improper_conversion_int_uint.go:14:6: cannot use a (type int8) as type uint8 in assignment ./07_improper_conversion_int_uint.go:20:6: cannot use c (type uint8) as type int8 in assignment
Naprostou většinu konverzí je nutné ve zdrojových kódech zapisovat explicitně tak, jak je to ukázáno v dalším demonstračním příkladu. Povšimněte si, že se zde nepoužívá klasické přetypování, ale zápis konverze vypadá jako volání běžné funkce:
package main import "fmt" func main() { var a int8 = -10 var signed_int int32 = -100000 var unsigned_int uint32 = 100000 var e float32 = 1e4 var f float64 = 1.5e30 var x int32 = int32(a) var y int32 = int32(e) var z float32 = float32(f) fmt.Println(x) fmt.Println(y) fmt.Println(z) var b2 uint8 = uint8(signed_int) var b3 uint8 = uint8(unsigned_int) fmt.Println(b2) fmt.Println(b3) }
Tento příklad již půjde bez problémů přeložit i spustit:
-10 10000 1.5e+30 96 160
4. Tisk a formátování celočíselných hodnot
Pro tisk a popř. i základní naformátování celočíselných hodnot na standardním či na chybovém výstupu slouží funkce fmt.Printf. Tato funkce jako první parametr akceptuje takzvaný formátovací řetězec, jehož obsah je do jisté míry podobný, jako je tomu v céčkovské standardní funkci printf, popř. javovské metodě String.format. Pro celočíselné datové typy se používá především použití dekadické soustavy (%d), šestnáctkové soustavy (%x), určení šířky/počtu cifer (%5d), specifikace doplnění nul zleva (%05d), změna zarovnání (%-5d) a vynucení tisku znaménka (%+5d). Podívejme se na následující příklad s několika možnostmi formátování:
package main import "fmt" func main() { var a uint8 = 20 var b uint16 = 2000 var c uint32 = 20000 var d uint32 = 2000000 fmt.Println("%d") fmt.Printf("%d\n", a) fmt.Printf("%d\n", b) fmt.Printf("%d\n", c) fmt.Printf("%d\n", d) fmt.Println("\n%5d") fmt.Printf("%5d\n", a) fmt.Printf("%5d\n", b) fmt.Printf("%5d\n", c) fmt.Printf("%5d\n", d) fmt.Println("\n%05d") fmt.Printf("%05d\n", a) fmt.Printf("%05d\n", b) fmt.Printf("%05d\n", c) fmt.Printf("%05d\n", d) fmt.Println("\n%-5d") fmt.Printf("%-5d\n", a) fmt.Printf("%-5d\n", b) fmt.Printf("%-5d\n", c) fmt.Printf("%-5d\n", d) fmt.Println("\n%+5d") fmt.Printf("%+5d\n", a) fmt.Printf("%+5d\n", b) fmt.Printf("%+5d\n", c) fmt.Printf("%+5d\n", d) fmt.Println("\n%x") fmt.Printf("%x\n", a) fmt.Printf("%x\n", b) fmt.Printf("%x\n", c) fmt.Printf("%x\n", d) fmt.Println("\n%X") fmt.Printf("%X\n", a) fmt.Printf("%X\n", b) fmt.Printf("%X\n", c) fmt.Printf("%X\n", d) fmt.Println("\n%b") fmt.Printf("%b\n", a) fmt.Printf("%b\n", b) fmt.Printf("%b\n", c) fmt.Printf("%b\n", d) }
Výsledky ukazují vliv, jaký má formátovací řetězec na výsledný tvar čísla na standardním výstupu:
%d 20 2000 20000 2000000 %5d 20 2000 20000 2000000 %05d 00020 02000 20000 2000000 %-5d 20 2000 20000 2000000 %+5d +20 +2000 +20000 +2000000 %x 14 7d0 4e20 1e8480 %X 14 7D0 4E20 1E8480 %b 10100 11111010000 100111000100000 111101000010010000000
U záporných čísel se vždy bude vypisovat znaménko, nehledě na použití %+.. ve formátovacím řetězci:
package main import "fmt" func main() { var a int8 = -10 var b int16 = -1000 var c int32 = -10000 var d int32 = -1000000 var r1 rune = 'a' var r2 rune = '\x40' var r3 rune = '\n' var r4 rune = '\u03BB' fmt.Println("%d") fmt.Printf("%d\n", a) fmt.Printf("%d\n", b) fmt.Printf("%d\n", c) fmt.Printf("%d\n", d) fmt.Println("\n%5d") fmt.Printf("%5d\n", a) fmt.Printf("%5d\n", b) fmt.Printf("%5d\n", c) fmt.Printf("%5d\n", d) fmt.Println("\n%05d") fmt.Printf("%05d\n", a) fmt.Printf("%05d\n", b) fmt.Printf("%05d\n", c) fmt.Printf("%05d\n", d) fmt.Println("\n%-5d") fmt.Printf("%-5d\n", a) fmt.Printf("%-5d\n", b) fmt.Printf("%-5d\n", c) fmt.Printf("%-5d\n", d) fmt.Println("\n%+5d") fmt.Printf("%+5d\n", a) fmt.Printf("%+5d\n", b) fmt.Printf("%+5d\n", c) fmt.Printf("%+5d\n", d) fmt.Println("\n%x") fmt.Printf("%x\n", a) fmt.Printf("%x\n", b) fmt.Printf("%x\n", c) fmt.Printf("%x\n", d) fmt.Println("\n%X") fmt.Printf("%X\n", a) fmt.Printf("%X\n", b) fmt.Printf("%X\n", c) fmt.Printf("%X\n", d) fmt.Println("\n%b") fmt.Printf("%b\n", a) fmt.Printf("%b\n", b) fmt.Printf("%b\n", c) fmt.Printf("%b\n", d) fmt.Println("%c") fmt.Printf("%c\n", r1) fmt.Printf("%c\n", r2) fmt.Printf("%c\n", r3) fmt.Printf("%c\n", r4) }
Výsledky:
%d -10 -1000 -10000 -1000000 %5d -10 -1000 -10000 -1000000 %05d -0010 -1000 -10000 -1000000 %-5d -10 -1000 -10000 -1000000 %+5d -10 -1000 -10000 -1000000 %x -a -3e8 -2710 -f4240 %X -A -3E8 -2710 -F4240 %b -1010 -1111101000 -10011100010000 -11110100001001000000 %c a @ λ
5. Podmnožiny reálných čísel a komplexní čísla
V programovacím jazyku Go samozřejmě nalezneme i datové typy určené pro uložení podmnožiny reálných čísel. Podobně, jako je tomu v prakticky všech moderních programovacích jazycích, jsou tyto datové typy odvozeny od formátů numerických hodnot specifikovaných v normě IEEE 754 (ať již v její základní variantě, či novější verzi). K dispozici jsou tedy dva základní typy: čísla s jednoduchou přesností (single) a čísla s přesností dvojitou (double).
V mnoha programovacích jazycích se jména těchto typů odvozují skutečně od zmíněné normy IEEE 754, ovšem v Go (a podobně i v Rustu, i když Rust používá ještě kratší jména) jsou zvolena odlišná označení, konkrétně float32 a float64. Kromě toho najdeme v jazyku Go i typy pojmenované complex64 a complex128 pro reprezentaci komplexních čísel. Interně se jedná o dvojici float32+float32 nebo float64+float64:
Označení | Rozsah hodnot | Stručný popis |
---|---|---|
float32 | –3,4×1038 až 3,4×1038 | číslo s jednoduchou přesností podle IEEE 754 |
float64 | –1,7×10308 až 1,7×10308 | číslo s dvojitou přesností podle IEEE 754 |
complex64 | ± rozsah float32 + i ± rozsah float32 | dvojice hodnot s jednoduchou přesností |
complex128 | ± rozsah float64 + i ± rozsah float64 | dvojice hodnot s dvojitou přesností |
Nejdříve se podívejme na různé způsoby deklarací proměnných typu float32 a float64:
package main import "fmt" func main() { var a float32 = -1.5 var b float32 = 1.5 var c float32 = 1e30 var d float32 = 1e-30 fmt.Println(a) fmt.Println(b) fmt.Println(c) fmt.Println(d) var e float64 = -1.5 var f float64 = 1.5 var g float64 = 1e300 var h float64 = 1e-300 fmt.Println(e) fmt.Println(f) fmt.Println(g) fmt.Println(h) }
Překladač jazyka Go opět kontroluje, zda se nesnažíme používat příliš velké konstanty, které neodpovídají povolenému rozsahu:
package main func main() { var c float32 = 1e300 var d float32 = -1e300 var g float64 = 1e3000 var h float64 = -1e3000 }
Výsledky kontroly překladačem:
./12_fp_types_checks.go:11:18: constant 1e+300 overflows float32 ./12_fp_types_checks.go:12:18: constant -1e+300 overflows float32 ./12_fp_types_checks.go:14:18: constant 1e+3000 overflows float64 ./12_fp_types_checks.go:15:18: constant -1e+3000 overflows float64
Vyzkoušejme se nyní, jakým způsobem se zapisují hodnoty typu complex64 nebo complex128. V jazyce Go se používá zápis, který pravděpodobně znáte z matematiky: za druhé číslo se připojuje označení imaginární jednotky. Ta je v Go zapisována znakem i (matematický způsob) a nikoli j (používáno v elektro oborech, taktéž v Pythonu). Nutno říci, že zápis s „i“ je z typografického hlediska hezčí :-). V dalším programu si povšimněte, že můžeme zapsat pouze imaginární část čísla, ovšem před „i“ musí být vždy alespoň jedna číslice (jinak by došlo k chybě popř. k přiřazení hodnoty proměnné i, pokud náhodou existuje):
package main import "fmt" func main() { var a complex64 = -1.5 + 0i var b complex64 = 1.5 + 1000i var c complex64 = 1e30 + 1e30i var d complex64 = 1i fmt.Println(a) fmt.Println(b) fmt.Println(c) fmt.Println(d) var e complex128 = -1.5 + 0i var f complex128 = 1.5 + 1000i var g complex128 = 1e300 + 1e300i fmt.Println(e) fmt.Println(f) fmt.Println(g) }
Zajímavý je způsob zobrazení hodnot komplexních čísel – celá hodnota je umístěna do závorek:
(-1.5+0i) (1.5+1000i) (1e+30+1e+30i) (0+1i) (-1.5+0i) (1.5+1000i) (1e+300+1e+300i)
6. Pravdivostní hodnoty
V programovacím jazyku Go je samozřejmě možné pracovat i s pravdivostními hodnotami, které jsou reprezentovány pravdivostním typem (bool, boolean). Tento datový typ používá pouze dvě hodnoty, které jsou pojmenovány klasicky true a false (pro Pythonisty: s malými písmeny na začátku). Příklad základního použití tohoto datového typu je velmi jednoduchý:
package main import "fmt" func main() { var a bool = true var b bool = false fmt.Println(a) fmt.Println(b) }
Na tomto místě je vhodné upozornit na fakt, že v programovacím jazyce Go není možné provést automatický a implicitní převod mezi celočíselným typem a pravdivostní hodnotou. Podobně nelze provést převod mezi řetězcem (polem atd.) a pravdivostní hodnotou, na rozdíl od některých jiných programovacích jazyků (příkladem může být Python s několika hodnotami, které se vyhodnotí jako false, kdežto všechny ostatní hodnoty mohou být v logických výrazech považovány za true). Vyzkoušejme si tuto vlastnost jazyka Go na dalším demonstračním příkladu:
package main import "fmt" func main() { var a bool = true var b bool = false var c bool = 0 var d bool = "" fmt.Println(a) fmt.Println(b) fmt.Println(c) fmt.Println(d) }
Při pokusu o překlad tohoto zdrojového kódu se zobrazí chybová hlášení o nemožnosti provedení konverze:
./15_boolean_type_checks.go:15:6: cannot use 0 (type int) as type bool in assignment ./15_boolean_type_checks.go:16:15: cannot use "" (type string) as type bool in assignment
7. Řetězce
Naprostá většina moderních programovacích jazyků podporuje práci s řetězci, přesněji řečeno řetězce jsou plnohodnotným datovým typem (výjimkou je vlastně jen klasické céčko, které rozpoznává jen řetězcové literály). Nejinak je tomu i v programovacím jazyce Go, v němž nalezneme datový typ string. Interně se s řetězcem pracuje stejně, jako s řezem (slice) pole bajtů (s pojmem řez se seznámíme v dalších kapitolách). A právě z tohoto způsobu práce s řetězci vychází i jejich základní vlastnosti, které mohou některé programátory překvapit a je dobré je znát:
- V první řadě jsou řetězce neměnitelné (immutable).
- Dále je u řetězců známá jejich délka, ovšem udávaná v bajtech, nikoli ve znacích.
- Při přístupu k jednotlivým prvkům řetězce pomocí operátoru indexování [] získáme jednotlivé bajty, nikoli znaky!
- A za čtvrté: v řetězci je skutečně možné mít uloženy libovolné kombinace bajtů. Teprve až ve chvíli, kdy budeme chtít pracovat s jednotlivými znaky, budou se jednotlivé sekvence bajtů transformovat do Unicode (k tomu je ovšem nutné použít buď jednu z forem smyčkyfor nebo knihovní funkce).
I když jsou řetězce interně reprezentovány jako sekvence bajtů, můžeme ve zdrojovém kódu používat řetězcové literály obsahující znaky z Unicode. Z předchozího článku již víme, že zdrojové kódy psané v Go jsou uloženy v UTF-8, takže by to nemělo představovat žádný problém. Povšimněte si, že samozřejmě můžeme použít prázdný řetězec a v řetězci mohou být umístěny řídicí znaky zapisované stejně, jako v C/C++, Javě atd. atd:
package main import "fmt" func main() { var s1 string = "www.root.cz" var s2 string = "" var s3 string = "Hello\nworld!\n" var s4 string = "шщэюя" fmt.Println(s1) fmt.Println(s2) fmt.Println(s3) fmt.Println(s4) }
V některých situacích může být výhodné zapsat řetězec v přesně takové podobě, jak má být uložen v paměti; tj. budeme potřebovat zrušit význam řídicích znaků „\n“ atd. I to je možné, protože programovací jazyk Go podporuje takzvané „raw“ (surové) řetězce, které se nezapisují do uvozovek, ale do zpětných apostrofů (backtick). Díky tomuto způsobu zápisu řetězce se vyhneme nutnosti zdvojování zpětného lomítka a podobných triků:
package main import "fmt" func main() { var s1 string = "Hello\nworld!\n" var s2 string = `Hello\nworld!\n` fmt.Println(s1) fmt.Println(s2) }
Nakonec si ještě ukážeme – prozatím bez hlubší analýzy – jak vlastně vypadá pole bajtů vytvořené z řetězcového literálu, který obsahuje různé znaky z Unicode:
package main import "fmt" func main() { var s string = "Hello\nworld!\nžluťoučký kůň" for i := 0; i < len(s); i++ { fmt.Printf("%02x ", s[i]) } }
Po spuštění tohoto příkladu by se na standardním výstupu měly objevit tyto hodnoty:
48 65 6c 6c 6f 0a 77 6f 72 6c 64 21 0a c5 be 6c 75 c5 a5 6f 75 c4 8d 6b c3 bd 20 6b c5 af c5 88
Příště si ukážeme užitečnější příklad, v němž se budeme řetězci zabývat podrobněji.
8. Deklarace lokálních proměnných
S deklaracemi proměnných jsme se již setkali v první části tohoto seriálu, takže si jen krátce zopakujme, že lokální proměnné je nutné deklarovat a popř. je můžeme i inicializovat. Při zápisu můžeme použít zkrácený zápis – vzhledem k tomu, že překladač pozná, jakého typu je hodnota přiřazovaná do proměnné, může být explicitní specifikace jejího typu poněkud nadbytečné. Pro zkrácený zápis deklarace proměnné s její inicializací je v programovacím jazyku Go vytvořen operátor :=, který se používá následujícím způsobem (opět ho známe z předchozího článku):
package main import "fmt" func main() { a := 10 fmt.Println(a) b := "hello" fmt.Println(b) c := true fmt.Println(c) }
9. Implicitní hodnoty proměnných
Dále je ve specifikaci jazyka Go přesně určeno, jaké hodnoty budou mít neinicializované proměnné. U číselných hodnot je to nula (reprezentovaná různým způsobem), u řetězců prázdný řetězec atd. Toto chování platí i pro lokální proměnné:
package main import "fmt" func main() { var i1 int8 var i2 int32 var u1 uint8 var u2 uint32 var f1 float32 var f2 float64 var c1 complex64 var c2 complex128 var s string fmt.Println(i1) fmt.Println(i2) fmt.Println(u1) fmt.Println(u2) fmt.Println(f1) fmt.Println(f2) fmt.Println(c1) fmt.Println(c2) fmt.Println(s) }
Tento příklad půjde bez problémů přeložit a spustit s těmito výsledky (včetně prázdného řádku na konci):
0 0 0 0 0 0 (0+0i) (0+0i)
10. Pole
V programovacím jazyce Go se, podobně jako v mnoha dalších programovacích jazycích, setkáme s poli (array). Pole jsou homogenní, což znamená, že prvky pole mají vždy stejný a již v čase překladu známý typ. Pole mají současně (minimálně v Go) neměnnou délku, kterou je nutné specifikovat při vytváření pole. Současně jsou pole plnohodnotnými datovými typy s vlastní sémantikou a dokonce i vlastními metadaty, na rozdíl od jazyka C, v němž je práce se skutečnými poli v mnoha případech omezena typovým systémem.
Podívejme se nejdříve na způsob definice pole bez jeho přímé inicializace. Prvky pole budou mít v takovém případě nulovou hodnotu:
var a1 [10]byte
Pole ovšem můžeme současně inicializovat:
a3 := [10]int32{1,10,2,9,3,8,4,7,5,6}
„Čtení“ datového typu je zde provedeno přímočaře zleva doprava: „pole deseti prvků typu int32 s hodnotami 1,10 …“
Vytvořit můžeme i vícerozměrná pole:
var matice [10][10]float32
Pro zjištění délky pole použijeme funkci len, tj. například:
x := len(a1)
A pro přístup k prvkům se používají klasické hranaté závorky, přičemž první prvek má nulový index:
for i:= 0; i < len(a1); i++ { a[i] = i*2; }
Podívejme se nyní na úplný příklad, v němž se deklaruje a použije několik polí:
package main import "fmt" func main() { var a1 [10]byte var a2 [10]int32 a3 := [10]int32{1,10,2,9,3,8,4,7,5,6} fmt.Printf("Delka pole 1: %d\n", len(a1)) fmt.Printf("Delka pole 2: %d\n", len(a2)) fmt.Printf("Delka pole 3: %d\n", len(a3)) var a[10]int fmt.Printf("Pole pred upravou: %v\n", a) for i:= 0; i < len(a1); i++ { a[i] = i*2; } fmt.Printf("Pole po uprave: %v\n", a) var matice [10][10]float32 fmt.Printf("Matice: %v\n", matice) }
11. Kopie polí
Zajímavé bude zjistit, co se stane v případě, že v programu vytvoříme novou proměnnou a přiřadíme jí pole:
a2 := a1
V tomto případě se sémantika odlišuje podle použitého jazyka:
- V Go se provede skutečná kopie pole, takže výsledkem budou dvě na sobě nezávislá pole.
- V Javě (například) se jen přiřadí reference, takže dvě proměnné budou ukazovat na stejné pole.
Chování Go si můžeme velmi snadno otestovat na následujícím příkladu, v němž nejprve vytvoříme kopii pole a posléze původní pole změníme:
package main import "fmt" func main() { var a1[10]int a2 := a1 fmt.Printf("Pole 1: %v\n", a1) fmt.Printf("Pole 2: %v\n", a2) for i:= 0; i <len(a1); i++ { a1[i] = i*2; } fmt.Printf("Pole 1: %v\n", a1) fmt.Printf("Pole 2: %v\n", a2) }
Výsledek odpovídá předchozímu popisu – pole jsou odlišná:
Pole 1: [0 0 0 0 0 0 0 0 0 0] Pole 2: [0 0 0 0 0 0 0 0 0 0] Pole 1: [0 2 4 6 8 10 12 14 16 18] Pole 2: [0 0 0 0 0 0 0 0 0 0]
Přibližně syntakticky (ne sémanticky!) ekvivalentní program v Javě by mohl vypadat takto:
import java.util.Arrays; public class Test { public static void main(String[] args) { int[] a1 = new int[10]; int[] a2 = a1; for (int i=0; i<a1.length; i++) { a1[i] = i*2; } System.out.println(Arrays.toString(a1)); System.out.println(Arrays.toString(a2)); } }
Po překladu a spuštění se ovšem vypíšou dva stejné řádky – to znamená, že v programové smyčce jsme měnili prvky pole sdíleného mezi proměnnými a1 a a2:
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18] [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
12. Řezy (slices), jejich význam a rozdílné vlastnosti oproti polím
Jak jsme si již řekli v předchozích kapitolách, nemusí být klasické pole dostatečně flexibilní datovou strukturou, která by plně vyhovovala potřebám vytvářené aplikace popř. implementovaného algoritmu. Pro dosažení větší flexibility byl do programovacího jazyka Go přidán další datový typ nazývaný řez neboli slice. Interně se jedná o referenci na automaticky vytvořené pole nebo na pole, které je explicitně „nasalámováno“ operací, s níž se seznámíme v navazující kapitole. Každý řez je v operační paměti uložen ve formě trojice hodnot (jde o záznam – struct či record):
- Ukazatele (reference) na zvolený prvek pole s daty, ke kterým přes řez přistupujeme.
- Délky řezu, tj. počtu prvků.
- Kapacity řezu (do jaké míry může řez narůstat v důsledku přidávání dalších prvků).
Tato interní struktura řezů s sebou přináší několik zajímavých důsledků. Je totiž možné, aby existovalo větší množství řezů ukazujících na obecně různé prvky jediného pole. Pokud nyní změníme prvek v jednom řezu, znamená to, že se vlastně modifikuje obsah původního pole a i ostatní řezy nový prvek uvidí. Co je však užitečnější – s řezy jako s datovým typem se velmi snadno pracuje; řezy mohou být předávány do funkcí, vráceny z funkcí atd.
13. Základní operace s řezy
Datový typ řez (slice), s nímž jsme se setkali v předchozí kapitole, získal svůj název kvůli následující operaci provedené s polem:
a[index1:index2]
Výsledkem předchozího výrazu je nový (obecně zúžený) pohled na pole, který se nazývá řez.
Jednoduchým příkladem může získání několika řezů z pole šesti řetězců:
a := [6]string{"C", "C++", "Java", "Python", "Go", "Rust"}
Zkusme si vytvořit různé řezy a zobrazit si jejich výsledek:
slice1 := a[1:4] slice2 := a[:3] slice3 := a[2:] slice4 := a[:] fmt.Println("Array a =", a) fmt.Println("slice1 =", slice1) fmt.Println("slice2 =", slice2) fmt.Println("slice3 =", slice3) fmt.Println("slice4 =", slice4)
Výstup bude vypadat následovně:
Array a = [C C++ Java Python Go Rust] slice1 = [C++ Java Python] slice2 = [C C++ Java] slice3 = [Java Python Go Rust] slice4 = [C C++ Java Python Go Rust]
U řezů máme k dispozici již zmíněnou délku a kapacitu:
package main import "fmt" func main() { var a1 [100]byte var a2 [100]int32 fmt.Printf("Delka pole 1: %d\n", len(a1)) fmt.Printf("Delka pole 2: %d\n", len(a2)) var slice1 []byte = a1[10:20] fmt.Printf("Delka rezu 1: %d\n", len(slice1)) fmt.Printf("Kapacita rezu 1: %d\n", cap(slice1)) var slice2 = a1[20:30] fmt.Printf("Delka rezu 2: %d\n", len(slice2)) fmt.Printf("Kapacita rezu 2: %d\n", cap(slice2)) slice3 := a1[30:40] fmt.Printf("Delka rezu 3: %d\n", len(slice3)) fmt.Printf("Kapacita rezu 3: %d\n", cap(slice3)) }
Důležité je, že kapacita je počítána takovým způsobem, že dosahuje až do konce pole, tj. je obecně větší, než délka řezu. To je užitečné pro mnoho operací, které budou popsány příště:
Delka pole 1: 100 Delka pole 2: 100 Delka rezu 1: 10 Kapacita rezu 1: 90 Delka rezu 2: 10 Kapacita rezu 2: 80 Delka rezu 3: 10 Kapacita rezu 3: 70
14. Pokročilejší operace s řezy
Vzhledem k tomu, že řez je pouhým „pohledem“ na pole, znamená to, že modifikace prvků pole bude viditelná i při přístupu k prvkům řezu a naopak. Toto chování je ukázáno na dalším příkladu, v němž se nejdříve prvky modifikují přes pole a posléze přes řez:
package main import "fmt" func main() { var a[10]int slice := a[:] fmt.Printf("Pole před modifikací: %v\n", a) fmt.Printf("Řez před modifikací: %v\n", slice) for i:= 0; i < len(a); i++ { a[i] = i*2; } fmt.Printf("Pole po modifikací: %v\n", a) fmt.Printf("Řez po modifikaci: %v\n", slice) for i:= 0; i < len(slice); i++ { slice[i] = 42; } fmt.Printf("Pole po modifikací: %v\n", a) fmt.Printf("Řez po modifikaci: %v\n", slice) }
Výsledek by nás už neměl překvapit:
Pole před modifikací: [0 0 0 0 0 0 0 0 0 0] Řez před modifikací: [0 0 0 0 0 0 0 0 0 0] Pole po modifikací: [0 2 4 6 8 10 12 14 16 18] Řez po modifikaci: [0 2 4 6 8 10 12 14 16 18] Pole po modifikací: [42 42 42 42 42 42 42 42 42 42] Řez po modifikaci: [42 42 42 42 42 42 42 42 42 42]
15. Vytvoření řezu z jiného řezu
V praxi se taktéž můžeme setkat s tím, že se vytvoří řez z jiného řezu. Modifikace prvků provedená ve druhém řezu se samozřejmě přímo „propisuje“ jak do původního řezu, tak i do pole, z něhož byl původní řez vytvořen. V dalším příkladu takto modifikujeme prvky přes řez slice2:
package main import "fmt" func main() { var a[10]int slice1 := a[4:9] slice2 := slice1[3:] fmt.Printf("Pole: %v\n", a) fmt.Printf("Delka pole: %d\n\n", len(a)) fmt.Printf("Rez 1: %v\n", slice1) fmt.Printf("Delka rezu 1: %d\n", len(slice1)) fmt.Printf("Kapacita rezu 1: %d\n\n", cap(slice1)) fmt.Printf("Rez 2: %v\n", slice2) fmt.Printf("Delka rezu 2: %d\n", len(slice2)) fmt.Printf("Kapacita rezu 2: %d\n\n", cap(slice2)) slice2[0] = 99 slice2[1] = 99 fmt.Printf("Pole: %v\n", a) fmt.Printf("Rez 1: %v\n", slice1) fmt.Printf("Rez 2: %v\n", slice2) }
Výsledek zobrazený po spuštění příkladu by měl vypadat následovně:
Pole: [0 0 0 0 0 0 0 0 0 0] Delka pole: 10 Rez 1: [0 0 0 0 0] Delka rezu 1: 5 Kapacita rezu 1: 6 Rez 2: [0 0] Delka rezu 2: 2 Kapacita rezu 2: 3 Pole: [0 0 0 0 0 0 0 99 99 0] Rez 1: [0 0 0 99 99] Rez 2: [99 99]
16. Datové typy popsané příště
V navazujícím článku téma datových typů v jazyce Go dokončíme. Zabývat se budeme především ukazateli, záznamy (record, struct), mapami, ale i rozhraními (interface) a do jisté míry i kanály (channel).
17. Speciální hodnota nil a její specifický význam v jazyku Go
V jazyce Go se na několika místech můžeme setkat se speciální hodnotou nil. Tato hodnota se používá pro reprezentaci neinicializované hodnoty. Proto má nil význam především u těch datových typů, kde je nutné neinicializovanou hodnotu odlišit od ostatních hodnot. Příkladem mohou být ukazatele, rozhraní (popíšeme si je příště), mapy popř. řezy (prázdný řez je odlišný od řezu neinicializovaného). nil se naopak nepoužívá u číselných typů, řetězců ani u pravdivostních hodnot, protože v tomto případě je vždy proměnná inicializována – na nulu u číselných typů, na prázdný řetězec u řetězců a na hodnotu false u pravdivostních hodnot:
Typ | Výchozí hodnota |
---|---|
všechny numerické typy | 0, 0.0, 0.0+0.0i atd. |
řetězce | "" |
pole | prvky rekurzivně inicializované dle této tabulky |
ukazatel | nil |
řez | nil |
kanál | nil |
mapa | nil |
18. 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:
kopie polí19. Odkazy na Internetu
- The Go Programming Language (home page)
https://golang.org/ - GoDoc
https://godoc.org/ - Go (programming language), Wikipedia
https://en.wikipedia.org/wiki/Go_(programming_language) - Go Books (kniha o jazyku Go)
https://github.com/dariubs/GoBooks - The Go Programming Language Specification
https://golang.org/ref/spec - Go: the Good, the Bad and the Ugly
https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/ - Package builtin
https://golang.org/pkg/builtin/ - Package fmt
https://golang.org/pkg/fmt/ - The Little Go Book (další kniha)
https://github.com/dariubs/GoBooks - The Go Programming Language by Brian W. Kernighan, Alan A. A. Donovan
https://www.safaribooksonline.com/library/view/the-go-programming/9780134190570/ebook_split010.html - Learning Go
https://www.miek.nl/go/ - Go Bootcamp
http://www.golangbootcamp.com/ - Programming in Go: Creating Applications for the 21st Century (další kniha o jazyku Go)
http://www.informit.com/store/programming-in-go-creating-applications-for-the-21st-9780321774637 - Introducing Go (Build Reliable, Scalable Programs)
http://shop.oreilly.com/product/0636920046516.do - Learning Go Programming
https://www.packtpub.com/application-development/learning-go-programming - The Go Blog
https://blog.golang.org/ - Getting to Go: The Journey of Go's Garbage Collector
https://blog.golang.org/ismmkeynote - Go (programovací jazyk, Wikipedia)
https://cs.wikipedia.org/wiki/Go_(programovac%C3%AD_jazyk) - Rychle, rychleji až úplně nejrychleji s jazykem Go
https://www.root.cz/clanky/rychle-rychleji-az-uplne-nejrychleji-s-jazykem-go/ - Installing Go on the Raspberry Pi
https://dave.cheney.net/2012/09/25/installing-go-on-the-raspberry-pi - How the Go runtime implements maps efficiently (without generics)
https://dave.cheney.net/2018/05/29/how-the-go-runtime-implements-maps-efficiently-without-generics - Niečo málo o Go – Golang (slovensky)
http://golangsk.logdown.com/ - How Many Go Developers Are There?
https://research.swtch.com/gophercount - Most Popular Technologies (Stack Overflow Survery 2018)
https://insights.stackoverflow.com/survey/2018/#most-popular-technologies - Most Popular Technologies (Stack Overflow Survery 2017)
https://insights.stackoverflow.com/survey/2017#technology - The Go Programming Language: Release History
https://golang.org/doc/devel/release.html - Go 1.11 Release Notes
https://golang.org/doc/go1.11 - Go 1.10 Release Notes
https://golang.org/doc/go1.10 - Go 1.9 Release Notes (tato verze je stále používána)
https://golang.org/doc/go1.9 - Go 1.8 Release Notes (i tato verze je stále používána)
https://golang.org/doc/go1.8 - Go on Fedora
https://developer.fedoraproject.org/tech/languages/go/go-installation.html - Writing Go programs
https://developer.fedoraproject.org/tech/languages/go/go-programs.html - The GOPATH environment variable
https://tip.golang.org/doc/code.html#GOPATH - Command gofmt
https://tip.golang.org/cmd/gofmt/ - The Go Blog: go fmt your code
https://blog.golang.org/go-fmt-your-code - C? Go? Cgo!
https://blog.golang.org/c-go-cgo - Spaces vs. Tabs: A 20-Year Debate Reignited by Google’s Golang
https://thenewstack.io/spaces-vs-tabs-a-20-year-debate-and-now-this-what-the-hell-is-wrong-with-go/ - 400,000 GitHub repositories, 1 billion files, 14 terabytes of code: Spaces or Tabs?
https://medium.com/@hoffa/400–000-github-repositories-1-billion-files-14-terabytes-of-code-spaces-or-tabs-7cfe0b5dd7fd - Gofmt No Longer Allows Spaces. Tabs Only
https://news.ycombinator.com/item?id=7914523 - Why does Go „go fmt“ uses tabs instead of whitespaces?
https://www.quora.com/Why-does-Go-go-fmt-uses-tabs-instead-of-whitespaces - Interactive: The Top Programming Languages 2018
https://spectrum.ieee.org/static/interactive-the-top-programming-languages-2018 - Go vs. Python
https://www.peterbe.com/plog/govspy - PackageManagementTools
https://github.com/golang/go/wiki/PackageManagementTools - A Tour of Go: Type inference
https://tour.golang.org/basics/14 - Go Slices: usage and internals
https://blog.golang.org/go-slices-usage-and-internals - Go by Example: Slices
https://gobyexample.com/slices - What is the point of slice type in Go?
https://stackoverflow.com/questions/2098874/what-is-the-point-of-slice-type-in-go - The curious case of Golang array and slices
https://medium.com/@hackintoshrao/the-curious-case-of-golang-array-and-slices-2565491d4335 - Introduction to Slices in Golang
https://www.callicoder.com/golang-slices/ - Golang: Understanding ‚null‘ and nil
https://newfivefour.com/golang-null-nil.html - What does nil mean in golang?
https://stackoverflow.com/questions/35983118/what-does-nil-mean-in-golang - nils In Go
https://go101.org/article/nil.html - Go slices are not dynamic arrays
https://appliedgo.net/slices/ - Go-is-no-good (nelze brát doslova)
https://github.com/ksimka/go-is-not-good - Rust vs. Go
https://news.ycombinator.com/item?id=13430108 - Seriál Programovací jazyk Rust
https://www.root.cz/serialy/programovaci-jazyk-rust/ - Modern garbage collection: A look at the Go GC strategy
https://blog.plan99.net/modern-garbage-collection-911ef4f8bd8e - Go GC: Prioritizing low latency and simplicity
https://blog.golang.org/go15gc - Is Golang a good language for embedded systems?
https://www.quora.com/Is-Golang-a-good-language-for-embedded-systems - Running GoLang on an STM32 MCU. A quick tutorial.
https://www.mickmake.com/post/running-golang-on-an-mcu-a-quick-tutorial - Go, Robot, Go! Golang Powered Robotics
https://gobot.io/ - Emgo: Bare metal Go (language for programming embedded systems)
https://github.com/ziutek/emgo - UTF-8 history
https://www.cl.cam.ac.uk/~mgk25/ucs/utf-8-history.txt - Less is exponentially more
https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html - Should I Rust, or Should I Go
https://codeburst.io/should-i-rust-or-should-i-go-59a298e00ea9 - Setting up and using gccgo
https://golang.org/doc/install/gccgo - Elastic Tabstops
http://nickgravgaard.com/elastic-tabstops/ - Strings, bytes, runes and characters in Go
https://blog.golang.org/strings - Datový typ
https://cs.wikipedia.org/wiki/Datov%C3%BD_typ - Seriál o programovacím jazyku Rust: Základní (primitivní) datové typy
https://www.root.cz/clanky/programovaci-jazyk-rust-nahrada-c-nebo-slepa-cesta/#k09 - Seriál o programovacím jazyku Rust: Vytvoření „řezu“ z pole
https://www.root.cz/clanky/prace-s-poli-v-programovacim-jazyku-rust/#k06 - Seriál o programovacím jazyku Rust: Řezy (slice) vektoru
https://www.root.cz/clanky/prace-s-vektory-v-programovacim-jazyku-rust/#k05 - Printf Format Strings
https://www.cprogramming.com/tutorial/printf-format-strings.html - Java: String.format
https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#format-java.lang.String-java.lang.Object…- - Java: format string syntax
https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html#syntax