Obsah
1. Datové typy v programovacím jazyku Go (2.část)
2. Deklarace vlastních uživatelských typů
12. Ukazatel na strukturu (záznam)
13. Ukazatel na položku záznamu
14. Ukazatel na vybraný prvek pole
15. Funkce jakožto plnohodnotný datový typ
16. Funkce s parametry a její typ
17. Definice vlastního datového typu funkce s návratovou hodnotou
18. Repositář s demonstračními příklady
1. Datové typy v programovacím jazyku Go (2.část)
Typový systém programovacího jazyka Go je poměrně rozsáhlý, protože kromě jednoduchých typů, polí a řetězců (které nalezneme v mnoha dalších jazycích) je možné pracovat i se záznamy (strukturami), mapami (asociativními poli), ukazateli, rozhraními a kanály. Kromě toho tento programovací jazyk považuje i funkce za plnohodnotný datový typ, což je téma, kterému se dnes budeme věnovat v poslední části článku.
Při pohledu na hierarchii datových typů programovacího jazyka Go můžeme vidět, že v předchozím a dnešním článku jsme si popsali většinu základních datových typů. Příště toto téma dokončíme popisem rozhraní a především pak kanálů (channel), které jsou jedním z důvodů, proč programátoři na Go vlastně přechází:
- 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
2. Deklarace vlastních uživatelských typů
V úvodní části dnešního článku si ukážeme, jakým způsobem je možné v programovacím jazyku Go deklarovat vlastní uživatelské typy. Z předchozích dvou článků již víme, že Go je staticky a silně typovaným jazykem, takže možnost vytvářet vlastní datové typy je velmi užitečná a jak uvidíme v dalších příkladech, umožňují uživatelsky definované datové typy (pokud se využívají skutečně důsledně) tvorbu bezpečnějších aplikací.
Nový uživatelský datový typ se vytváří s využitím klíčového slova type, za nímž se uvede jméno (identifikátor) nového typu a posléze i jeho vlastní definice. Pokud například budeme chtít vytvořit nový datový typ pojmenovaný Name, který bude odvozený od klasického řetězce (string), můžeme to provést tímto způsobem:
type Name string
Proměnnou nazvanou n, která má být typu Name můžeme vytvořit nám jíž známým způsobem s použitím klíčového slova var (čteme „proměnná n je typu Name“). Ihned poté je možné této proměnné přiřadit hodnotu (která musí být řetězcem):
var n Name n = "Jan"
Podívejme se nyní na nepatrně složitější příklad s definicí tří uživatelských typů pojmenovaných Id, Name a Surname:
package main import "fmt" type Id uint32 type Name string type Surname string func main() { var i Id i = 0 fmt.Println(i) var n Name var s Surname n = "Jan" s = "Novák" fmt.Println(n) fmt.Println(s) }
Po spuštění tohoto příkladu by se mělo vypsat:
0 Jan Novák
V dalším demonstračním příkladu je ukázáno, jak se vytváří funkce, jejíž parametry mají být určitého konkrétního typu. S tvorbou funkcí jsme se již (i když prozatím jen povrchně) seznámili v úvodním článku o programovacím jazyce Go:
package main import "fmt" type Id uint32 type Name string type Surname string func register_user(id Id, name Name, surname Surname) { fmt.Printf("Registering: %d %s %s", id, name, surname) } func main() { var i Id = 1 var n Name = "Jan" var s Surname = "Novák" register_user(i, n, s) }
Spuštění příkladu:
Registering: 1 Jan Novák
Zkusme si nyní otestovat nepatrně složitější příklad, v němž vytvoříme nový datový typ pojmenovaný Mesic a následně vytvoříme proměnnou typu „pole dvanácti měsíců“:
package main import "fmt" type Mesic string func main() { var mesice [12]Mesic fmt.Println(mesice) mesice[0] = "Leden" mesice[1] = "Únor" mesice[2] = "Březen" mesice[3] = "Duben" mesice[4] = "Květen" mesice[5] = "Červen" mesice[6] = "Červenec" mesice[7] = "Srpen" mesice[8] = "Září" mesice[9] = "Říjen" mesice[10] = "Listopad" mesice[11] = "Prosinec" fmt.Println(mesice) }
Povšimněte si, že funkce fmp.Println dokáže bez problémů vytisknout i hodnotu celého pole:
[ ] [Leden Únor Březen Duben Květen Červen Červenec Srpen Září Říjen Listopad Prosinec]
Pole samozřejmě můžeme deklarovat a ihned (v jediném příkazu) i inicializovat, což je ukázáno ve třetím demonstračním příkladu:
package main import "fmt" type Mesic string func main() { mesice := [12]Mesic { "Leden", "Únor", "Březen", "Duben", "Květen", "Červen", "Červenec", "Srpen", "Září", "Říjen", "Listopad", "Prosinec"} fmt.Println(mesice) }
Výsledek zobrazený po spuštění tohoto příkladu:
[Leden Únor Březen Duben Květen Červen Červenec Srpen Září Říjen Listopad Prosinec]
3. Silná typová kontrola
Důležitost a současně i velká praktická užitečnost uživatelských datových typů spočívá v tom, že programovací jazyk Go provádí při překladu velmi důkladnou typovou kontrolu. Nedovolí například provést přiřazení hodnoty z proměnné (či výrazu) odlišného typu, i když bráno do důsledků se může interně jednat o totožně reprezentovaná data nebo o data velmi jednoduše převoditelná (viz již zmíněnou nemožnost implicitní konverze mezi int8 a int16 apod.).
Podívejme se ostatně na pátý demonstrační příklad, který nebude možné přeložit, a to z toho důvodu, že se do proměnných typu Name a Surname snažíme přiřadit hodnotu z proměnné typu string. Z pohledu překladače programovacího jazyka Go se totiž jedná o rozdílné typy, i když interní způsob uložení Name a Surname je pochopitelně totožný s řetězcem. Takto silné typové kontroly sice mohou vypadat pedanticky, ovšem filozofií Go je používat vždy explicitní zápis (včetně případného přetypování):
package main import "fmt" type Id uint32 type Name string type Surname string func main() { var i Id i = 0 fmt.Println(i) var str = "common string" var n Name = str var s Surname = str n = "Jan" s = "Novák" fmt.Println(n) fmt.Println(s) }
Při pokusu o překlad se vypíšou dvě chybové zprávy:
./05_user_type_checks.go:23:6: cannot use str (type string) as type Name in assignment ./05_user_type_checks.go:24:6: cannot use str (type string) as type Surname in assignment
Skutečná užitečnost typového systému programovacího jazyka Go se ovšem ukáže až v šestém demonstračním příkladu. V něm máme opět deklarovanou funkci register_user, která akceptuje tři parametry s těmito typy: Id, Name a Surname. Při volání této funkce však omylem prohodíme jméno a příjmení. Pokud by funkce byla deklarovaná tak, že oba dva parametry by byly typu string, překladač by na omyl nemohl přijít. Taktéž pokud by typová kontrola byla prováděna méně pedantským způsobem, například zjištěním, že Name i Surname jsou odvozeny od řetězce, takže se mezi nimi může provést konverze, překladač by na omyl nemusel upozornit. Ovšem Go používá výše zmíněnou silnou kontrolu, takže chyba bude korektně nahlášena:
package main import "fmt" type Id uint32 type Name string type Surname string func register_user(id Id, name Name, surname Surname) { fmt.Printf("Registering: %d %s %s", id, name, surname) } func main() { var i Id = 1 var n Name = "Jan" var s Surname = "Novák" register_user(i, s, n) }
Výsledek pokusu o překlad:
./06_type_check_func.go:25:15: cannot use s (type Surname) as type Name in argument to register_user ./06_type_check_func.go:25:15: cannot use n (type Name) as type Surname in argument to register_user
V následujícím (již sedmém) příkladu se uživatelské datové typy nepoužívají; příklad si uvádíme jen proto, abychom si připomněli, že pole (přesněji řečeno prvky pole) se při přiřazování kopírují:
package main import "fmt" func main() { mesice := [12]string{ "Leden", "Únor", "Březen", "Duben", "Květen", "Červen", "Červenec", "Srpen", "Září", "Říjen", "Listopad", "Prosinec"} var mesice2 [12]string = mesice fmt.Println(mesice) fmt.Println(mesice2) }
Výsledek po spuštění:
[Leden Únor Březen Duben Květen Červen Červenec Srpen Září Říjen Listopad Prosinec] [Leden Únor Březen Duben Květen Červen Červenec Srpen Září Říjen Listopad Prosinec]
Hned navazující demonstrační příklad je sice velmi podobný příkladu předchozímu, ovšem zde se snažíme o přiřazení polí odlišného datového typu: první je totiž pole typu „dvanáct měsíců“, druhé pole je typu „dvanáct řetězců“, což jsou z pohledu překladače opět zcela odlišné a nekompatibilní datové typy:
package main import "fmt" type Mesic string func main() { mesice := [12]Mesic{ "Leden", "Únor", "Březen", "Duben", "Květen", "Červen", "Červenec", "Srpen", "Září", "Říjen", "Listopad", "Prosinec"} var mesice2 [12]string = mesice fmt.Println(mesice) fmt.Println(mesice2) }
Výsledek pokusu o překlad tohoto příkladu:
./08_typed_array_check.go:29:6: cannot use mesice (type [12]Mesic) as type [12]string in assignment
4. Záznamy (struktury)
V programovacím jazyku Go je možné deklarovat i složené (a obecně nehomogenní) datové struktury záznamy (record), které jsou ovšem v jazycích odvozených od céčka známé spíše pod jménem struktury (struct). Záznam se skládá z většího množství pojmenovaných položek, přičemž každá položka má přesně definovaný typ a současně i pozici v záznamu (pozice je důležitá ve chvíli, kdy se hodnota typu záznam vytváří, i když Go ve skutečnosti podporuje i explicitní zápis jmen a hodnot položek). Nový datový typ záznam se vytváří opět s využitím klíčového slova type; celá definice může vypadat následovně:
type User struct { id uint32 name string surname string }
Pro přístup k položkám záznamu se používá standardní tečková notace:
var user1 User user1.id = 1 user1.name = "Pepek" user1.surname = "Vyskoč"
Záznamy (resp. přesněji řečeno jejich obsah) je možné vytisknout pomocí nám již známé funkce fmt.Println:
fmt.Println(user1)
Podívejme se nyní na úplný demonstrační příklad, v němž napřed definujeme nový datový typ záznam pojmenovaný User a následně vytvoříme lokální proměnnou tohoto typu, jejíž obsah vytiskneme:
package main import "fmt" type User struct { id uint32 name string surname string } func main() { var user1 User fmt.Println(user1) user1.id = 1 user1.name = "Pepek" user1.surname = "Vyskoč" fmt.Println(user1) }
Po spuštění tohoto příkladu by se nejdříve měl vypsat čistě inicializovaný záznam (viz poznámka v navazující kapitole) a posléze záznam s položkami nastavenými uživatelem:
{0 } {1 Pepek Vyskoč}
5. Inicializace záznamů
V navazujícím příkladu je ukázáno, jak lze jediným příkazem deklarovat proměnnou typu záznam a současně i naplnit položky záznamu:
user1 := User{ 1, "Pepek", "Vyskoč"}
Zde se již používá odlišný způsob zápisu, v němž je nutné oddělit jednotlivé hodnoty čárkou. Uzavírací složená závorka se zapisuje ihned za poslední položku (překladač si formát zápisu automaticky pohlídá).
Upravený příklad vypadá takto:
package main import "fmt" type User struct { id uint32 name string surname string } func main() { user1 := User{ 1, "Pepek", "Vyskoč"} fmt.Println(user1) user1.id = 2 user1.name = "Josef" user1.surname = "Vyskočil" fmt.Println(user1) }
Výsledek po spuštění:
{1 Pepek Vyskoč} {2 Josef Vyskočil}
Programovací jazyk Go ve skutečnosti podporuje ještě jeden způsob deklarace a inicializace záznamu, v němž jsou jednotlivé položky uvedeny explicitně. Tento způsob inicializace je jednoznačnější a navíc i bezpečnější ve chvíli, kdy se změní datový typ User (například se do něj přidá další položka):
user1 := User{ id: 1, name: "Pepek", surname: "Vyskoč"}
Opět si ukažme celý <a>demonstrační příklad:
package main import "fmt" type User struct { id uint32 name string surname string } func main() { user1 := User{ id: 1, name: "Pepek", surname: "Vyskoč"} fmt.Println(user1) user1.id = 2 user1.name = "Josef" user1.surname = "Vyskočil" fmt.Println(user1) }
Výsledek by měl být totožný s příkladem předchozím:
{1 Pepek Vyskoč} {2 Josef Vyskočil}
Zajímavé je, že záznamy stejného typu (v našem případě záznamy typu User) je možné porovnávat operátorem ==. Interně se totiž porovnání provede s jednotlivými položkami, nikoli například tak, že by se pouze zjistily a porovnaly adresy obou porovnávaných záznamů. Podívejme se na jednoduchý příklad:
package main import "fmt" type User struct { id uint32 name string surname string } func main() { user1 := User{ id: 1, name: "Pepek", surname: "Vyskoč"} var user2 User user2.id = 1 user2.name = "Pepek" user2.surname = "Vyskoč" user3 := User{ id: 1, name: "Josef", surname: "Vyskočil"} fmt.Println(user1 == user2) fmt.Println(user1 == user3) }
Po spuštění tohoto příkladu získáme výsledek porovnání user1 == user2 a user1 == user3:
true false
Vidíme, že první dva záznamy jsou při porovnání shodné, i když se ve skutečnosti jedná o rozdílné bloky v operační paměti.
6. Pole záznamů
Podívejme se ještě na způsob vytvoření pole záznamů, což je v praxi poměrně často používaná datová struktura tvořící základ pro ještě častěji používaný řez záznamů. Pole, které může obsahovat tři záznamy typu User se vytvoří snadno podle nám již známého schématu:
var users [3]User
Současně se automaticky provede inicializace pole. Již minule jsme se totiž zmínili o tom, že každá datová struktura je po své alokaci v paměti i inicializována (typicky na nulovou hodnotu v případě číselných typů, řetězce jsou inicializovány prázdnými řetězci a u většiny strukturovaných datových typů se používá speciální hodnota nil). V případě záznamu se toto pravidlo vztahuje na všechny jeho položky a v případě pole na všechny prvky pole (popř. i rekurzivně na položky vnořené struktury atd.):
var users [3]User fmt.Println(users)
Druhý řádek vypíše:
[{0 } {0 } {0 }]
Úplný demonstrační příklad s polem tří záznamů by mohl vypadat následovně:
package main import "fmt" type User struct { id uint32 name string surname string } func main() { var users [3]User fmt.Println(users) user1 := User{ id: 1, name: "Pepek", surname: "Vyskoč"} var user2 User user2.id = 1 user2.name = "Pepek" user2.surname = "Vyskoč" user3 := User{ id: 1, name: "Josef", surname: "Vyskočil"} users[0] = user1 users[1] = user2 users[2] = user3 fmt.Println(users) }
Dvojice zpráv zobrazená po spuštění:
[{0 } {0 } {0 }] [{1 Pepek Vyskoč} {1 Pepek Vyskoč} {1 Josef Vyskočil}]
V praxi se taktéž někdy setkáme s tím, že se pole záznamů deklaruje a současně i inicializuje v jediném příkazu. Zápis je již nepatrně složitější a je v něm nutné dodržet formátovací pravidla programovacího jazyka Go, zejména použití čárek za jednotlivými prvky inicializovaného pole. Povšimněte si, že každý prvek pole je zapsán formou konstruktoru záznamu:
package main import "fmt" type User struct { id uint32 name string surname string } func main() { var users = [3]User{ User{ id: 1, name: "Pepek", surname: "Vyskoč"}, User{ id: 2, name: "Pepek", surname: "Vyskoč"}, User{ id: 3, name: "Josef", surname: "Vyskočil"}, } fmt.Println(users) var users2 = [...]User{ User{ id: 1, name: "Pepek", surname: "Vyskoč"}, User{ id: 2, name: "Pepek", surname: "Vyskoč"}, User{ id: 3, name: "Josef", surname: "Vyskočil"}, } fmt.Println(users2) }
Po překladu a spuštění získáme tyto dva totožné řádky:
[{1 Pepek Vyskoč} {2 Pepek Vyskoč} {3 Josef Vyskočil}] [{1 Pepek Vyskoč} {2 Pepek Vyskoč} {3 Josef Vyskočil}]
7. Mapy
V programovacím jazyku Go patří (možná poněkud překvapivě) mezi standardní datové typy i mapy (maps), které se v některých jiných jazycích nazývají asociativní pole či pouze hashe. Tyto strukturované datové typy slouží pro ukládání dvojic klíč-hodnota, přičemž typ klíčů i typ hodnot musí být známý již v době překladu (uplatňují se zde všechna pravidla pro silný typový systém jazyka Go).
Ukažme si nyní způsob deklarace proměnné pojmenované m1, která je typu mapa celé číslo:řetězec:
var m1 map[int]string
Samozřejmě můžeme vytvořit i jinou mapu, konkrétně mapu s klíči typu string a hodnotami typu User
var m2 map[string]User
V dalším příkladu vytvoříme proměnnou typu mapa a posléze se do ní pokusíme zapsat novou dvojici s klíčem rovným nule a hodnotou „nula“:
package main import "fmt" func main() { var m1 map[int]string fmt.Println(m1) m1[0] = "nula" }
Při pokusu o spuštění tohoto příkladu ovšem dojde k běhové chybě, která vypadá takto:
map[] panic: assignment to entry in nil map goroutine 1 [running]: main.main() /home/tester/go-root/article_03/15_uninitialized_map.go:16 +0x76 exit status 2
Co to znamená? Předchozí proměnná byla sice deklarována korektně, ovšem uplatnila se zde již dříve popsaná pravidla pro inicializaci hodnoty proměnné. Zde se konkrétně vytvořila mapa a byla jí přiřazena speciální hodnota nil. Pokud budeme skutečně chtít mapu použít (naplnit ji dvojicemi klíč-hodnota), musíme při inicializaci zavolat vestavěnou funkci make.
Korektní inicializace mapy je ukázána na dalším příkladu:
package main import "fmt" func main() { var m1 map[int]string = make(map[int]string) fmt.Println(m1) m1[0] = "nula" m1[1] = "jedna" m1[2] = "dva" m1[3] = "tri" m1[4] = "ctyri" m1[5] = "pet" m1[6] = "sest" fmt.Println(m1) }
Po spuštění tohoto příkladu se nejdříve vypíše prázdná mapa a posléze mapa naplněná šesti dvojicemi:
map[] map[6:sest 0:nula 1:jedna 2:dva 3:tri 4:ctyri 5:pet]
Předchozí příklad je možné přepsat s využitím operátoru := takto:
package main import "fmt" func main() { m1 := make(map[int]string) fmt.Println(m1) m1[0] = "nula" m1[1] = "jedna" m1[2] = "dva" m1[3] = "tri" m1[4] = "ctyri" m1[5] = "pet" m1[6] = "sest" fmt.Println(m1) }
8. Mapy a struktury
Do map se samozřejmě mohou ukládat i struktury, s jejichž deklarací a použitím jsme se dnes již seznámili. V dalším příkladu je deklarována mapa s dvojicemi string-User:
package main import "fmt" type User struct { id uint32 name string surname string } func main() { m1 := make(map[string]User) fmt.Println(m1) m1["prvni"] = User{ id: 1, name: "Pepek", surname: "Vyskoč"} m1["druhy"] = User{ id: 2, name: "Josef", surname: "Vyskočil"} fmt.Println(m1) }
Výsledek příkladu je předvídatelný – nejdříve se vypíše prázdná mapa a posléze mapa se dvěma dvojicemi klíč-hodnota:
map[] map[prvni:{1 Pepek Vyskoč} druhy:{2 Josef Vyskočil}]
Podívejme se nyní na to, jak se příklad změní ve chvíli, kdy jsou klíče mapy taktéž uživatelský typ. Vkládání jednotlivých prvků do mapy je nyní nepatrně složitější:
package main import "fmt" type Key struct { id uint32 role string } type User struct { id uint32 name string surname string } func main() { m1 := make(map[Key]User) fmt.Println(m1) m1[Key{1, "admin"}] = User{ id: 1, name: "Pepek", surname: "Vyskoč"} m1[Key{2, "user"}] = User{ id: 2, name: "Josef", surname: "Vyskočil"} fmt.Println(m1) }
Po spuštění tohoto příkladu by se měly vypsat tyto dva řádky:
map[] map[{1 admin}:{1 Pepek Vyskoč} {2 user}:{2 Josef Vyskočil}]
9. Čtení hodnot z map
Při čtení hodnot z map, tj. při výběru prvků s využitím klíče, je někdy nutné rozlišit mezi hodnotou nil a situací, kdy se daná dvojice klíč-hodnota v mapě ve skutečnosti vůbec nenachází. Další programovací jazyky se k tomuto problému staví různě a nabízí funkce typu exist atd. V programovacím jazyku Go je situace řešena relativně elegantně – ve skutečnosti totiž při čtení z mapy získáme dvě hodnoty: vlastní prvek, který v mapě byl uložen (popř. nil) a taktéž příznak, zda byl prvek pod daným klíčem nalezen. Můžeme tedy použít tento zápis:
value, exist := mapa[klíč]
V proměnné exist nyní bude uložena pravdivostní hodnota s informací o tom, zda byl prvek s daným klíčem nalezen či nikoli. Ihned po čtení z mapy tedy můžeme provést explicitní rozeskok:
if exist { // prvek byl nalezen } else { // prvek nebyl nalezen }
Vše si ukážeme v dnešním dvacátém demonstračním příkladu, jehož zdrojový kód vypadá následovně:
package main import "fmt" func main() { m1 := make(map[string]int) fmt.Println(m1) m1["nula"] = 0 m1["jedna"] = 1 m1["dva"] = 2 m1["tri"] = 3 m1["ctyri"] = 4 m1["pet"] = 5 m1["sest"] = 6 fmt.Println(m1) value, exist := m1["dva"] if exist { fmt.Println("Found:", value) } else { fmt.Println("Not found") } value, exist = m1["sto"] if exist { fmt.Println("Found:", value) } else { fmt.Println("Not found") } }
Po spuštění by se měly vypsat následující čtyři řádky, přičemž nás samozřejmě zajímají především poslední dvě zprávy:
map[] map[jedna:1 dva:2 tri:3 ctyri:4 pet:5 sest:6 nula:0] Found: 2 Not found
10. Vymazání hodnot z map
Do mapy je možné prvky jak přidávat (což již umíme, viz předchozí kapitolu), tak i odebírat. Operace pro odebrání dvojice klíč-hodnota z mapy prošla určitým vývojem, což mj. znamená, že na webu můžeme najít již neplatné návody. V současnosti se pro odebrání položky (přesněji řečeno pro odebrání dvojice klíč-hodnota) používá vestavěná funkce nazvaná poněkud obecně delete. Této funkci se v prvním parametru předává mapa a ve druhém parametru klíč:
delete(mapa, klíč)
Užitečné je, že tato funkce neselže ani ve chvíli, kdy jí předáme neplatný klíč, ani tehdy, pokud se namísto mapy předá hodnota nil. Ostatní kontroly (typ mapy atd.) ovšem překladač samozřejmě stále provádí.
V dnešním dvacátém prvním demonstračním příkladu je ukázáno, jakým způsobem můžeme z již zkonstruované mapy vymazat nějakou dvojici klíč-hodnota. Nejprve vymažeme existující dvojici, poté ovšem zavoláme funkci delete pro neexistující klíč:
package main import "fmt" type Key struct { id uint32 role string } type User struct { id uint32 name string surname string } func main() { m1 := make(map[Key]User) fmt.Println(m1) m1[Key{1, "admin"}] = User{ id: 1, name: "Pepek", surname: "Vyskoč"} m1[Key{2, "user"}] = User{ id: 2, name: "Josef", surname: "Vyskočil"} fmt.Println(m1) delete(m1, Key{2, "user"}) fmt.Println(m1) delete(m1, Key{1000, "nekdo jiny"}) fmt.Println(m1) }
Na výstupu zobrazeném po spuštění tohoto příkladu je patrné, že druhé volání funkce delete nemělo na výslednou mapu žádný vliv, ovšem současně neskončilo s chybou:
map[] map[{2 user}:{2 Josef Vyskočil} {1 admin}:{1 Pepek Vyskoč}] map[{1 admin}:{1 Pepek Vyskoč}] map[{1 admin}:{1 Pepek Vyskoč}]
11. Ukazatele
Dalším datovým typem, s nímž se dnes alespoň ve stručnosti seznámíme, jsou ukazatele (pointer). Ukazatele mají v Go podobný význam, jako v mnoha dalších jazycích (ostatně byly zařazeny již do Pascalu apod.), ovšem na rozdíl od C/C++ jsou operace s nimi důsledně omezeny a práce s ukazateli je tak v Go bezpečnější. U každého ukazatele je nutné specifikovat, na jaký datový typ ukazuje. Příkladem je ukazatel na hodnotu typu int uloženou někde v operační paměti. Proměnná s ukazatelem musí být typu *int (hvězdičku zde čteme jako „ukazatel na“):
var p_i *int
Implicitní hodnotou přiřazenou do proměnné tohoto typu je nil:
fmt.Println(p_i)
Ukazatel na hodnotu uloženou v paměti se získá unárním operátorem &:
p_i = &i
V této chvíli je v proměnné p_i uložena adresa paměťové buňky, v níž je uložena hodnota proměnná i. Můžeme si tedy vypsat jak onu adresu, tak i hodnotu přečtenou z této adresy. Pro nepřímé (indirect) čtení přes ukazatel se používá unární operátor *:
fmt.Println(p_i) fmt.Println(*p_i)
Díky tomu, že výraz *p_i odkazuje na hodnotu uloženou v operační paměti, můžeme s touto hodnotou provádět i další operace, například její zvýšení o jedničku:
*p_i++
Podívejme se nyní na zdrojový kód celého příkladu:
package main import "fmt" func main() { var i int = 42 fmt.Println(i) var p_i *int fmt.Println(p_i) p_i = &i fmt.Println(p_i) fmt.Println(*p_i) *p_i++ fmt.Println(i) fmt.Println(*p_i) }
Po spuštění tohoto příkladu by se měly zobrazit následující řádky:
42 <nil> 0xc0000140e0 42 43 43
12. Ukazatel na strukturu (záznam)
V praxi se poměrně často setkáme s ukazatelem obsahujícím adresu nějaké struktury (záznamu). Takový ukazatel se vytvoří poměrně snadno. V případě, že existuje proměnná u typu User, získáme odkaz na hodnotu v paměti opět s využitím unárního operátoru &:
var u User var p_u *User p_u = &u
Pokud znáte programovací jazyk C a C++, asi vás napadlo, že přístup k položkám záznamu, na který známe ukazatel, bude prováděn s využitím operátoru ->. Ve skutečnosti tomu tak není; tento operátor Go nezná. Musíme tedy použít buď poměrně nečitelný explicitně zapsaný přístup přes *p_u s tečkovou notací (a s uzavřením do závorek kvůli prioritě operátorů):
(*p_u).id = 10000
Nebo můžeme využít další užitečnou vlastnost Go: automatické dereference při použití tečkového operátoru. Tato dereference je umožněna díky silnému typovému systému:
p_u.id = 20000
Ukažme si nyní obě možnosti na jednoduchém příkladu, v němž vytvoříme záznam, získáme na něj ukazatel a posléze přistoupíme k položce záznamu s využitím tečkového operátoru s explicitní i implicitní dereferencí:
package main import "fmt" type User struct { id uint32 name string surname string } func main() { var u User u.id = 1 u.name = "Pepek" u.surname = "Vyskoč" fmt.Println(u) var p_u *User p_u = &u fmt.Println(p_u) fmt.Println(*p_u) (*p_u).id = 10000 fmt.Println(*p_u) p_u.id = 20000 fmt.Println(*p_u) }
Výsledek spuštění tohoto příkladu (povšimněte si především druhého řádku – zde se šikovně zobrazí obsah referencovaného záznamu):
{1 Pepek Vyskoč} &{1 Pepek Vyskoč} {1 Pepek Vyskoč} {10000 Pepek Vyskoč} {20000 Pepek Vyskoč}
13. Ukazatel na položku záznamu
Bez dalších obsáhlých popisů se podívejme na způsob získání ukazatele na jednu položku záznamu:
package main import "fmt" type User struct { id uint32 name string surname string } func main() { var u User u.id = 1 u.name = "Pepek" u.surname = "Vyskoč" fmt.Println(u) var p_n *string p_n = &u.name fmt.Println(p_n) fmt.Println(*p_n) (*p_n) = "Zdeněk" fmt.Println(*p_n) }
Tento příklad bude samozřejmě funkční, o čemž se můžeme snadno přesvědčit:
{1 Pepek Vyskoč} 0xc0000840f8 Pepek Zdeněk
14. Ukazatel na vybraný prvek pole
Získat a použít je možné i ukazatel na vybraný prvek pole:
package main import "fmt" func main() { mesice := [12]string{ "Leden", "Únor", "Březen", "Duben", "Květen", "Červen", "Červenec", "Srpen", "Září", "Říjen", "Listopad", "Prosinec"} fmt.Println(mesice) p_treti := &mesice[2] *p_treti = "March" fmt.Println(mesice) }
15. Funkce jakožto plnohodnotný datový typ
V programovacím jazyku Go jsou funkce plnohodnotným datovým typem, což v praxi znamená, že funkci (interně referenci na funkci) je možné přiřadit do proměnné, předat do jiné funkce jako parametr atd. Ukažme si nejprve první případ, tj. uložení reference na funkci do lokální proměnné s pozdějším zavoláním této funkce. Proměnná, která je typu funkce bez parametrů a návratové hodnoty, se deklaruje následovně:
var a func()
Do takové proměnné můžeme přiřadit referenci na existující funkci prostým operátorem přiřazení (se jménem funkce se nyní zachází jako s běžnou proměnnou):
func funkce1() { fmt.Println("funkce1") } a = funkce1
A nakonec můžeme funkci zavolat, nikoli ovšem jejím skutečným jménem, ale přes proměnnou a:
a()
Tento způsob práce s funkcemi je ukázán v dalším demonstračním příkladu:
package main import "fmt" func funkce1() { fmt.Println("funkce1") } func funkce2() { fmt.Println("funkce2") } func main() { var a func() fmt.Println(a) a = funkce1 fmt.Println(a) a() a = funkce2 fmt.Println(a) a() }
Zkusme si nyní tento příklad spustit:
<nil> 0x484b10 funkce1 0x484b80 funkce2
Můžeme vidět, že se nejprve vypíše hodnota nil, tj. výchozí hodnota proměnné typu func(). Posléze se vypíše adresa (reference) na funkci a funkce se zavolá. Tytéž kroky jsou provedeny i s druhou funkcí.
16. Funkce s parametry a její typ
Podobně jako v případě polí, záznamů či ukazatelů je nutné datový typ funkce blíže specifikovat, protože funkce se od sebe liší jak počtem a typem svých parametrů, tak i počtem a typy návratových hodnot (prozatím pro jednoduchost nebudeme uvažovat funkce s proměnným počtem parametrů). Ukažme si příklad s funkcí, která bude mít dva parametry typu celé číslo (int) a jednu návratovou hodnotou:
func funkce1(x int, y int) int { return x + y }
Typ této funkce je func(int, int) int a proměnnou tohoto typu nadeklarujeme snadno:
var a func(int, int) int
Po přiřazení reference skutečné funkce můžeme proměnnou a použít pro její volání:
fmt.Println(a(10, 20))
Opět se podívejme na úplný demonstrační příklad:
package main import "fmt" func funkce1(x int, y int) int { return x + y } func funkce2(x int, y int) int { return x * y } func main() { var a func(int, int) int fmt.Println(a) a = funkce1 fmt.Println(a) fmt.Println(a(10, 20)) a = funkce2 fmt.Println(a) fmt.Println(a(10, 20)) }
Po spuštění tohoto příkladu dostaneme těchto pět řádků se zprávami:
<nil> 0x484b90 30 0x484bb0 200
package main import "fmt" func funkce1(x int) int { return 2 * x } func funkce2(x int, y int) int { return x * y } func main() { var a func(int) int fmt.Println(a) a = funkce1 fmt.Println(a) fmt.Println(a(10)) a = funkce2 fmt.Println(a) fmt.Println(a(10, 20)) }
V tomto příkladu se snažíme o přiřazení funkce se dvěma parametry typu int do proměnné, jejíž typ je funkce s jedním parametrem. Tato programátorská chyba je samozřejmě opět odhalena už překladačem:
./28_improper_func_type.go:21:4: cannot use funkce2 (type func(int, int) int) as type func(int) int in assignment ./28_improper_func_type.go:23:15: too many arguments in call to a have (number, number) want (int)
17. Definice vlastního datového typu funkce s návratovou hodnotou
V dnešním posledním demonstračním příkladu si ukážeme, jak lze vytvořit nový uživatelský datový typ představující referenci na funkci určitého typu. Pro tento účel opět použijeme klíčové slovo type:
type no_param_function func() int type two_int_param_function func(int, int) int
Alternativně můžeme obě definice sloučit do jediného bloku, což se v Go děje poměrně často:
type ( no_param_function func() int two_int_param_function func(int, int) int )
Deklarace proměnných nových typů je již snadná:
var a no_param_function var b two_int_param_function
Opět si ukažme celý příklad:
package main import "fmt" type no_param_function func() int type two_int_param_function func(int, int) int func funkce1() int { return 42 } func funkce2() int { return -1 } func funkce3(x int, y int) int { return x + y } func funkce4(x int, y int) int { return x * y } func main() { var a no_param_function var b two_int_param_function fmt.Println(a) fmt.Println(b) a = funkce1 fmt.Println(a) fmt.Println(a()) a = funkce2 fmt.Println(a) fmt.Println(a()) b = funkce3 fmt.Println(b) fmt.Println(b(10, 20)) b = funkce4 fmt.Println(b) fmt.Println(b(10, 20)) }
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:
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 - 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/