Obsah
1. Vývoj síťových aplikací v programovacím jazyku Go (pokračování)
2. Jednoduchý HTTP server – postup při tvorbě odpovědi posílané zpět klientovi
3. Nastavení hlavičky Content-Type
4. Posílání statických a dynamických stránek
5. Vylepšení předchozího příkladu – funkce pro načtení a vrácení statické stránky
6. Další zjednodušení, tentokrát s využitím uzávěrů (closures)
7. Šablona dynamicky generované HTML stránky
9. Kostra webové aplikace pro výpočet faktoriálu
10. Dokončení webové aplikace pro výpočet faktoriálu
12. Použití protokolu HTTPS namísto HTTP
13. Implementace jednoduchého HTTPS serveru
14. Vygenerování privátního klíče serveru a certifikátu
15. Přístup k certifikátu z klienta
16. Připojení klienta s využitím známého certifikátu
17. Použití certifikátu ve webovém prohlížeči s GUI
18. Repositář s demonstračními příklady
1. Vývoj síťových aplikací v programovacím jazyku Go (pokračování)
Dnešní článek naváže na jedenácté pokračování seriálu o programovacím jazyku Go, v němž jsme si ukázali některé (prozatím dosti základní) možnosti nabízené balíčkem net a taktéž balíčkem net/http. Dnes se zaměříme především na tvorbu serverů používajících protokoly HTTP a HTTPS. Nejprve si ukážeme, jakým způsobem se vlastně používá typ ResponseWriter, dále si vysvětlíme specifikaci MIME typů v hlavičce HTTP protokolu a následně se budeme zabývat různými způsoby posílání statických i dynamických HTML stránek, a to samozřejmě včetně použití jednoduchého šablonovacího nástroje (templates), který je taktéž součástí základní knihovny programovacího jazyka Go (i když je samozřejmě možné použít i jiné šablonovací systémy). Ve druhé části článku si popíšeme základy tvorby serveru používajícího protokol HTTPS a nikoli „pouze“ HTTP. I tuto možnost nám balíček net/http nabízí, ovšem budeme muset vykonat i nějakou práci navíc (vygenerování klíčů, certifikátů atd.).
2. Jednoduchý HTTP server – postup při tvorbě odpovědi posílané zpět klientovi
S tvorbou jednoduchého HTTP serveru jsme se již seznámili v předchozím článku. Připomeňme si, že základem pro implementaci HTTP serveru je v programovacím jazyku Go funkce nazvaná HandleFunc z balíčku net/http, která nám umožňuje zaregistrovat obslužnou funkci (handler) v případě, že je server volán s určitým URL (endpointem). Můžeme si například zaregistrovat handler pro endpoint / (pouhé lomítko):
http.HandleFunc("/", mainEndpoint)
Námi implementovaný handler představovaný funkcí mainEndpoint bude posílat na výstup (který je typu ResponseWriter) jednoduchý text, jenž bude zaslán klientovi v celé a automaticky zkonstruované HTTP odpovědi (s hlavičkami, stavovým kódem, délkou atd. atd.). Je zde pouze jediné omezení – pro typ ResponseWriter je předepsána metoda:
Write([]byte) (int, error)
která akceptuje řez bajtů a nikoli řetězec. Jen pro připomenutí – v programovacím jazyku Go je plně podporován Unicode, takže převod obecného řetězce na řez bajtů je relativně složitou záležitostí. Samotný převod provedeme například přetypováním řetězce na []byte, takže implementace handleru může vypadat následovně:
func mainEndpoint(writer http.ResponseWriter, request *http.Request) { response := "Hello world!\n" writer.Write([]byte(response)) }
Po registraci handlerů pro všechny podporované endpointy nám již stačí server spustit na určeném portu:
http.ListenAndServe(":8000", nil)
Úplný zdrojový kód dnešního prvního demonstračního příkladu s implementací HTTP serveru vypadá následovně:
package main import ( "net/http" ) func mainEndpoint(writer http.ResponseWriter, request *http.Request) { response := "Hello world!\n" writer.Write([]byte(response)) } func main() { http.HandleFunc("/", mainEndpoint) http.ListenAndServe(":8000", nil) }
Příklad komunikace klienta se serverem:
$ curl -v localhost:8000 * Rebuilt URL to: localhost:8000/ * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 8000 (#0) > GET / HTTP/1.1 > User-Agent: curl/7.35.0 > Host: localhost:8000 > Accept: */* > < HTTP/1.1 200 OK < Date: Sun, 17 Feb 2019 16:19:04 GMT < Content-Length: 13 < Content-Type: text/plain; charset=utf-8 < Hello world!
Existuje ovšem i jednodušší způsob, jak vytvořit textovou odpověď. Ten spočívá v použití funkce nazvané WriteString [1] z balíčku io. Této funkci předáme hodnotu typu ResponseWriter popř. hodnotu takového typu, který implementuje rozhraní Writer. Text specifikovaný programátorem bude automaticky převeden na sekvenci bajtů (přesněji řečeno na řez bajtů). Příklad si tedy můžeme nepatrně zjednodušit:
package main import ( "io" "net/http" ) func mainEndpoint(writer http.ResponseWriter, request *http.Request) { io.WriteString(writer, "Hello world!\n") } func main() { http.HandleFunc("/", mainEndpoint) http.ListenAndServe(":8000", nil) }
Příklad komunikace klienta se serverem bude naprosto stejný:
$ curl -v localhost:8000 * Rebuilt URL to: localhost:8000/ * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 8000 (#0) > GET / HTTP/1.1 > User-Agent: curl/7.35.0 > Host: localhost:8000 > Accept: */* > < HTTP/1.1 200 OK < Date: Sun, 17 Feb 2019 16:19:57 GMT < Content-Length: 13 < Content-Type: text/plain; charset=utf-8 < Hello world!
3. Nastavení hlavičky Content-Type
HTTP servery by u všech odpovědí měly specifikovat MIME typ odpovědi, protože právě na základě specifikovaného typu se klient rozhodne, jakým způsobem bude odpověď zpracovávat (může ji buď nějakým způsobem interpretovat sám nebo zavolat externí aplikaci). Tato důležitá informace je při použití protokolu HTTP zapsána v hlavičce nazvané Content-Type [2], která se v handleru požadavků konfiguruje následujícím způsobem:
writer.Header().Set("Content-Type", "text/html")
U velmi často používaných typů text/html a text/plain je samozřejmě možné dodat i doplňkové informace o použité znakové sadě, například:
writer.Header().Set("Content-Type", "text/html; charset=ISO-8859-2")
Podporované a registrované MIME typy naleznete například na stránce https://www.freeformatter.com/mime-types-list.html
Podívejme se nyní na demonstrační příklad s implementací HTTP serveru se třemi endpointy:
Endpoint | MIME typ odpovědi |
---|---|
/html | text/html |
/text | text/plain |
/asm | text/x-asm |
Úplný zdrojový kód tohoto demonstračního příkladu vypadá následovně:
package main import ( "net/http" ) func endpointHtml(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("Content-Type", "text/html") response := "<body><h1>Hello world!</h1></body>\n" writer.Write([]byte(response)) } func endpointText(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("Content-Type", "text/plain") response := "Hello world!\n" writer.Write([]byte(response)) } func endpointAsm(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("Content-Type", "text/x-asm") response := "START: brk\n" writer.Write([]byte(response)) } func main() { http.HandleFunc("/html", endpointHtml) http.HandleFunc("/text", endpointText) http.HandleFunc("/asm", endpointAsm) http.ListenAndServe(":8000", nil) }
Příklad komunikace klienta se serverem (formát odpovědi je vždy zvýrazněn):
$ curl -v localhost:8000/html * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 8000 (#0) > GET /html HTTP/1.1 > User-Agent: curl/7.35.0 > Host: localhost:8000 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/html < Date: Sun, 17 Feb 2019 16:33:21 GMT < Content-Length: 35 < <body><h1>Hello world!</h1></body>
Získání čistého textu (plain text):
$ curl -v localhost:8000/text * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 8000 (#0) > GET /text HTTP/1.1 > User-Agent: curl/7.35.0 > Host: localhost:8000 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/plain < Date: Sun, 17 Feb 2019 16:33:52 GMT < Content-Length: 13 < Hello world!
Získání zdrojového kódu s jedinou instrukcí v assembleru:
$ curl -v localhost:8000/asm * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 8000 (#0) > GET /asm HTTP/1.1 > User-Agent: curl/7.35.0 > Host: localhost:8000 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/x-asm < Date: Sun, 17 Feb 2019 16:34:30 GMT < Content-Length: 11 < START: brk
4. Posílání statických a dynamických stránek
V této kapitole i v navazujících kapitolách si ukážeme některé možnosti, které nám standardní balíček net/http nabízí při posílání statických a dynamických stránek klientům. Nejprve si ukažme triviální příklad, a to poslání statické stránky uložené v souboru pojmenovaném „index.html“, který má tento obsah:
<html> <head> <title>Test</title> <meta name="Generator" content="Go"> <meta http-equiv="content-type" content="text/html; charset=utf-8"> </head> <body> <h1>Test</h1> </body> </html>
Při posílání statického obsahu máme dvě možnosti. S první možností jsme se seznámili minule: jedná se o registraci endpointu/endpointů s využitím funkce http.FileServer:
http.Handle("/", http.FileServer(http.Dir("")))
Samozřejmě nám ovšem nic nebrání v tom, abychom statickou stránku explicitně načítali a vraceli ji klientovi. Jedna z možných implementací handleru může vypadat následovně:
body, err := ioutil.ReadFile("index.html") if err == nil { fmt.Fprint(writer, string(body)) } else { writer.WriteHeader(http.StatusNotFound) fmt.Fprint(writer, "Not found!") }
Povšimněte si, že v případě chyby při načítání se klientovi vrátí odpověď, v níž je nastaven stavový kód HTTP „Not Found“.
Samozřejmě si opět ukážeme úplný zdrojový příklad se serverem, který sice vrací statické stránky, ovšem interně je generuje dynamicky (druhá stránka neexistuje, což je vhodné pro zjištění, jak se server chová i v této situaci):
package main import ( "fmt" "io/ioutil" "net/http" ) func mainEndpoint(writer http.ResponseWriter, request *http.Request) { body, err := ioutil.ReadFile("index.html") if err == nil { fmt.Fprint(writer, string(body)) } else { writer.WriteHeader(http.StatusNotFound) fmt.Fprint(writer, "Not found!") } } func missingEndpoint(writer http.ResponseWriter, request *http.Request) { body, err := ioutil.ReadFile("missing.html") if err == nil { fmt.Fprint(writer, string(body)) } else { writer.WriteHeader(http.StatusNotFound) fmt.Fprint(writer, "Not found!") } } func main() { http.HandleFunc("/", mainEndpoint) http.HandleFunc("/missing", missingPageEndpoint) http.ListenAndServe(":8000", nil) }
Příklad chování serveru při přístupu na existující stránku:
$ curl -v localhost:8000 * Rebuilt URL to: localhost:8000/ * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 8000 (#0) > GET / HTTP/1.1 > User-Agent: curl/7.35.0 > Host: localhost:8000 > Accept: */* > < HTTP/1.1 200 OK < Date: Sun, 17 Feb 2019 17:06:53 GMT < Content-Length: 225 < Content-Type: text/html; charset=utf-8 < <html> <head> <title>Test</title> <meta name="Generator" content="Go"> <meta http-equiv="content-type" content="text/html; charset=utf-8"> </head> <body> <h1>Test</h1> </body> </html>
V případě, že stránka neexistuje, bude server reagovat odpovědí se stavovým kódem „404 Not Found“:
$ curl -v localhost:8000/missing * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 8000 (#0) > GET /missing HTTP/1.1 > User-Agent: curl/7.35.0 > Host: localhost:8000 > Accept: */* > < HTTP/1.1 404 Not Found < Date: Sun, 17 Feb 2019 17:07:17 GMT < Content-Length: 10 < Content-Type: text/plain; charset=utf-8 <
5. Vylepšení předchozího příkladu – funkce pro načtení a vrácení statické stránky
V předchozím příkladu byl použit opakující se kód pro poslání HTML stránek načtených ze souborů. To samozřejmě není to nejlepší řešení, ovšem úprava (přesněji řečeno první varianta úpravy) bude snadná – společný kód přesuneme do obecnější funkce vracející obsah libovolného souboru:
func sendStaticPage(writer http.ResponseWriter, filename string) { body, err := ioutil.ReadFile(filename) if err == nil { fmt.Fprint(writer, string(body)) } else { writer.WriteHeader(http.StatusNotFound) fmt.Fprint(writer, "Not found!") } }
Oba handlery se zjednoduší tak, že budou obsahovat jediný řádek:
func mainEndpoint(writer http.ResponseWriter, request *http.Request) { sendStaticPage(writer, "index.html") } func missingPageEndpoint(writer http.ResponseWriter, request *http.Request) { sendStaticPage(writer, "missing.html") }
Zbytek příkladu zůstane stejný, takže si jen (bez dalšího testování) ukažme jeho zdrojový kód:
package main import ( "fmt" "io/ioutil" "net/http" ) func sendStaticPage(writer http.ResponseWriter, filename string) { body, err := ioutil.ReadFile(filename) if err == nil { fmt.Fprint(writer, string(body)) } else { writer.WriteHeader(http.StatusNotFound) fmt.Fprint(writer, "Not found!") } } func mainEndpoint(writer http.ResponseWriter, request *http.Request) { sendStaticPage(writer, "index.html") } func missingPageEndpoint(writer http.ResponseWriter, request *http.Request) { sendStaticPage(writer, "missing.html") } func main() { http.HandleFunc("/", mainEndpoint) http.HandleFunc("/missing", missingPageEndpoint) http.ListenAndServe(":8000", nil) }
6. Další zjednodušení, tentokrát s využitím uzávěrů (closures)
Zdrojový kód je možné ještě dále zjednodušit, a to s využitím takzvaných uzávěrů neboli closure(s). Uzávěrem označujeme funkci (může být anonymní i pojmenovaná), která na sebe má navázanou alespoň jednu volnou proměnnou, která není deklarována uvnitř funkce. Podpora uzávěrů v programovacím jazyce Go umožňuje například tvorbu funkcí sdílejících společný kontext (testy, GUI), my ovšem uzávěr použijeme z toho důvodu, abychom k funkci sendStaticPage navázali jméno souboru s požadovanou stránkou. Samotný kód této funkce se nijak nezmění, ovšem nyní ji nebudeme volat přímo z pomocných funkcí mainEndpoint a missingPageEndpoint. Namísto toho použijeme pomocnou funkci vracející uzávěr:
func staticPage(filename string) func(writer http.ResponseWriter, request *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { sendStaticPage(writer, filename) } }
Tato funkce akceptuje jako svůj jediný parametr jméno souboru se statickou stránkou a vrací novou (anonymní funkci), která má stejnou hlavičku, jako jakýkoli jiný handler:
func(writer http.ResponseWriter, request *http.Request)
Ve skutečnosti si ovšem vrácená funkce „pamatuje“ i hodnotu parametru filename, protože se jedná o uzávěr. Můžeme tedy psát:
http.HandleFunc("/", staticPage("index.html")) http.HandleFunc("/missing", staticPage("missing.html"))
Zdrojový kód se nám sice nepatrně zvětšil, ovšem přidání další stránky bude v této chvíli triviální (dokonce je možné mapování mezi endpointem a stránkou uložit například do tabulky atd.):
package main import ( "fmt" "io/ioutil" "net/http" ) func sendStaticPage(writer http.ResponseWriter, filename string) { body, err := ioutil.ReadFile(filename) if err == nil { fmt.Fprint(writer, string(body)) } else { writer.WriteHeader(http.StatusNotFound) fmt.Fprint(writer, "Not found!") } } func staticPage(filename string) func(writer http.ResponseWriter, request *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { sendStaticPage(writer, filename) } } func main() { http.HandleFunc("/", staticPage("index.html")) http.HandleFunc("/missing", staticPage("missing.html")) http.ListenAndServe(":8000", nil) }
7. Šablona dynamicky generované HTML stránky
V dalším příkladu již použijeme šablonu HTML stránky, která bude obsahovat modifikovatelné části. Tato šablona bude vypadat následovně:
<html> <head> <title>{{.Title}}</title> <meta name="Generator" content="Go"> <meta http-equiv="content-type" content="text/html; charset=utf-8"> </head> <body> <h1>{{.Header}}</h1> </body> </html>
Povšimněte si zvýrazněných textů – právě tyto části budou nahrazeny jiným (dynamicky generovaným) obsahem.
8. Zpracování šablony
Pro zpracování šablony použijeme funkce nabízené balíčkem html/template. Jedná se o funkce odvozené od text/template, ovšem upravené takovým způsobem, aby správně konvertovaly znaky, které mají v HTML speciální význam: <, > a &. Samotná šablona (viz předchozí kapitolu) je uložena v souboru nazvaném „index_template.html“ a zpracujeme ji takto:
t, err := template.ParseFiles("index_template.html")
V případě, že se zpracování šablony podaří, musíme zajistit, aby se místo {{.Title}} a {{.Header}} použily hodnoty předané programátorem. Šablonovací systém umožňuje zpracování struktur, takže si jednu takovou strukturu nadeklarujeme:
type IndexPageDynContent struct { Title string Header string }
Povšimněte si zejména toho, že položky struktury odpovídají jménům v šabloně a že začínají velkými písmeny (tudíž jsou viditelné).
Přepis zástupných textů v šabloně je snadný – strukturu vytvoříme a zavoláme metodu šablona.Execute, které předáme jak instanci typu ResponseWriter, tak i datovou strukturu:
dynData := IndexPageDynContent{Title: "Test", Header: "Welcome!"} t.Execute(writer, dynData)
Pro ladění můžeme namísto writer použít přímo os.stdout.
Nakonec přidáme i kontrolu chybové hodnoty funkce ParseFiles a metody Execute:
t, err := template.ParseFiles("index_template.html") if err != nil { writer.WriteHeader(http.StatusNotFound) fmt.Fprint(writer, "Not found!") return } dynData := IndexPageDynContent{Title: "Test", Header: "Welcome!"} err = t.Execute(writer, dynData) if err != nil { println("Error executing template") }
Implementace serveru, v němž se dynamicky generuje HTML stránka ze šablony, tedy může vypadat následovně:
package main import ( "fmt" "html/template" "net/http" ) type IndexPageDynContent struct { Title string Header string } func mainEndpoint(writer http.ResponseWriter, request *http.Request) { t, err := template.ParseFiles("index_template.html") if err != nil { writer.WriteHeader(http.StatusNotFound) fmt.Fprint(writer, "Not found!") return } dynData := IndexPageDynContent{Title: "Test", Header: "Welcome!"} err = t.Execute(writer, dynData) if err != nil { println("Error executing template") } } func main() { http.HandleFunc("/", mainEndpoint) http.ListenAndServe(":8000", nil) }
Po spuštění serveru a přístupu z klienta získáme korektní HTML stránku se správným titulkem i nadpisem:
$ curl localhost:8000 <html> <head> <title>Test</title> <meta name="Generator" content="Go"> <meta http-equiv="content-type" content="text/html; charset=utf-8"> </head> <body> <h1>Welcome!</h1> </body> </html>
9. Kostra webové aplikace pro výpočet faktoriálu
Nyní si ukážeme, jakým způsobem je možné vytvořit kostru webové aplikace určené pro výpočet faktoriálu. Začneme první verzí šablony, která obsahuje formulář, do něhož se zapisuje n a současně se v něm zobrazí výsledek z předchozího výpočtu:
<html> <head> <title>Factorial calculator</title> <meta name="Generator" content="Go"> <meta http-equiv="content-type" content="text/html; charset=utf-8"> </head> <body> <h1>Factorial calculator</h1> <form action="/" method="get"> <input type="text" name="n" value="0" size="3" />! = {{.Result}}<br /> <input type="submit" value="compute n!" /> </form> </body> </html>
První verze serveru prozatím nebude načítat n zadané uživatelem na webové stránce, ale pouze připraví šablonu a doplní do něj pevně zadaný „výsledek“ 1. Použijeme přitom novou strukturu pojmenovanou FactorialPageDynContent:
package main import ( "fmt" "html/template" "net/http" ) type FactorialPageDynContent struct { Result int } func mainEndpoint(writer http.ResponseWriter, request *http.Request) { t, err := template.ParseFiles("factorial.html") if err != nil { writer.WriteHeader(http.StatusNotFound) fmt.Fprint(writer, "Not found!") return } dynData := FactorialPageDynContent{1} err = t.Execute(writer, dynData) if err != nil { println("Error executing template") } } func main() { http.HandleFunc("/", mainEndpoint) http.ListenAndServe(":8000", nil) }
10. Dokončení webové aplikace pro výpočet faktoriálu
Dokončení webové aplikace pro výpočet faktoriálu vyžaduje tři úpravy:
- Změnu šablony, aby si webová aplikace pamatovala dříve zadané n
- Implementaci vlastního výpočtu faktoriálu
- A konečně získání hodnoty n z formuláře
Úprava šablony je jednoduchá, pouze přidáme atribut value ke vstupnímu textovému poli:
<html> <head> <title>Factorial calculator</title> <meta name="Generator" content="Go"> <meta http-equiv="content-type" content="text/html; charset=utf-8"> </head> <body> <h1>Factorial calculator</h1> <form action="/" method="get"> <input type="text" name="n" value="{{.N}}" size="3" />! = {{.Result}}<br /> <input type="submit" value="compute n!" /> </form> </body> </html>
Výpočet faktoriálu prozatím provedeme pouze pro rozsah datového typu int64:
func Factorial(n int64) int64 { switch { case n < 0: return 1 case n == 0: return 1 default: return n * Factorial(n-1) } }
Zbývá nám tedy získat hodnotu, která byla uživatelem zapsána do webového formuláře. Použijeme přitom metodu ParseForm datového typu Request. Pokud tato metoda neskončí s chybou, je možné přes metodu FormValue(ID) přistupovat k obsahu jednotlivých prvků formuláře (a případně je převádět z řetězcové podoby na celá čísla atd. atd.):
err := request.ParseForm() if err != nil { writer.WriteHeader(http.StatusBadRequest) return } n, err := strconv.ParseInt(request.FormValue("n"), 10, 64) if err != nil { n = 0 }
Celý příklad s implementací webové aplikace lze přepsat následujícím způsobem:
package main import ( "fmt" "html/template" "net/http" "strconv" ) type FactorialPageDynContent struct { N int64 Result int64 } func Factorial(n int64) int64 { switch { case n < 0: return 1 case n == 0: return 1 default: return n * Factorial(n-1) } } func mainEndpoint(writer http.ResponseWriter, request *http.Request) { err := request.ParseForm() if err != nil { writer.WriteHeader(http.StatusBadRequest) return } n, err := strconv.ParseInt(request.FormValue("n"), 10, 64) if err != nil { n = 0 } t, err := template.ParseFiles("factorial_compute.html") if err != nil { writer.WriteHeader(http.StatusNotFound) fmt.Fprint(writer, "Not found!") return } dynData := FactorialPageDynContent{N: n, Result: Factorial(n)} err = t.Execute(writer, dynData) if err != nil { println("Error executing template") } } func main() { http.HandleFunc("/", mainEndpoint) http.ListenAndServe(":8000", nil) }
11. Statická šablona
Ještě si ukažme jednu úpravu webové aplikace. Ta spočívá v tom, že se šablona bude parsovat pouze jedenkrát, a to při spuštění serveru:
t, err := template.ParseFiles("factorial_compute.html") if err != nil { println("Cannot load and parse template") os.Exit(1) }
Toto řešení přináší dvě změny v chování serveru:
- Server bude odpovídat rychleji, protože nemusí pro každou odpověď znovu načítat a parsovat šablonu z disku
- Na druhou stranu ovšem nebude možné šablonu měnit bez restartu serveru
Podívejme se, jak bude vypadat upravená varianta serveru:
package main import ( "html/template" "net/http" "os" "strconv" ) type FactorialPageDynContent struct { N int64 Result int64 } func Factorial(n int64) int64 { switch { case n < 0: return 1 case n == 0: return 1 default: return n * Factorial(n-1) } } func getNFromForm(request *http.Request) (int64, error) { err := request.ParseForm() if err != nil { return 0, err } n, err := strconv.ParseInt(request.FormValue("n"), 10, 64) if err != nil { n = 0 } return n, nil } func mainEndpoint(writer http.ResponseWriter, request *http.Request, t *template.Template) { n, err := getNFromForm(request) if err != nil { writer.WriteHeader(http.StatusBadRequest) return } result := Factorial(n) dynData := FactorialPageDynContent{N: n, Result: result} err = t.Execute(writer, dynData) if err != nil { println("Error executing template") } } func main() { t, err := template.ParseFiles("factorial_compute.html") if err != nil { println("Cannot load and parse template") os.Exit(1) } http.HandleFunc("/", func( writer http.ResponseWriter, request *http.Request) { mainEndpoint(writer, request, t) }) http.ListenAndServe(":8000", nil) }
12. Použití protokolu HTTPS namísto HTTP
Ve druhé části článku si ukážeme, jakým způsobem je možné s využitím programovacího jazyka Go a jeho základních knihoven implementovat HTTPS server. Uvidíme, že samotná implementace bude velmi podobná implementaci HTTP serveru, ovšem pro správnou funkčnost protokolu HTTPS budeme muset použít externího nástroje pro vytvoření privátního klíče serveru a jeho certifikátu. Bližší informace o samotném konceptu, na němž je HTTPS postaveno, naleznete například na stránce https://en.wikipedia.org/wiki/HTTPS.
13. Implementace jednoduchého HTTPS serveru
Samotná implementace jednoduchého HTTPS serveru se ve skutečnosti podobá implementaci běžného HTTP serveru. Jediným rozdílem je, že se server bude spouštět odlišným způsobem. Namísto:
http.ListenAndServe(":8000", nil)
použijeme:
http.ListenAndServeTLS(":4443", "server.crt", "server.key", nil)
kde „server.crt“ a „server.key“ jsou soubory, které si vygenerujeme podle návodu uvedeného v dalších kapitolách.
Korektnější bude provést kontrolu, zda funkce http.ListenAndServeTLS neskončila s chybou:
err := http.ListenAndServeTLS(":4443", "server.crt", "server.key", nil) if err != nil { log.Fatal("ListenAndServe: ", err) }
Implementace HTTPS serveru bude vypadat takto:
package main import ( "io" "log" "net/http" ) func mainEndpoint(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("Content-Type", "text/plain") io.WriteString(writer, "Hello world!\n") } func main() { http.HandleFunc("/", mainEndpoint) err := http.ListenAndServeTLS(":4443", "server.crt", "server.key", nil) if err != nil { log.Fatal("ListenAndServe: ", err) } }
Prozatím ovšem ještě nemáme připraveny všechny potřebné soubory, a to ani na straně serveru, ani na straně klienta. Proto se pokus o zavolání serveru nezdaří:
$ curl -v https://localhost:4443 * Rebuilt URL to: https://localhost:4443/ * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 4443 (#0) * successfully set certificate verify locations: * CAfile: none CApath: /etc/ssl/certs * SSLv3, TLS handshake, Client hello (1): * SSLv3, TLS handshake, Server hello (2): * SSLv3, TLS handshake, CERT (11): * SSLv3, TLS alert, Server hello (2): * SSL certificate problem: self signed certificate * Closing connection 0 curl: (60) SSL certificate problem: self signed certificate More details here: http://curl.haxx.se/docs/sslcerts.html curl performs SSL certificate verification by default, using a "bundle" of Certificate Authority (CA) public keys (CA certs). If the default bundle file isn't adequate, you can specify an alternate file using the --cacert option. If this HTTPS server uses a certificate signed by a CA represented in the bundle, the certificate verification probably failed due to a problem with the certificate (it might be expired, or the name might not match the domain name in the URL). If you'd like to turn off curl's verification of the certificate, use the -k (or --insecure) option.
14. Vygenerování privátního klíče serveru a certifikátu
Nyní musíme vygenerovat soubory server.key a server.crt, které bude naše implementace HTTPS serveru používat.
Privátní klíč používaný serverem vygenerujeme s využitím nástroje openssl, který již pravděpodobně máte v systému nainstalovaný:
$ openssl genrsa -out server.key 2048
Výsledkem by měl být soubor server.key obsahující klíč pro 2048bitové RSA:
-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAyhgV0Gmo0dCkdcEO5X0J//xKD73E+n0pyw7htM/1gPnU9h2X JYNqFnq0xz9QsxTAPHYLkueW1SNhWT9gq3Sad/M3Cxb6uomB+i0qSk71Q6PkaqHQ KveSsNNa4lw5DBFVjTD/JPnWhVvKS7v0A266snwmTi18+fRpWZ/TaQN5uQRy0bik ... ... ... RbBs8QKBgB3dl+NGC+iTPVviPixjFkP5KAcf3Is57Pi0RUgTj4Fmq2q90Scoi6Vv OzZoo2XHmqAnqxV75OWqA7NiKdBHwWg2O9BupFa+G3uRXgoP7cpCeT9ZoUbbMKww j49BC9GHmOhlcz3fBT4YE3OgoeM5Fga8sVtWew9YkKe/gBAkR0+Y -----END RSA PRIVATE KEY-----
Dále, opět nástrojem openssl, vytvoříme soubor s certifikátem s uvedenou platností:
$ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
V tomto okamžiku se bude openssl interaktivně ptát na několik údajů, které jsou v přepisu konverzace vypsány tučně:
You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]:CZ State or Province Name (full name) [Some-State]: Locality Name (eg, city) []:Kocourkov Organization Name (eg, company) [Internet Widgits Pty Ltd]:Mestska garda Organizational Unit Name (eg, section) []: Common Name (e.g. server FQDN or YOUR name) []:localhost Email Address []:nikdo@nikde.cz
Výsledný soubor nazvaný „server.crt“ může vypadat takto:
-----BEGIN CERTIFICATE----- MIIDlzCCAn+gAwIBAgIJALw/AUKjIONeMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNV BAYTAkNaMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlLb2NvdXJrb3Yx FjAUBgNVBAoMDU1lc3Rza2EgZ2FyZGExEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0x ... ... ... rnPxzautFYD++NjhJ/j537I0Lcj9t/DkjvBiECZYkJF8p9dL4+lWZXc27n3RYS6L 7Dj+85WUXwxkfPqhkggGi8jSrZesUDtWw4XFw7bLGOaKTo2JMGxOfxL3RhrFtiMO 4j9Rvz9cr2R6a0Y= -----END CERTIFICATE-----
15. Přístup k certifikátu z klienta
HTTPS server již máme připravený (a pravděpodobně i úspěšně spuštěný), takže ještě musíme provést konfiguraci na straně klienta. Nejprve získáme certifikát z běžícího serveru, opět s využitím nástroje openssl, který zkontaktuje HTTPS server a získá od něj všechny potřebné údaje:
$ openssl s_client -showcerts -connect localhost:4443
Výsledek může vypadat následovně – nejprve je zobrazen vlastní certifikát a posléze další metadata:
CONNECTED(00000003) --- Certificate chain 0 s:/C=CZ/ST=Some-State/L=Kocourkov/O=Mestska garda/CN=localhost i:/C=CZ/ST=Some-State/L=Kocourkov/O=Mestska garda/CN=localhost -----BEGIN CERTIFICATE----- MIIDlzCCAn+gAwIBAgIJALw/AUKjIONeMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNV BAYTAkNaMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlLb2NvdXJrb3Yx FjAUBgNVBAoMDU1lc3Rza2EgZ2FyZGExEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0x OTAyMTYyMDE0MTJaFw0yOTAyMTMyMDE0MTJaMGIxCzAJBgNVBAYTAkNaMRMwEQYD VQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlLb2NvdXJrb3YxFjAUBgNVBAoMDU1l c3Rza2EgZ2FyZGExEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEB BQADggEPADCCAQoCggEBAMoYFdBpqNHQpHXBDuV9Cf/8Sg+9xPp9KcsO4bTP9YD5 1PYdlyWDahZ6tMc/ULMUwDx2C5LnltUjYVk/YKt0mnfzNwsW+rqJgfotKkpO9UOj 5Gqh0Cr3krDTWuJcOQwRVY0w/yT51oVbyku79ANuurJ8Jk4tfPn0aVmf02kDebkE ctG4pIsfu6HPfBPyMFgEBXYDiObKfGCEgpnGIeX8Li4n9r8Law45+KEFz4n2Yj3c Jq77ZLopjV4w4n+JZYNXkK9JeV9twM5PrsYLqrLEvstXqyo/2ccYFtMvTsXx57SY BEKABLYuPsEzYVzNo2lgtXJxxgcXfS+PrCnH6KhS4c0CAwEAAaNQME4wHQYDVR0O BBYEFBdmG7K8HXslTnR5OkOLZCGVPD1hMB8GA1UdIwQYMBaAFBdmG7K8HXslTnR5 OkOLZCGVPD1hMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKYKeOz4 u0er6BQmy72Wc4H9ZjWnXjphfVAC0UK2gz7UHXDnyzfrBKR6FkVbeiIUjBzbrWG5 xUoHcZsfayefOEEpqcAyKpa8CRkbissHF6qtFZArt+cOWwTmmPfYQxfa9KqVP13L FcZqcchyvTLdNTGD5ZBtLI9B5Pcm4a7vgEdMqdJb++FpSNhW9H2P0wvfhTK7Mh6/ rnPxzautFYD++NjhJ/j537I0Lcj9t/DkjvBiECZYkJF8p9dL4+lWZXc27n3RYS6L 7Dj+85WUXwxkfPqhkggGi8jSrZesUDtWw4XFw7bLGOaKTo2JMGxOfxL3RhrFtiMO 4j9Rvz9cr2R6a0Y= -----END CERTIFICATE----- --- Server certificate subject=/C=CZ/ST=Some-State/L=Kocourkov/O=Mestska garda/CN=localhost issuer=/C=CZ/ST=Some-State/L=Kocourkov/O=Mestska garda/CN=localhost --- No client certificate CA names sent --- SSL handshake has read 1529 bytes and written 421 bytes --- New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES128-GCM-SHA256 Server public key is 2048 bit Secure Renegotiation IS supported Compression: NONE Expansion: NONE SSL-Session: Protocol : TLSv1.2 Cipher : ECDHE-RSA-AES128-GCM-SHA256 Session-ID: 1F74CA2806A25B6AE774B7A0D4E470A477D16B73130EAB527C03FE861086B076 Session-ID-ctx: Master-Key: 555000EA285A2EEFE95D5268756ED98CC71711075F8036251EC6B34494C7ED8F6861EDDDA842BC847922F536AC9CF0EA Key-Arg : None PSK identity: None PSK identity hint: None SRP username: None TLS session ticket: 0000 - 71 d2 38 c8 ec 5d e7 5e-c7 fe a7 f5 d4 33 03 e8 q.8..].^.....3.. 0010 - e9 ba 8d ff f2 1e e7 9f-8e 66 9b 0e 2b 34 eb db .........f..+4.. 0020 - 83 c1 4b 96 f7 a4 67 71-26 3a a5 2d 65 2e 08 ae ..K...gq&:.-e... 0030 - 84 38 3f bf 90 2e 04 0a-62 25 aa 0e 86 ca 31 4a .8?.....b%....1J 0040 - b7 2a 1b 1a b7 b0 b2 d9-d5 3c f4 9e 39 37 a1 69 .*.......<..97.i 0050 - 6c ac 2c 8b 83 d0 25 53-da 7c 43 17 4d 55 d6 fc l.,...%S.|C.MU.. 0060 - 7a 55 2f 74 bd 6a e2 6f-59 b0 cc 16 d7 e0 a9 14 zU/t.j.oY....... 0070 - 71 35 d4 62 27 85 93 f7- q5.b'... Start Time: 1550348405 Timeout : 300 (sec) Verify return code: 18 (self signed certificate) ---
Nástroj budeme muset ukončit klávesovou zkratkou Ctrl+C.
Pro klienta je nejjednodušší přesměrovat výstup z předchozího volání nástroje openssl do souboru, který bývá nazván „certs.pem“:
$ openssl s_client -showcerts -connect localhost:4443 > certs.pem
To je ze strany klienta vše – klient pouze potřebuje pro každé volání použít soubor „certs.pem“ s certifikátem serveru, aby ho mohl ověřit.
16. Připojení klienta s využitím známého certifikátu
Nyní již máme vše připravené pro to, aby se klient mohl připojit k serveru s využitím certifikátu uloženého v lokálním souboru certs.pem. Příkaz volající utilitu curl bude vypadat následovně:
$ curl -v --cacert certs.pem https://localhost:4443
Po spuštění nástroje curl by se měl klient připojit k serveru s využitím protokolu HTTPS, ověřit certifikát a následně přečíst odpověď serveru („Hello world!“):
* Rebuilt URL to: https://localhost:4443/ * Hostname was NOT found in DNS cache % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 4443 (#0) * successfully set certificate verify locations: * CAfile: certs.pem CApath: /etc/ssl/certs * SSLv3, TLS handshake, Client hello (1): } [data not shown] * SSLv3, TLS handshake, Server hello (2): { [data not shown] * SSLv3, TLS handshake, CERT (11): { [data not shown] * SSLv3, TLS handshake, Server key exchange (12): { [data not shown] * SSLv3, TLS handshake, Server finished (14): { [data not shown] * SSLv3, TLS handshake, Client key exchange (16): } [data not shown] * SSLv3, TLS change cipher, Client hello (1): } [data not shown] * SSLv3, TLS handshake, Finished (20): } [data not shown] * SSLv3, TLS change cipher, Client hello (1): { [data not shown] * SSLv3, TLS handshake, Finished (20): { [data not shown] * SSL connection using ECDHE-RSA-AES128-GCM-SHA256 * Server certificate: * subject: C=CZ; ST=Some-State; L=Kocourkov; O=Mestska garda; CN=localhost * start date: 2019-02-16 20:14:12 GMT * expire date: 2029-02-13 20:14:12 GMT * common name: localhost (matched) * issuer: C=CZ; ST=Some-State; L=Kocourkov; O=Mestska garda; CN=localhost * SSL certificate verify ok. > GET / HTTP/1.1 > User-Agent: curl/7.35.0 > Host: localhost:4443 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/plain < Date: Sat, 16 Feb 2019 20:20:29 GMT < Content-Length: 13 < { [data not shown] 100 13 100 13 0 0 1106 0 --:--:-- --:--:-- --:--:-- 1181 * Connection #0 to host localhost left intact Hello world!
Vidíme, že certifikát byl skutečně použit.
17. Použití certifikátu ve webovém prohlížeči s GUI
Pokud se používá webový prohlížeč s GUI, bývá práce s certifikáty snazší:
Obrázek 1: Informace o certifikátu při prvním přístupu k našemu serveru. Certifikát jsme si podepsali sami a nebyl potvrzen žádnou certifikační autoritou.
Obrázek 2: Zobrazení dalších informací o certifikátu – tyto informace jsme zadali při jeho vytváření.
Po potvrzení, že certifikátu důvěřujeme, se již zobrazí kýžená webová stránka.
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á přibližně jeden megabajt), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:
19. Odkazy na Internetu
- Writing Web Applications
https://golang.org/doc/articles/wiki/ - Golang Web Apps
https://www.reinbach.com/blog/golang-webapps-1/ - Build web application with Golang
https://legacy.gitbook.com/book/astaxie/build-web-application-with-golang/details - Golang Templates – Golang Web Pages
https://www.youtube.com/watch?v=TkNIETmF-RU - Simple Golang HTTPS/TLS Examples
https://github.com/denji/golang-tls - Playing with images in HTTP response in golang
https://www.sanarias.com/blog/1214PlayingwithimagesinHTTPresponseingolang - MIME Types List
https://www.freeformatter.com/mime-types-list.html - Go Mutex Tutorial
https://tutorialedge.net/golang/go-mutex-tutorial/ - Creating A Simple Web Server With Golang
https://tutorialedge.net/golang/creating-simple-web-server-with-golang/ - Building a Web Server in Go
https://thenewstack.io/building-a-web-server-in-go/ - How big is the pipe buffer?
https://unix.stackexchange.com/questions/11946/how-big-is-the-pipe-buffer - How to turn off buffering of stdout in C
https://stackoverflow.com/questions/7876660/how-to-turn-off-buffering-of-stdout-in-c - setbuf(3) – Linux man page
https://linux.die.net/man/3/setbuf - setvbuf(3) – Linux man page (stejný obsah jako předchozí stránka)
https://linux.die.net/man/3/setvbuf - 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 - JavaScript vs. Golang for IoT: Is Gopher Winning?
https://www.iotforall.com/javascript-vs-golang-iot/ - 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