Standardní šablonovací systém jazyka Go a šablony HTML stránek

1. 2. 2022
Doba čtení: 35 minut

Sdílet

 Autor: Depositphotos
Navážeme na předchozí články o využití šablonovacího systému v jazyce Go. Ukážeme si, jak je možné použít HTML šablony a jak je lze integrovat do služeb postavených na HTTP serveru.

Obsah

1. Standardní šablonovací systém jazyka Go a šablony HTML stránek

2. Textové šablony vs. HTML šablony

3. Krátké zopakování – jednoduchá textová šablona a výsledek po její aplikaci

4. Balíček text/template a HTML šablona

5. Použití balíčku html/template namísto text/template

6. Problematika XSS a balíčku text/template

7. Zabránění XSS aplikací šablony přes balíček html/template

8. Kontext, ve kterém se data do HTML stránky vkládají při aplikaci šablony

9. HTML šablony a HTTP server realizovaný v jazyce Go

10. Ukázka jednoduchého serveru, který poskytuje pouze statické stránky

11. HTTP server a aplikace HTML šablon

12. Benchmark: rychlost načtení a aplikace šablon

13. Vytvoření šablon pouze při inicializaci HTTP serveru

14. Detekce modifikace šablon

15. Příklad detekce změn v souborech

16. HTTP server detekující změny šablon

17. Úplný zdrojový kód serveru

18. Závěr

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Standardní šablonovací systém jazyka Go a šablony HTML stránek

Na dva předchozí články [1] [2], v nichž jsme si ukázali možnosti standardního šablonovacího systému programovacího jazyka Go, dnes navážeme. Ukážeme si totiž, jakým způsobem je možné použít HTML šablony. Ve skutečnosti totiž ve standardní knihovně jazyka Go existuje kromě standardního šablonovacího systému text/template i odvozená varianta nazvaná html/template. Ta je určena pro aplikaci šablon HTML stránek. Ovšem ve skutečnosti se nemusí jednat pouze o čisté HTML, protože šablonovací systém dokáže pracovat například i s CSS (tedy s kaskádními styly).

Ve druhé části dnešního článku si ukážeme, jak lze šablony použít přímo v backendu, tedy ve webovém serveru, který bude generovat HTML stránky či jejich části a posílat je klientovi (nebude se tedy jednat o dnes tak populární SPA). Taktéž si vysvětlíme, jak zařídit, aby se šablony nemusely načítat při každém požadavku klienta a současně aby bylo možné šablony měnit za běhu serveru (což je velmi užitečné, a to nejenom při vývoji).

Poznámka: většina dále uvedených demonstračních příkladů je založena pouze na standardní knihovně programovacího jazyka Go. Pouze dva příklady s detekcí změny textu šablony využívají externí knihovnu (relativně malou). Konkrétně se jedná o knihovnu fsnotify/fsnotify.

2. Textové šablony vs. HTML šablony

Jak se však od sebe vlastně odlišuje balíček text/template od balíčku html/template? Šablonovací systém implementovaný v balíčku text/template jednoduše aplikuje šablonu na předaná data, přičemž se nesnaží žádným způsobem „rozumět“ šabloně či datům – předpokládá se, že jak samotná šablona, tak i předávaná data jsou v takové podobě, že nedojde k žádným problematickým jevům – například že se „rozhodí“ výsledný dokument kvůli nějakému znaku se speciálním významem, který je součástí dat.

U balíčku html/template je tomu jinak, protože se předpokládá, že výsledkem aplikace šablony má být HTML stránka, jež bude zobrazena ve webovém prohlížeči uživatele. A současně se předpokládá, že data mohou pocházet z neověřeného zdroje (například mohou být zadána potenciálním útočníkem). Balíček html/template je tedy navržen takovým způsobem, aby vhodným způsobem upravil data před jejich vložením do šablony tak, aby výsledná HTML stránka byla (do značné míry) korektní. Cílem je zabránit jak „pouhému“ špatnému zobrazení stránky (například pokud do dat někdo omylem vloží neuzavřený HTML prvek), tak i útokům typu XSS.

3. Krátké zopakování – jednoduchá textová šablona a výsledek po její aplikaci

Připomeňme si, jak může vypadat jednoduchá (čistě textová) šablona, která je zpracovatelná balíčkem text/template. Konkrétně se jedná o šablonu, která je aplikovatelná na pole (resp. řez) obsahující záznamy s prvky pojmenovanými Name, Surname a Popularity. Při aplikaci šablony se postupně prochází jednotlivými záznamy a pro každý záznam se vygenerují tři textové řádky (poslední řádek bude prázdný). Povšimněte si, že přímo ze šablony je možné volat například standardní funkci str.Printf popř. použít podmínky:

--------------------------------------------------------------------
{{range .}}Jméno {{printf "%-15s" .Name}} {{printf "%-15s" .Surname}} {{if gt .Popularity 0}} Popularita {{printf "%2d" .Popularity}} {{end}}
{{end}}
--------------------------------------------------------------------

Šablonu budeme aplikovat na pole/řez se záznamy typu:

type Role struct {
        Name       string
        Surname    string
        Popularity int
}

Konkrétně se bude jednat o následující záznamy:

roles := []Role{
        Role{"Eliška", "Najbrtová", 4},
        Role{"Jenny", "Suk", 3},
        Role{"Anička", "Šafářová", 0},
        Role{"Sváťa", "Pulec", 3},
        Role{"Blažej", "Motyčka", 8},
        Role{"Eda", "Wasserfall", 0},
        Role{"Přemysl", "Hájek", 10},
}

Podívejme se nyní na úplný zdrojový kód demonstračního příkladu, v němž je implementováno načtení šablony i její aplikace na předaná data:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateFilename = "template17.txt"
)
 
// datový typ, jehož prvky budou vypisovány v šabloně
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.ParseFiles(templateFilename))
 
        // tyto hodnoty budou použity při aplikaci šablony
        roles := []Role{
                Role{"Eliška", "Najbrtová", 4},
                Role{"Jenny", "Suk", 3},
                Role{"Anička", "Šafářová", 0},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "Motyčka", 8},
                Role{"Eda", "Wasserfall", 0},
                Role{"Přemysl", "Hájek", 10},
        }
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, roles)
        if err != nil {
                panic(err)
        }
}

Výsledek získaný po spuštění tohoto demonstračního příkladu by měl vypadat následovně:

--------------------------------------------------------------------
Jméno Eliška          Najbrtová        Popularita  4
Jméno Jenny           Suk              Popularita  3
Jméno Anička          Šafářová
Jméno Sváťa           Pulec            Popularita  3
Jméno Blažej          Motyčka          Popularita  8
Jméno Eda             Wasserfall
Jméno Přemysl         Hájek            Popularita 10
 
--------------------------------------------------------------------

Úplný zdrojový kód demonstračního příkladu z této kapitoly je dostupný na adrese https://github.com/tisnik/go-root/blob/master/article80/tem­plate17.go.

4. Balíček text/template a HTML šablona

Samozřejmě nám nic nebrání v tom použít balíček text/template společně s HTML šablonou. Můžeme například předchozí textovou šablonu nahradit za šablonu, která obsahuje kostru HTML stránky. Díky tomu, že HTML používá jiné speciální znaky než šablonovací systém Go, je realizace takové šablony snadná:

<html>
    <head>
        <title>Role ve hře Švestka</title>
    </head>
    <body>
        <h1>Role ve hře Švestka</h1>
        <table>
            <tr><th>Jméno</th><th>Příjmení</th><th>Popularita</th></tr>
{{range .}}
            <tr><td>{{.Name}}</td><td>{{.Surname}}</td><td>{{if gt .Popularity 0}}{{.Popularity}}{{else}}&times;{{end}}</td></tr>
{{end}}
        </table>
    </body>
</html>
Poznámka: ve skutečnosti je do šablony ještě přidána větev „else“, aby se vyplnily všechny buňky HTML tabulky.

Samotný zdrojový kód příkladu se – až na odlišné jméno šablony – nebude odlišovat od příkladu, který byl popsán v předchozí kapitole:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateFilename = "html_template01.htm"
)
 
// datový typ, jehož prvky budou vypisovány v šabloně
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.ParseFiles(templateFilename))
 
        // tyto hodnoty budou použity při aplikaci šablony
        roles := []Role{
                Role{"Eliška", "Najbrtová", 4},
                Role{"Jenny", "Suk", 3},
                Role{"Anička", "Šafářová", 0},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "Motyčka", 8},
                Role{"Eda", "Wasserfall", 0},
                Role{"Přemysl", "Hájek", 10},
        }
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, roles)
        if err != nil {
                panic(err)
        }
}

Výsledek, tedy vygenerovaná HTML stránka, odpovídá očekávání:

<html>
    <head>
        <title>Role ve hře Švestka</title>
    </head>
    <body>
        <h1>Role ve hře Švestka</h1>
        <table>
            <tr><th>Jméno</th><th>Příjmení</th><th>Popularita</th></tr>
 
            <tr><td>Eliška</td><td>Najbrtová</td><td>4</td></tr>
 
            <tr><td>Jenny</td><td>Suk</td><td>3</td></tr>
 
            <tr><td>Anička</td><td>Šafářová</td><td>&times;</td></tr>
 
            <tr><td>Sváťa</td><td>Pulec</td><td>3</td></tr>
 
            <tr><td>Blažej</td><td>Motyčka</td><td>8</td></tr>
 
            <tr><td>Eda</td><td>Wasserfall</td><td>&times;</td></tr>
 
            <tr><td>Přemysl</td><td>Hájek</td><td>10</td></tr>
 
        </table>
    </body>
</html>

Obrázek 1: Vygenerovaná HTML stránka po zobrazení ve webovém prohlížeči.

5. Použití balíčku html/template namísto text/template

Náhrada balíčku text/template za balíček html/template je triviální, protože se jedná pouze o změnu jména balíčku, který je importován. Konkrétně namísto zvýrazněného importu:

import (
        "os"
        "text/template"
)

použijeme:

import (
        "html/template"
        "os"
)

Žádné další změny není nutné ve zdrojovém kódu provádět, o čemž se ostatně můžeme velmi snadno přesvědčit:

package main
 
import (
        "html/template"
        "os"
)
 
const (
        templateFilename = "html_template02.htm"
)
 
// datový typ, jehož prvky budou vypisovány v šabloně
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.ParseFiles(templateFilename))
 
        // tyto hodnoty budou použity při aplikaci šablony
        roles := []Role{
                Role{"Eliška", "Najbrtová", 4},
                Role{"Jenny", "Suk", 3},
                Role{"Anička", "Šafářová", 0},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "Motyčka", 8},
                Role{"Eda", "Wasserfall", 0},
                Role{"Přemysl", "Hájek", 10},
        }
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, roles)
        if err != nil {
                panic(err)
        }
}

Použitá šablona se taktéž nezmění:

<html>
    <head>
        <title>Role ve hře Švestka</title>
    </head>
    <body>
        <h1>Role ve hře Švestka</h1>
        <table>
            <tr><th>Jméno</th><th>Příjmení</th><th>Popularita</th></tr>
{{range .}}
            <tr><td>{{.Name}}</td><td>{{.Surname}}</td><td>{{if gt .Popularity 0}}{{.Popularity}}{{else}}&times;{{end}}</td></tr>
{{end}}
        </table>
    </body>
</html>

Výsledek aplikace šablony:

<html>
    <head>
        <title>Role ve hře Švestka</title>
    </head>
    <body>
        <h1>Role ve hře Švestka</h1>
        <table>
            <tr><th>Jméno</th><th>Příjmení</th><th>Popularita</th></tr>
 
            <tr><td>Eliška</td><td>Najbrtová</td><td>4</td></tr>
 
            <tr><td>Jenny</td><td>Suk</td><td>3</td></tr>
 
            <tr><td>Anička</td><td>Šafářová</td><td>&times;</td></tr>
 
            <tr><td>Sváťa</td><td>Pulec</td><td>3</td></tr>
 
            <tr><td>Blažej</td><td>Motyčka</td><td>8</td></tr>
 
            <tr><td>Eda</td><td>Wasserfall</td><td>&times;</td></tr>
 
            <tr><td>Přemysl</td><td>Hájek</td><td>10</td></tr>
 
        </table>
    </body>
</html>

Obrázek 2: Vygenerovaná HTML stránka po zobrazení ve webovém prohlížeči (naprosto shodná s prvním HTML stránkou).

6. Problematika XSS a balíčku text/template

V úvodních kapitolách jsme si řekli, že balíček html/template se od balíčku text/template liší především v tom ohledu, že zabraňuje tomu, aby se do výsledné HTML stránky (popř. do CSS souboru) vložily při aplikaci šablony takové údaje, které by mohly vést ke XSS (Cross-site scripting). Ostatně velmi jednoduché XSS si můžeme vytvořit sami. Nepatrně upravíme záznamy, na které bude aplikována šablona. Původní záznamy vypadaly takto:

roles := []Role{
        Role{"Eliška", "Najbrtová", 4},
        Role{"Jenny", "Suk", 3},
        Role{"Anička", "Šafářová", 0},
        Role{"Sváťa", "Pulec", 3},
        Role{"Blažej", "Motyčka", 8},
        Role{"Eda", "Wasserfall", 0},
        Role{"Přemysl", "Hájek", 10},
}

V nové verzi záznamů je v jednom řetězci uložena HTML značka s kódem v JavaScriptu:

roles := []Role{
        Role{"Eliška", "Najbrtová", 4},
        Role{"Jenny", "Suk", 3},
        Role{"Anička", "Šafářová", 0},
        Role{"Sváťa", "Pulec", 3},
        Role{"Blažej", "<script>alert('you have been pwned')</script>", 8},
        Role{"Eda", "Wasserfall", 0},
        Role{"Přemysl", "Hájek", 10},
}

Nejprve použijeme běžnou šablonu aplikovanou přes balíček text/template. Výsledkem bude tato HTML stránka. Povšimněte si, že značka script se stala nedílnou součástí této stránky:

<html>
    <head>
        <title>Role ve hře Švestka</title>
    </head>
    <body>
        <h1>Role ve hře Švestka</h1>
        <table>
            <tr><th>Jméno</th><th>Příjmení</th><th>Popularita</th></tr>
 
            <tr><td>Eliška</td><td>Najbrtová</td><td>4</td></tr>
 
            <tr><td>Jenny</td><td>Suk</td><td>3</td></tr>
 
            <tr><td>Anička</td><td>Šafářová</td><td>&times;</td></tr>
 
            <tr><td>Sváťa</td><td>Pulec</td><td>3</td></tr>
 
            <tr><td>Blažej</td><td><script>alert('you have been pwned')</script></td><td>8</td></tr>
 
            <tr><td>Eda</td><td>Wasserfall</td><td>&times;</td></tr>
 
            <tr><td>Přemysl</td><td>Hájek</td><td>10</td></tr>
 
        </table>
    </body>
</html>

A v důsledku toho se po otevření stránky v prohlížeči zobrazí výsledek činnosti tohoto skriptu:

Obrázek 3: Toto s velkou pravděpodobností není očekávané chování.

Pro úplnost doplníme jak zdrojový kód, tak i použitou šablonu, i když se prakticky neliší od předchozích příkladů:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateFilename = "html_template03.htm"
)
 
// datový typ, jehož prvky budou vypisovány v šabloně
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.ParseFiles(templateFilename))
 
        // tyto hodnoty budou použity při aplikaci šablony
        roles := []Role{
                Role{"Eliška", "Najbrtová", 4},
                Role{"Jenny", "Suk", 3},
                Role{"Anička", "Šafářová", 0},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "<script>alert('you have been pwned')</script>", 8},
                Role{"Eda", "Wasserfall", 0},
                Role{"Přemysl", "Hájek", 10},
        }
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, roles)
        if err != nil {
                panic(err)
        }
}
<html>
    <head>
        <title>Role ve hře Švestka</title>
    </head>
    <body>
        <h1>Role ve hře Švestka</h1>
        <table>
            <tr><th>Jméno</th><th>Příjmení</th><th>Popularita</th></tr>
{{range .}}
            <tr><td>{{.Name}}</td><td>{{.Surname}}</td><td>{{if gt .Popularity 0}}{{.Popularity}}{{else}}&times;{{end}}</td></tr>
{{end}}
        </table>
    </body>
</html>

7. Zabránění XSS aplikací šablony přes balíček html/template

V případě, že namísto aplikace šablony nabízené balíčkem text/template využijeme balíček html/template, bude výše uvedenému XSS zabráněno, protože nyní se šablonovací systém bude snažit porozumět kontextu, ve kterém data do šablony vkládá. Ostatně si to můžeme velmi snadno ověřit:

package main
 
import (
        "html/template"
        "os"
)
 
const (
        templateFilename = "html_template04.htm"
)
 
// datový typ, jehož prvky budou vypisovány v šabloně
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.ParseFiles(templateFilename))
 
        // tyto hodnoty budou použity při aplikaci šablony
        roles := []Role{
                Role{"Eliška", "Najbrtová", 4},
                Role{"Jenny", "Suk", 3},
                Role{"Anička", "Šafářová", 0},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "<script>alert('you have been pwned')</script>", 8},
                Role{"Eda", "Wasserfall", 0},
                Role{"Přemysl", "Hájek", 10},
        }
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, roles)
        if err != nil {
                panic(err)
        }
}

Výsledek bude nyní vypadat následovně:

        Role ve hře Švestka


        Role ve hře Švestka

 

 

 

 

 

 

 

 
Jméno Příjmení Popularita
Eliška Najbrtová 4
Jenny Suk 3
Anička Šafářová &times;
Sváťa Pulec 3
Blažej <script>alert(‚you have been pwned‘)</script> 8
Eda Wasserfall &times;
Přemysl Hájek 10

Nyní již bude zobrazení HTML stránky korektní, resp. přesněji řečeno k×XSS nedošlo:

Obrázek 4: Korektní zobrazení HTML stránky.

8. Kontext, ve kterém se data do HTML stránky vkládají při aplikaci šablony

Při aplikaci šablony balíčkem html/template dokáže šablonovací systém rozeznat kontext, ve kterém se data do HTML stránky vkládají. Například je rozeznáváno, zda mají být data uložena do HTML značek (což jsme si ukázali výše) nebo do odkazů (URL). Týká se to i detektoru XSS. V následujícím demonstračním příkladu je ukázáno, jak se stejné vstupní údaje do výsledné HTML stránky propíšou odlišně, pokud se bude jednat o běžný text nebo o část URL.

Samotný program pro aplikaci šablony na sedm záznamů představujících vstupní data vypadá stejně, jako tomu bylo i v předchozí kapitole:

package main
 
import (
        "html/template"
        "os"
)
 
const (
        templateFilename = "html_template05.htm"
)
 
// datový typ, jehož prvky budou vypisovány v šabloně
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.ParseFiles(templateFilename))
 
        // tyto hodnoty budou použity při aplikaci šablony
        roles := []Role{
                Role{"Eliška", "Najbrtová", 4},
                Role{"Jenny", "Suk", 3},
                Role{"Anička", "Šafářová", 0},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "Motyčka", 8},
                Role{"Eda", "Wasserfall", 0},
                Role{"Přemysl", "Hájek", 10},
        }
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, roles)
        if err != nil {
                panic(err)
        }
}

Odlišná je však samotná šablona. Povšimněte si, že nyní se atributy (položky) Name a Surname používají při konstrukci odkazu a posléze i přímo pro vložení jména postavy do tabulky:

        Role ve hře Švestka


        Role ve hře Švestka

{{range .}}

{{end}}
Jméno Příjmení Popularita
{{.Name}} {{.Surname}} {{if gt .Popularity 0}}{{.Popularity}}{{else}}&ti­mes;{{end}}

Po aplikaci šablony je patrné, že odkazy obsahují jinak zakódovaná jména (+ příjmení), než samotné buňky tabulky. Podobně by se postupovalo i při detekci XSS:

        Role ve hře Švestka


        Role ve hře Švestka

 

 

 

 

 

 

 

 
Jméno Příjmení Popularita
Eliška Najbrtová 4
Jenny Suk 3
Anička Šafářová &times;
Sváťa Pulec 3
Blažej Motyčka 8
Eda Wasserfall &times;
Přemysl Hájek 10

Obrázek 5: Výsledná stránka po svém zobrazení v HTML prohlížeči.

9. HTML šablony a HTTP server realizovaný v jazyce Go

Šablony, jejichž výsledkem má být HTML stránka, lze použít například při generování statických webů. Ovšem mnohem zajímavější je využití šablon ve chvíli, kdy je přímo v jazyce Go implementován HTTP server (a pro tyto účely se jazyk Go používá velmi často). V následujících kapitolách si ukážeme několik příkladů HTTP serverů:

  1. HTTP server, který posílá (vybrané) statické HTML stránky popř. další typy souborů. Tyto stránky mohou vzniknout například aplikací šablon.
  2. HTTP server, který posílá dynamicky vytvořenou HTML stránku na základě šablony. Přitom je šablona vždy (tj. pro každý požadavek) znovu načtena ze souboru, což se hodí zejména při vývoji.
  3. HTTP server, který taktéž posílá dynamicky vytvořenou HTML stránku, ovšem samotná šablona je načtena jedenkrát, konkrétně během inicializace serveru. Ovšem šablona je pochopitelně znovu a znovu aplikována pro každý požadavek. Jedná se sice o jednoduché řešení, ovšem v praxi nemusí vždy vyhovovat.
  4. A konečně HTTP server, který šablonu načte ve chvíli, kdy je změněna. Jedná se sice o nejsložitější řešení, ovšem taktéž nejpraktičtější – šablonu není nutné znovunačítat při každém požadavku od uživatele, ovšem na druhou stranu je (většinou) vhodné reagovat na to, že šablonu někdo modifikuje.

10. Ukázka jednoduchého serveru, který poskytuje pouze statické stránky

Programovací jazyk Go obsahuje podporu pro tvorbu HTTP serverů přímo ve standardní knihovně, konkrétně v balíčku net/http. Vytvoření skutečného a plnohodnotného serveru je v tomto případě otázkou několika řádků zdrojového kódu. Základem je funkce HandleFunc, 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 /:

http.HandleFunc("/", mainEndpoint)

Hlavička funkce HandleFunc přitom vypadá následovně:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

Povšimněte si, že druhým parametrem této funkce je jiná funkce (onen handler) s hlavičkou:

func MujHandler(ResponseWriter, *Request)

Konkrétně může implementace našeho handleru poslat na výstup (typu ResponseWriter) jednoduchý text, který bude zaslán klientovi v celé HTTP odpovědi (s hlavičkami, stavovým kódem, délkou atd. atd.):

func mainEndpoint(writer http.ResponseWriter, request *http.Request) {
        io.WriteString(writer, "Hello world!\n")
}

Popř. můžeme klientovi poslat stránku načtenou ze souboru (zde prozatím velmi jednoduše – bez vyrovnávací paměti):

func sendStaticPage(writer http.ResponseWriter, filename string) {
        log.Printf("Sending static file %s", filename)
 
        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)
        }
}

Existuje i snadnější možnost kombinující handler, ovšem upravený tak, aby přímo předávat statický obsah:

func filesEndpoint(writer http.ResponseWriter, request *http.Request) {
        url := request.URL.Path[len("/files/"):]
        println("Serving file from URL: " + url)
        http.ServeFile(writer, request, url)
}

Pro čistě statické soubory je ovšem výhodnější použít http.FileServer a odstranit tak značnou část předchozího kódu:

fileServer := http.FileServer(http.Dir("./www"))
http.Handle("/resources/", http.StripPrefix("/resources", fileServer))

Následně již stačí server spustit na určeném portu:

http.ListenAndServe(":8000", nil)

Úplná implementace jednoduchého HTTP serveru může vypadat takto:

package main
 
import (
        "fmt"
        "io/ioutil"
        "log"
        "net/http"
)
 
func sendStaticPage(writer http.ResponseWriter, filename string) {
        log.Printf("Sending static file %s", filename)
 
        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() {
        const address = ":8080"
 
        log.Printf("Starting server on address %s", address)
 
        http.HandleFunc("/", staticPage("index.html"))
        http.HandleFunc("/missing", staticPage("missing.html"))
        http.ListenAndServe(address, nil)
}

Obrázek 6: Otestování HTTP serveru v prohlížeči.

11. HTTP server a aplikace HTML šablon

Nyní již můžeme spojit znalosti o šablonovacím systému se znalostmi o tvorbě HTTP serverů. V nové variantě HTTP serveru zaregistrujeme handler vyvolaný při přístupu na koncový bod /roles:

http.HandleFunc("/roles", rolesHandler("html_template05.htm", roles))

Můžeme vidět, že tento handler bude volán s uvedením jména souboru se šablonou a současně i s daty, které se mají šabloně předat. Handler tedy provede načtení šablony, aplikaci šablony a výsledek (což bude obsah dynamicky generované HTML stránky) pošle uživateli:

func rolesHandler(templateFilename string, roles []Role) func(writer http.ResponseWriter, request *http.Request) {
        return func(writer http.ResponseWriter, request *http.Request) {
                log.Printf("Constructing template from file %s", templateFilename)
 
                // vytvoření nové šablony
                tmpl, err := template.ParseFiles(templateFilename)
                if err != nil {
                        writer.WriteHeader(http.StatusInternalServerError)
                        log.Printf("Template can't be constructed: %v", err)
                        return
                }
 
                log.Printf("Application template for %d data records", len(roles))
                // aplikace šablony - přepis hodnot
                err = tmpl.Execute(writer, roles)
                if err != nil {
                        writer.WriteHeader(http.StatusInternalServerError)
                        log.Printf("Error executing template: %v", err)
                        return
                }
        }
}
Poznámka: šablona je načtena pro každý požadavek (request) od klienta, což má své výhody (vždy se použije poslední verze šablony), ale i nevýhody (neustálé mnohdy zbytečné načítání). Řešení si ukážeme v dalším textu. Dalším nedostatkem je, že se v jediné funkci rolesHandler provádí tři operace, které by bylo vhodnější rozdělit – případný refaktoring popř. implementaci nějaké formy MVC ponechám na laskavém čtenáři.

Úplný zdrojový kód takto upraveného HTTP serveru vypadá následovně:

package main
 
import (
        "fmt"
        "html/template"
        "io/ioutil"
        "log"
        "net/http"
)
 
// datový typ, jehož prvky budou vypisovány v šabloně
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
func sendStaticPage(writer http.ResponseWriter, filename string) {
        log.Printf("Sending static file %s", filename)
 
        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 rolesHandler(templateFilename string, roles []Role) func(writer http.ResponseWriter, request *http.Request) {
        return func(writer http.ResponseWriter, request *http.Request) {
                log.Printf("Constructing template from file %s", templateFilename)
 
                // vytvoření nové šablony
                tmpl, err := template.ParseFiles(templateFilename)
                if err != nil {
                        writer.WriteHeader(http.StatusInternalServerError)
                        log.Printf("Template can't be constructed: %v", err)
                        return
                }
 
                log.Printf("Application template for %d data records", len(roles))
                // aplikace šablony - přepis hodnot
                err = tmpl.Execute(writer, roles)
                if err != nil {
                        writer.WriteHeader(http.StatusInternalServerError)
                        log.Printf("Error executing template: %v", err)
                        return
                }
        }
}
 
func main() {
        // tyto hodnoty budou použity při aplikaci šablony
        roles := []Role{
                Role{"Eliška", "Najbrtová", 4},
                Role{"Jenny", "Suk", 3},
                Role{"Anička", "Šafářová", 0},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "Motyčka", 8},
                Role{"Eda", "Wasserfall", 0},
                Role{"Přemysl", "Hájek", 10},
        }
 
        const address = ":8080"
 
        log.Printf("Starting server on address %s", address)
 
        http.HandleFunc("/", staticPage("index.html"))
        http.HandleFunc("/missing", staticPage("missing.html"))
        http.HandleFunc("/roles", rolesHandler("html_template05.htm", roles))
        http.ListenAndServe(address, nil)
}

12. Benchmark: rychlost načtení a aplikace šablon

Varianta HTTP serveru ukázaná v předchozí kapitole načítala a aplikovala šablonu v každém požadavku. Bylo by tedy dobré vědět, jak rychlé jsou vlastně tyto dvě operace a jaké odezvy popř. propustnost (throughput) můžeme od takto navrženého serveru očekávat. Přiblížení k reálným číslům nám může dát benchmark naprogramovaný s využitím standardního balíčku testing. Nejprve vytvoříme hlavní modul s funkcemi, které budeme v benchmarku spouštět; konkrétně s funkcemi pro načtení šablony a pro její aplikaci:

package main
 
import (
        "bytes"
        "fmt"
        "html/template"
)
 
const (
        templateFilename = "html_template05.htm"
)
 
// datový typ, jehož prvky budou vypisovány v šabloně
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
func readTemplate(templateFilename string) *template.Template {
        // vytvoření nové šablony
        return template.Must(template.ParseFiles(templateFilename))
}
 
func applyTemplate(tmpl *template.Template, roles []Role) int {
        buffer := new(bytes.Buffer)
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(buffer, roles)
        if err != nil {
                panic(err)
        }
 
        return buffer.Len()
}

Tyto dvě funkce budeme spouštět z benchmarku. Pro zajímavost si otestujeme i rychlost provádění obou zmíněných operací. Samotný benchmark může vypadat následovně:

package main
 
import (
        "testing"
)
 
func BenchmarkReadTemplate(b *testing.B) {
        for i := 0; i < b.N; i++ {
                tmpl := readTemplate("./html_template05.htm")
                if tmpl == nil {
                        b.Fatal("Template was not created")
                }
        }
}
 
func BenchmarkApplyTemplate(b *testing.B) {
        // tyto hodnoty budou použity při aplikaci šablony
        roles := []Role{
                Role{"Eliška", "Najbrtová", 4},
                Role{"Jenny", "Suk", 3},
                Role{"Anička", "Šafářová", 0},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "Motyčka", 8},
                Role{"Eda", "Wasserfall", 0},
                Role{"Přemysl", "Hájek", 10},
        }
 
        // načtení šablony (jen jedenkrát)
        tmpl := readTemplate("./html_template05.htm")
 
        // samotný benchmark
        for i := 0; i < b.N; i++ {
                length := applyTemplate(tmpl, roles)
                if length <= 0 {
                        b.Fatal("Don't work")
                }
        }
}
 
func BenchmarkReadAndApplyTemplate(b *testing.B) {
        // tyto hodnoty budou použity při aplikaci šablony
        roles := []Role{
                Role{"Eliška", "Najbrtová", 4},
                Role{"Jenny", "Suk", 3},
                Role{"Anička", "Šafářová", 0},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "Motyčka", 8},
                Role{"Eda", "Wasserfall", 0},
                Role{"Přemysl", "Hájek", 10},
        }
 
        // samotný benchmark
        for i := 0; i < b.N; i++ {
                tmpl := readTemplate("./html_template05.htm")
                if tmpl == nil {
                        b.Fatal("Template was not created")
                }
 
                length := applyTemplate(tmpl, roles)
                if length <= 0 {
                        b.Fatal("Don't work")
                }
        }
}

Benchmark spustíme tímto příkazem:

$ go test -bench=.

Výsledky dosažené na notebooku s mikroprocesorem i7:

goos: linux
goarch: amd64
pkg: template
cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
BenchmarkReadTemplate-8                    44815             26499 ns/op
BenchmarkApplyTemplate-8                   22891             53443 ns/op
BenchmarkReadAndApplyTemplate-8            10000            122381 ns/op
PASS
ok      template        4.451s
Poznámka: z výsledků je mj. patrné, že načtení a na něj navazující aplikace šablony se podle očekávání vykoná pomaleji než aplikace již dopředu načtené šablony. Teoreticky lze provést přibližně 8100 těchto operací za sekundu, ovšem nutno podotknout, že se jedná o relativně malé šablony i malé množství dat (záznamů).

13. Vytvoření šablon pouze při inicializaci HTTP serveru

V případě, že šablony nebudou měněny často, lze čas vyřizování požadavků zkrátit, a to takovým způsobem, že se šablony načtou již při inicializaci serveru. Poté již bude docházet pouze k jejich aplikaci. V případě, že se šablona změní (tedy pokud někdo modifikuje její soubor), nebude tato změna HTTP serverem vůbec reflektována.

Ve zdrojovém kódu je nutné změnit handler rolesHandler, a to takovým způsobem, že se mu namísto jména souboru se šablonou předá přímo již načtená šablona:

package main
 
import (
        "fmt"
        "html/template"
        "io/ioutil"
        "log"
        "net/http"
)
 
// datový typ, jehož prvky budou vypisovány v šabloně
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
func sendStaticPage(writer http.ResponseWriter, filename string) {
        log.Printf("Sending static file %s", filename)
 
        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 rolesHandler(tmpl *template.Template, roles []Role) func(writer http.ResponseWriter, request *http.Request) {
        return func(writer http.ResponseWriter, request *http.Request) {
 
                log.Printf("Application template for %d data records", len(roles))
                // aplikace šablony - přepis hodnot
                err := tmpl.Execute(writer, roles)
                if err != nil {
                        writer.WriteHeader(http.StatusInternalServerError)
                        log.Printf("Error executing template: %v", err)
                        return
                }
        }
}
 
func main() {
        const templateFilename = "html_template05.htm"
 
        // tyto hodnoty budou použity při aplikaci šablony
        roles := []Role{
                Role{"Eliška", "Najbrtová", 4},
                Role{"Jenny", "Suk", 3},
                Role{"Anička", "Šafářová", 0},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "Motyčka", 8},
                Role{"Eda", "Wasserfall", 0},
                Role{"Přemysl", "Hájek", 10},
        }
 
        log.Printf("Constructing template from file %s", templateFilename)
 
        // vytvoření nové šablony
        tmpl, err := template.ParseFiles(templateFilename)
        if err != nil {
                log.Fatalf("Template can't be constructed: %v", err)
                return
        }
 
        const address = ":8080"
 
        log.Printf("Starting server on address %s", address)
 
        http.HandleFunc("/", staticPage("index.html"))
        http.HandleFunc("/missing", staticPage("missing.html"))
        http.HandleFunc("/roles", rolesHandler(tmpl, roles))
        http.ListenAndServe(address, nil)
}

14. Detekce modifikace šablon

Prozatím jsme si ukázali dvě varianty HTTP serverů. V první variantě byla šablona načítána pro každý požadavek (a tudíž se reflektovaly všechny změny, ovšem na úkor neustále prováděných načítání); ve variantě druhé se pak šablona načetla při inicializaci serveru a její následné úpravy vůbec nebyly reflektovány. V praxi by však bylo vhodné oba přístupy nějakým způsobem zkombinovat, konkrétně dosáhnout toho, aby změny šablon byly reflektovány, ale aby šablony nebylo nutné načítat při každém zpracování požadavku. Řešení pochopitelně existuje a je založeno na sledování změn souborů se šablonou. Teoreticky by bylo možné využít standardní balíček os s funkcí Stat a sledovat čas modifikace souborů, ovšem výhodnější bude využít možnosti nabízené jádrem systému – to totiž umožňuje sledovat změny provedené ve sledovaných souborech (kde soubor je v tomto případě na Linuxu identifikován i-uzlem).

Tato funkcionalita je implementovaná v knihovně fsnotify/fsnotify, kterou využijeme v dalších dvou příkladech. Nejdříve je nutné vytvořit projekt, který tuto knihovnu využívá, takže po příkazu go mod init provedeme úpravu tohoto souboru:

module fsnotify-test
 
go 1.17
 
require (
        github.com/fsnotify/fsnotify v1.5.1 // indirect
        golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
)

15. Příklad detekce změn v souborech

Knihovna fsnotify/fsnotify je založena na sledování změn v souboru, který je identifikován svým i-uzlem. To ovšem znamená, že není možné sledovat přímo konkrétní soubor se šablonou, protože textové editory při ukládání typicky původní soubor přejmenují a změny uloží do souboru nového. Sledovat je tedy nutné změny v adresáři, v němž se soubor nachází. Zajímat nás budou zápisy do tohoto speciálního souboru (adresáře jsou z pohledu systému taktéž soubory). Pokud k zápisu dojde, zjistíme jméno změněného souboru přímo z události, kterou knihovna fsnotify/fsnotify vytvoří:

select {
case event, ok := <-watcher.Events:
        if !ok {
                return
        }
        log.Println("event:", event)
        if event.Op&fsnotify.Write == fsnotify.Write {
                log.Println("modified file:", event.Name)
                if filename == event.Name {
                        log.Println("Template change detected")
                }
        }
case err, ok := <-watcher.Errors:
        if !ok {
                return
        }
        log.Println("error:", err)
}
Poznámka: pokud by se sledovaly změny konkrétního souboru, vše by zdánlivě fungovalo až do druhé změny. Tehdy totiž (typicky) již nový soubor má odlišný i-uzel – opět doporučuji si vše vyzkoušet.

Program, který bude sledovat změny souboru html_template06.htm může vypadat následovně:

package main
 
import (
        "log"
 
        "github.com/fsnotify/fsnotify"
)
 
const templateFilename = "./html_template06.htm"
 
func startWatcher(directory string, filename string) {
        log.Print("Starting watcher")
 
        watcher, err := fsnotify.NewWatcher()
        if err != nil {
                log.Fatal(err)
        }
        defer watcher.Close()
 
        log.Printf("Watching directory %s", directory)
 
        err = watcher.Add(directory)
        if err != nil {
                log.Fatal(err)
        }
 
        for {
                select {
                case event, ok := <-watcher.Events:
                        if !ok {
                                return
                        }
                        log.Println("event:", event)
                        if event.Op&fsnotify.Write == fsnotify.Write {
                                log.Println("modified file:", event.Name)
                                if filename == event.Name {
                                        log.Println("Template change detected")
                                }
                        }
                case err, ok := <-watcher.Errors:
                        if !ok {
                                return
                        }
                        log.Println("error:", err)
                }
        }
}
 
func main() {
        startWatcher(".", templateFilename)
}

16. HTTP server detekující změny šablon

Knihovnu fsnotify/fsnotify můžeme integrovat do HTTP serveru a zajistit tak, aby se šablony načetly až ve chvíli, kdy skutečně dojde k jejich změně. Samotný detektor změn bude spuštěn ve vlastní gorutině a informace o změněných souborech bude posílat do kanálu changed:

changed := make(chan string)
go startWatcher(".", templateFilename, changed)

Samotná implementace gorutiny vychází z příkladu uvedeného výše. Přidán je pouze kanál, do něhož jsou posílána jména změněných souborů:

func startWatcher(directory string, filename string, changed chan string) {
        log.Print("Starting watcher")
 
        watcher, err := fsnotify.NewWatcher()
        if err != nil {
                log.Fatal(err)
        }
        defer watcher.Close()
 
        log.Printf("Watching directory %s", directory)
 
        err = watcher.Add(directory)
        if err != nil {
                log.Fatal(err)
        }
 
        for {
                select {
                case event, ok := <-watcher.Events:
                        if !ok {
                                return
                        }
                        log.Println("event:", event)
                        if event.Op&fsnotify.Write == fsnotify.Write {
                                log.Println("modified file:", event.Name)
                                if filename == event.Name {
                                        log.Println("Template change detected")
                                        changed <- filename
                                }
                        }
                case err, ok := <-watcher.Errors:
                        if !ok {
                                return
                        }
                        log.Println("error:", err)
                }
        }
}

Změnit se ovšem musí i samotný handler, který nejprve otestuje, jestli došlo k modifikaci šablony a pokud ano, tuto šablonu načte (nekontroluje se však jméno souboru – tuto část resp. podmínku je však triviální přidat):

func rolesHandler(tmpl *template.Template, roles []Role, changed chan string) func(writer http.ResponseWriter, request *http.Request) {
        return func(writer http.ResponseWriter, request *http.Request) {
                select {
                case filename := <-changed:
                        log.Printf("Have to reload template from file %s", filename)
                        tmpl = loadTemplate(filename)
                default:
                        log.Print("Using old template")
                }
 
                log.Printf("Application template for %d data records", len(roles))
                // aplikace šablony - přepis hodnot
                err := tmpl.Execute(writer, roles)
                if err != nil {
                        writer.WriteHeader(http.StatusInternalServerError)
                        log.Printf("Error executing template: %v", err)
                        return
                }
        }
}

17. Úplný zdrojový kód serveru

Úplný zdrojový kód serveru popsaného v předchozí kapitole bude vypadat následovně:

bitcoin_skoleni

package main
 
import (
        "fmt"
        "html/template"
        "io/ioutil"
        "log"
        "net/http"
 
        "github.com/fsnotify/fsnotify"
)
 
const templateFilename = "./html_template06.htm"
 
// datový typ, jehož prvky budou vypisovány v šabloně
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
func sendStaticPage(writer http.ResponseWriter, filename string) {
        log.Printf("Sending static file %s", filename)
 
        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 rolesHandler(tmpl *template.Template, roles []Role, changed chan string) func(writer http.ResponseWriter, request *http.Request) {
        return func(writer http.ResponseWriter, request *http.Request) {
                select {
                case filename := <-changed:
                        log.Printf("Have to reload template from file %s", filename)
                        tmpl = loadTemplate(filename)
                default:
                        log.Print("Using old template")
                }
 
                log.Printf("Application template for %d data records", len(roles))
                // aplikace šablony - přepis hodnot
                err := tmpl.Execute(writer, roles)
                if err != nil {
                        writer.WriteHeader(http.StatusInternalServerError)
                        log.Printf("Error executing template: %v", err)
                        return
                }
        }
}
 
func loadTemplate(templateFilename string) *template.Template {
        log.Printf("Constructing template from file %s", templateFilename)
 
        // vytvoření nové šablony
        tmpl, err := template.ParseFiles(templateFilename)
        if err != nil {
                log.Fatalf("Template can't be constructed: %v", err)
                return nil
        }
 
        return tmpl
}
 
func startWatcher(directory string, filename string, changed chan string) {
        log.Print("Starting watcher")
 
        watcher, err := fsnotify.NewWatcher()
        if err != nil {
                log.Fatal(err)
        }
        defer watcher.Close()
 
        log.Printf("Watching directory %s", directory)
 
        err = watcher.Add(directory)
        if err != nil {
                log.Fatal(err)
        }
 
        for {
                select {
                case event, ok := <-watcher.Events:
                        if !ok {
                                return
                        }
                        log.Println("event:", event)
                        if event.Op&fsnotify.Write == fsnotify.Write {
                                log.Println("modified file:", event.Name)
                                if filename == event.Name {
                                        log.Println("Template change detected")
                                        changed <- filename
                                }
                        }
                case err, ok := <-watcher.Errors:
                        if !ok {
                                return
                        }
                        log.Println("error:", err)
                }
        }
}
 
func main() {
        // tyto hodnoty budou použity při aplikaci šablony
        roles := []Role{
                Role{"Eliška", "Najbrtová", 4},
                Role{"Jenny", "Suk", 3},
                Role{"Anička", "Šafářová", 0},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "Motyčka", 8},
                Role{"Eda", "Wasserfall", 0},
                Role{"Přemysl", "Hájek", 10},
        }
 
        changed := make(chan string)
        go startWatcher(".", templateFilename, changed)
 
        tmpl := loadTemplate(templateFilename)
 
        const address = ":8080"
 
        log.Printf("Starting server on address %s", address)
 
        http.HandleFunc("/", staticPage("index.html"))
        http.HandleFunc("/missing", staticPage("missing.html"))
        http.HandleFunc("/roles", rolesHandler(tmpl, roles, changed))
        http.ListenAndServe(address, nil)
}

18. Závěr

Většinou zjistíte, že neustálé načítání šablon aplikaci nijak zásadně nezpomaluje, přesněji řečeno že onen pověstný bottleneck se nachází v jiné části HTTP serveru. Popř. si infrastruktura vyžádá, že změny šablon vůbec nemohou být provedeny bez „otočení“ uzlu s HTTP serverem. Nicméně i přesto může být užitečné tuto část HTTP serveru (resp. celé služby) sledovat a například exportovat formou metrik pro Prometheus.

19. Repositář s demonstračními příklady

Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do nového 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ě stovku kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:

# Příklad/soubor Stručný popis Cesta
1 text_template01.go aplikace textové šablony na pole/řez záznamů https://github.com/tisnik/go-root/blob/master/article87/tex­t_template01.go
2 text_template01.txt textová šablona použitá v tomto demonstračním příkladu https://github.com/tisnik/go-root/blob/master/article87/tex­t_template01.txt
       
3 html_template01.go aplikace HTML šablony balíčkem text/template https://github.com/tisnik/go-root/blob/master/article87/html_tem­plate01.go
4 html_template01.htm HTML šablona použitá v tomto demonstračním příkladu https://github.com/tisnik/go-root/blob/master/article87/html_tem­plate01.htm
       
5 html_template02.go aplikace HTML šablony balíčkem html/template https://github.com/tisnik/go-root/blob/master/article87/html_tem­plate02.go
6 html_template02.htm HTML šablona použitá v tomto demonstračním příkladu https://github.com/tisnik/go-root/blob/master/article87/html_tem­plate02.htm
       
7 html_template03.go aplikace HTML šablony s daty obsahujícími přípravu pro XSS https://github.com/tisnik/go-root/blob/master/article87/html_tem­plate03.go
8 html_template03.htm HTML šablona použitá v tomto demonstračním příkladu https://github.com/tisnik/go-root/blob/master/article87/html_tem­plate03.htm
       
9 html_template04.go aplikace HTML šablony s daty obsahujícími přípravu pro XSS https://github.com/tisnik/go-root/blob/master/article87/html_tem­plate04.go
10 html_template04.htm HTML šablona použitá v tomto demonstračním příkladu https://github.com/tisnik/go-root/blob/master/article87/html_tem­plate04.htm
       
11 html_template05.go vkládání dat do HTML stránky v různém kontextu https://github.com/tisnik/go-root/blob/master/article87/html_tem­plate05.go
12 html_template05.htm HTML šablona použitá v tomto demonstračním příkladu https://github.com/tisnik/go-root/blob/master/article87/html_tem­plate05.htm
       
13 static_pages.go HTTP server se statickými stránkami https://github.com/tisnik/go-root/blob/master/article87/sta­tic_pages.go
14 template_pages.go HTTP server aplikující šablony na předaná data https://github.com/tisnik/go-root/blob/master/article87/tem­plate_pages.go
15 template_pages2.go HTTP server aplikující šablony na předaná data, cache šablon https://github.com/tisnik/go-root/blob/master/article87/tem­plate_pages2.go
       
16 fsnotify/ detekce změny souboru s využitím knihovny fsnotify/fsnotify https://github.com/tisnik/go-root/blob/master/article87/fsnotify/
       
17 template_pages3/ implementace HTTP serveru, který dokáže automaticky načíst upravené šablony https://github.com/tisnik/go-root/blob/master/article87/tem­plate_pages3/
       
18 benchmark/ benchmark měřící rychlost načítání šablon i aplikace šablon https://github.com/tisnik/go-root/blob/master/article87/ben­chmark/

20. Odkazy na Internetu

  1. Dokumentace ke knihovně fsnotify
    https://pkg.go.dev/github­.com/fsnotify/fsnotify
  2. Cross-site scripting
    https://en.wikipedia.org/wiki/Cross-site_scripting
  3. 5 Real-World Cross Site Scripting Examples
    https://websitesecuritysto­re.com/blog/real-world-cross-site-scripting-examples/
  4. Cross Site Scripting (XSS) Attack Tutorial with Examples, Types & Prevention
    https://www.softwaretestin­ghelp.com/cross-site-scripting-xss-attack-test/
  5. Dokumentace ke standardní knihovně jazyka Go
    https://pkg.go.dev/std
  6. Awesome Go
    https://awesome-go.com/
  7. Template Engines for Go
    https://awesome-go.com/#template-engines
  8. Mail merge
    https://en.wikipedia.org/wi­ki/Mail_merge
  9. Template processor
    https://en.wikipedia.org/wi­ki/Template_processor
  10. Text/template
    https://pkg.go.dev/text/template
  11. Go Template Engines
    https://go.libhunt.com/categories/556-template-engines
  12. Template Engines
    https://reposhub.com/go/template-engines
  13. GoLang Templating Made Easy
    https://awkwardferny.medium.com/golang-templating-made-easy-4d69d663c558
  14. Templates in GoLang
    https://golangdocs.com/templates-in-golang
  15. What are the best template engines for Go apart from „html/template“?
    https://www.quora.com/What-are-the-best-template-engines-for-Go-apart-from-html-template?share=1
  16. Ace – HTML template engine for Go
    https://github.com/yosssi/ace
  17. amber
    https://github.com/eknkc/amber
  18. quicktemplate
    https://github.com/valyala/qu­icktemplate
  19. Šablonovací systém ace
    https://github.com/yosssi/ace
  20. Šablonovací systém amber
    https://github.com/eknkc/amber
  21. Šablonovací systém damsel
    https://github.com/dskinner/damsel
  22. Šablonovací systém ego
    https://github.com/benbjohnson/ego
  23. Šablonovací systém extemplate
    https://github.com/dannyvan­kooten/extemplate
  24. Šablonovací systém fasttemplate
    https://github.com/valyala/fas­ttemplate
  25. Šablonovací systém gofpdf
    https://github.com/jung-kurt/gofpdf
  26. Šablonovací systém gospin
    https://github.com/m1/gospin
  27. Šablonovací systém goview
    https://github.com/foolin/goview

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.