Hlavní navigace

Vývoj síťových aplikací v programovacím jazyku Go (pokračování)

Pavel Tišnovský

Síťové aplikace, mj. i HTTP servery, tvoří důležitou oblast, v níž je jazyk Go poměrně často nasazován. Proto se budeme popisem tvorby síťových aplikací a utilit zabývat i dnes, mj. si ukážeme i použití protokolu HTTPS.

Doba čtení: 35 minut

11. Statická šablona

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

19. Odkazy na Internetu

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)
Poznámka: číslo portu by mělo být větší než 1023, protože porty s nižšími čísly vyžadují administrátorská práva.

Ú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!
Poznámka: přepínač -v budeme při volání nástroje curl používat velmi často, protože nám zobrazí jak průběh komunikace, tak i úplnou (zpracovanou) odpověď serveru. Pokud vás bude zajímat přesný tvar odpovědi, bude postačovat použít telnet a příkaz GET.

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
Poznámka: poslední typ nijak nespecifikuje, o jaký assembler a o jakou architekturu se jedná.

Ú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.

Poznámka: zápis {{Placeholder}} je vyžadován šablonovacím systémem.

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:

  1. Změnu šablony, aby si webová aplikace pamatovala dříve zadané n
  2. Implementaci vlastního výpočtu faktoriálu
  3. 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:

  1. Server bude odpovídat rychleji, protože nemusí pro každou odpověď znovu načítat a parsovat šablonu z disku
  2. 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.

Poznámka: v příkladu budeme používat port 4443 a nikoli obvyklejší port 443. Je tomu tak z toho důvodu, že pro otevření portů s nižšími čísly je nutné mít práva administrátora.

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
Poznámka: vyplnit můžete téměř jakékoli údaje, pouze u „Common Name“ ponechejte „localhost“ popř. pravé doménové jméno (pokud ho váš počítač má přiřazené).

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:

# Demonstrační příklad Popis Cesta
1 01_simple_http_server.go jednoduchý HTTP server https://github.com/tisnik/go-fedora/blob/master/article12/01_sim­ple_http_server.go
2 02_simple_http_server_write_bytes.go alternativní způsob vytvoření sekvence bajtů z řetězce https://github.com/tisnik/go-fedora/blob/master/article12/02_sim­ple_http_server_write_bytes­.go
3 03_specify_content_type.go HTTP server se specifikací MIME typu odpovědí https://github.com/tisnik/go-fedora/blob/master/article12/03_spe­cify_content_type.go
4 04_serve_static_html_page.go server, který vrací statické HTML stránky načtené ze souborů https://github.com/tisnik/go-fedora/blob/master/article12/04_ser­ve_static_html_page.go
5 05_static_html_page.go úprava předchozího příkladu https://github.com/tisnik/go-fedora/blob/master/article12/05_sta­tic_html_page.go
6 06_serve_static_html_page_closure.go využití uzávěrů pro zápis kratšího kódu https://github.com/tisnik/go-fedora/blob/master/article12/06_ser­ve_static_html_page_closu­re.go
7 07_html_templates.go základní použití šablon https://github.com/tisnik/go-fedora/blob/master/article12/07_html_tem­plates.go
8 08_factorial_prepare.go webová aplikace pro výpočet faktoriálu (příprava) https://github.com/tisnik/go-fedora/blob/master/article12/08_fac­torial_prepare.go
9 09_factorial_compute.go druhá varianta webové aplikace pro výpočet faktoriálu https://github.com/tisnik/go-fedora/blob/master/article12/09_fac­torial_compute.go
10 10_factorial_compute_const_template.go neměnná šablona stránky https://github.com/tisnik/go-fedora/blob/master/article12/10_fac­torial_compute_const_templa­te.go
11 11_https_server.go jednoduchý HTTPS server https://github.com/tisnik/go-fedora/blob/master/article12/11_https_ser­ver.go

19. Odkazy na Internetu

  1. Writing Web Applications
    https://golang.org/doc/articles/wiki/
  2. Golang Web Apps
    https://www.reinbach.com/blog/golang-webapps-1/
  3. Build web application with Golang
    https://legacy.gitbook.com/bo­ok/astaxie/build-web-application-with-golang/details
  4. Golang Templates – Golang Web Pages
    https://www.youtube.com/wat­ch?v=TkNIETmF-RU
  5. Simple Golang HTTPS/TLS Examples
    https://github.com/denji/golang-tls
  6. Playing with images in HTTP response in golang
    https://www.sanarias.com/blog/1214Pla­yingwithimagesinHTTPrespon­seingolang
  7. MIME Types List
    https://www.freeformatter.com/mime-types-list.html
  8. Go Mutex Tutorial
    https://tutorialedge.net/golang/go-mutex-tutorial/
  9. Creating A Simple Web Server With Golang
    https://tutorialedge.net/go­lang/creating-simple-web-server-with-golang/
  10. Building a Web Server in Go
    https://thenewstack.io/building-a-web-server-in-go/
  11. How big is the pipe buffer?
    https://unix.stackexchange­.com/questions/11946/how-big-is-the-pipe-buffer
  12. How to turn off buffering of stdout in C
    https://stackoverflow.com/qu­estions/7876660/how-to-turn-off-buffering-of-stdout-in-c
  13. setbuf(3) – Linux man page
    https://linux.die.net/man/3/setbuf
  14. setvbuf(3) – Linux man page (stejný obsah jako předchozí stránka)
    https://linux.die.net/man/3/setvbuf
  15. Select waits on a group of channels
    https://yourbasic.org/golang/select-explained/
  16. Rob Pike: Simplicity is Complicated (video)
    http://www.golang.to/posts/dotgo-2015-rob-pike-simplicity-is-complicated-youtube-16893
  17. Algorithms to Go
    https://yourbasic.org/
  18. Využití knihovny Pygments (nejenom) pro obarvení zdrojových kódů
    https://www.root.cz/clanky/vyuziti-knihovny-pygments-nejenom-pro-obarveni-zdrojovych-kodu/
  19. 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/
  20. Go Defer Simplified with Practical Visuals
    https://blog.learngoprogram­ming.com/golang-defer-simplified-77d3b2b817ff
  21. 5 More Gotchas of Defer in Go — Part II
    https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-ii-cc550f6ad9aa
  22. The Go Blog: Defer, Panic, and Recover
    https://blog.golang.org/defer-panic-and-recover
  23. The defer keyword in Swift 2: try/finally done right
    https://www.hackingwithswift.com/new-syntax-swift-2-defer
  24. Swift Defer Statement
    https://andybargh.com/swift-defer-statement/
  25. Modulo operation (Wikipedia)
    https://en.wikipedia.org/wi­ki/Modulo_operation
  26. Node.js vs Golang: Battle of the Next-Gen Languages
    https://www.hostingadvice­.com/blog/nodejs-vs-golang/
  27. The Go Programming Language (home page)
    https://golang.org/
  28. GoDoc
    https://godoc.org/
  29. Go (programming language), Wikipedia
    https://en.wikipedia.org/wi­ki/Go_(programming_langua­ge)
  30. Go Books (kniha o jazyku Go)
    https://github.com/dariubs/GoBooks
  31. The Go Programming Language Specification
    https://golang.org/ref/spec
  32. Go: the Good, the Bad and the Ugly
    https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/
  33. Package builtin
    https://golang.org/pkg/builtin/
  34. Package fmt
    https://golang.org/pkg/fmt/
  35. The Little Go Book (další kniha)
    https://github.com/dariubs/GoBooks
  36. The Go Programming Language by Brian W. Kernighan, Alan A. A. Donovan
    https://www.safaribookson­line.com/library/view/the-go-programming/9780134190570/e­book_split010.html
  37. Learning Go
    https://www.miek.nl/go/
  38. Go Bootcamp
    http://www.golangbootcamp.com/
  39. Programming in Go: Creating Applications for the 21st Century (další kniha o jazyku Go)
    http://www.informit.com/sto­re/programming-in-go-creating-applications-for-the-21st-9780321774637
  40. Introducing Go (Build Reliable, Scalable Programs)
    http://shop.oreilly.com/pro­duct/0636920046516.do
  41. Learning Go Programming
    https://www.packtpub.com/application-development/learning-go-programming
  42. The Go Blog
    https://blog.golang.org/
  43. Getting to Go: The Journey of Go's Garbage Collector
    https://blog.golang.org/ismmkeynote
  44. Go (programovací jazyk, Wikipedia)
    https://cs.wikipedia.org/wi­ki/Go_(programovac%C3%AD_ja­zyk)
  45. Rychle, rychleji až úplně nejrychleji s jazykem Go
    https://www.root.cz/clanky/rychle-rychleji-az-uplne-nejrychleji-s-jazykem-go/
  46. Installing Go on the Raspberry Pi
    https://dave.cheney.net/2012/09/25/in­stalling-go-on-the-raspberry-pi
  47. 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
  48. Niečo málo o Go – Golang (slovensky)
    http://golangsk.logdown.com/
  49. How Many Go Developers Are There?
    https://research.swtch.com/gop­hercount
  50. Most Popular Technologies (Stack Overflow Survery 2018)
    https://insights.stackover­flow.com/survey/2018/#most-popular-technologies
  51. Most Popular Technologies (Stack Overflow Survery 2017)
    https://insights.stackover­flow.com/survey/2017#techno­logy
  52. JavaScript vs. Golang for IoT: Is Gopher Winning?
    https://www.iotforall.com/javascript-vs-golang-iot/
  53. The Go Programming Language: Release History
    https://golang.org/doc/de­vel/release.html
  54. Go 1.11 Release Notes
    https://golang.org/doc/go1.11
  55. Go 1.10 Release Notes
    https://golang.org/doc/go1.10
  56. Go 1.9 Release Notes (tato verze je stále používána)
    https://golang.org/doc/go1.9
  57. Go 1.8 Release Notes (i tato verze je stále používána)
    https://golang.org/doc/go1.8
  58. Go on Fedora
    https://developer.fedorapro­ject.org/tech/languages/go/go-installation.html
  59. Writing Go programs
    https://developer.fedorapro­ject.org/tech/languages/go/go-programs.html
  60. The GOPATH environment variable
    https://tip.golang.org/doc/co­de.html#GOPATH
  61. Command gofmt
    https://tip.golang.org/cmd/gofmt/
  62. The Go Blog: go fmt your code
    https://blog.golang.org/go-fmt-your-code
  63. C? Go? Cgo!
    https://blog.golang.org/c-go-cgo
  64. 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/
  65. 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
  66. Gofmt No Longer Allows Spaces. Tabs Only
    https://news.ycombinator.com/i­tem?id=7914523
  67. Why does Go „go fmt“ uses tabs instead of whitespaces?
    https://www.quora.com/Why-does-Go-go-fmt-uses-tabs-instead-of-whitespaces
  68. Interactive: The Top Programming Languages 2018
    https://spectrum.ieee.org/sta­tic/interactive-the-top-programming-languages-2018
  69. Go vs. Python
    https://www.peterbe.com/plog/govspy
  70. PackageManagementTools
    https://github.com/golang/go/wi­ki/PackageManagementTools
  71. A Tour of Go: Type inference
    https://tour.golang.org/basics/14
  72. Go Slices: usage and internals
    https://blog.golang.org/go-slices-usage-and-internals
  73. Go by Example: Slices
    https://gobyexample.com/slices
  74. What is the point of slice type in Go?
    https://stackoverflow.com/qu­estions/2098874/what-is-the-point-of-slice-type-in-go
  75. The curious case of Golang array and slices
    https://medium.com/@hackintoshrao/the-curious-case-of-golang-array-and-slices-2565491d4335
  76. Introduction to Slices in Golang
    https://www.callicoder.com/golang-slices/
  77. Golang: Understanding ‚null‘ and nil
    https://newfivefour.com/golang-null-nil.html
  78. What does nil mean in golang?
    https://stackoverflow.com/qu­estions/35983118/what-does-nil-mean-in-golang
  79. nils In Go
    https://go101.org/article/nil.html
  80. Go slices are not dynamic arrays
    https://appliedgo.net/slices/
  81. Go-is-no-good (nelze brát doslova)
    https://github.com/ksimka/go-is-not-good
  82. Rust vs. Go
    https://news.ycombinator.com/i­tem?id=13430108
  83. Seriál Programovací jazyk Rust
    https://www.root.cz/seria­ly/programovaci-jazyk-rust/
  84. Modern garbage collection: A look at the Go GC strategy
    https://blog.plan99.net/modern-garbage-collection-911ef4f8bd8e
  85. Go GC: Prioritizing low latency and simplicity
    https://blog.golang.org/go15gc
  86. Is Golang a good language for embedded systems?
    https://www.quora.com/Is-Golang-a-good-language-for-embedded-systems
  87. Running GoLang on an STM32 MCU. A quick tutorial.
    https://www.mickmake.com/post/running-golang-on-an-mcu-a-quick-tutorial
  88. Go, Robot, Go! Golang Powered Robotics
    https://gobot.io/
  89. Emgo: Bare metal Go (language for programming embedded systems)
    https://github.com/ziutek/emgo
  90. UTF-8 history
    https://www.cl.cam.ac.uk/~mgk25/uc­s/utf-8-history.txt
  91. Less is exponentially more
    https://commandcenter.blog­spot.com/2012/06/less-is-exponentially-more.html
  92. Should I Rust, or Should I Go
    https://codeburst.io/should-i-rust-or-should-i-go-59a298e00ea9
  93. Setting up and using gccgo
    https://golang.org/doc/install/gccgo
  94. Elastic Tabstops
    http://nickgravgaard.com/elastic-tabstops/
  95. Strings, bytes, runes and characters in Go
    https://blog.golang.org/strings
  96. Datový typ
    https://cs.wikipedia.org/wi­ki/Datov%C3%BD_typ
  97. Seriál o programovacím jazyku Rust: Základní (primitivní) datové typy
    https://www.root.cz/clanky/pro­gramovaci-jazyk-rust-nahrada-c-nebo-slepa-cesta/#k09
  98. 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
  99. Seriál o programovacím jazyku Rust: Řezy (slice) vektoru
    https://www.root.cz/clanky/prace-s-vektory-v-programovacim-jazyku-rust/#k05
  100. Printf Format Strings
    https://www.cprogramming.com/tu­torial/printf-format-strings.html
  101. Java: String.format
    https://docs.oracle.com/ja­vase/8/docs/api/java/lang/Strin­g.html#format-java.lang.String-java.lang.Object…-
  102. Java: format string syntax
    https://docs.oracle.com/ja­vase/8/docs/api/java/util/For­matter.html#syntax
  103. Selectors
    https://golang.org/ref/spec#Selectors
  104. Calling Go code from Python code
    http://savorywatt.com/2015/09/18/ca­lling-go-code-from-python-code/
  105. Go Data Structures: Interfaces
    https://research.swtch.com/interfaces
  106. How to use interfaces in Go
    http://jordanorelli.com/pos­t/32665860244/how-to-use-interfaces-in-go
  107. Interfaces in Go (part I)
    https://medium.com/golangspec/in­terfaces-in-go-part-i-4ae53a97479c
  108. Part 21: Goroutines
    https://golangbot.com/goroutines/
  109. Part 22: Channels
    https://golangbot.com/channels/
  110. [Go] Lightweight eventbus with async compatibility for Go
    https://github.com/asaske­vich/EventBus
  111. What about Trait support in Golang?
    https://www.reddit.com/r/go­lang/comments/8mfykl/what_a­bout_trait_support_in_golan­g/
  112. 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/
  113. Control Flow
    https://en.wikipedia.org/wi­ki/Control_flow
  114. Structured programming
    https://en.wikipedia.org/wi­ki/Structured_programming
  115. Control Structures
    https://www.golang-book.com/books/intro/5
  116. Control structures – Go if else statement
    http://golangtutorials.blog­spot.com/2011/06/control-structures-if-else-statement.html
  117. Control structures – Go switch case statement
    http://golangtutorials.blog­spot.com/2011/06/control-structures-go-switch-case.html
  118. Control structures – Go for loop, break, continue, range
    http://golangtutorials.blog­spot.com/2011/06/control-structures-go-for-loop-break.html
  119. Single Function Exit Point
    http://wiki.c2.com/?Single­FunctionExitPoint
  120. Entry point
    https://en.wikipedia.org/wi­ki/Entry_point
  121. Why does Go have a GOTO statement?!
    https://www.reddit.com/r/go­lang/comments/kag5q/why_do­es_go_have_a_goto_statemen­t/
  122. Effective Go
    https://golang.org/doc/ef­fective_go.html
  123. GoClipse: an Eclipse IDE for the Go programming language
    http://goclipse.github.io/
  124. GoClipse Installation
    https://github.com/GoClip­se/goclipse/blob/latest/do­cumentation/Installation.md#in­stallation
Našli jste v článku chybu?