Hlavní navigace

Standardní šablonovací systém jazyka Go

7. 12. 2021
Doba čtení: 24 minut

Sdílet

 Autor: Go lang
Dnes se seznámíme se standardním šablonovacím systémem tohoto jazyka, který je představován knihovnou text/template. Jedná se o až překvapivě výkonný a taktéž rozšiřitelný šablonovací systém.

Obsah

1. Standardní šablonovací systém jazyka Go

2. Konstrukce šablony

3. Aplikace šablony obsahující pouze konstantní text

4. Aplikace šablony metodou ExecuteTemplate

5. Získání řetězce s aplikovanou šablonou

6. Konstrukce šablony s automatickou kontrolou chyb – template.Must

7. Předání dat, které se v šabloně použijí

8. Vícenásobné použití vstupních dat v šabloně

9. Využití složitější datové struktury v šabloně

10. Pokus o předání nekompatibilní datové struktury s odlišnými jmény prvků

11. Předání a využití struktury obsahující textové položky

12. Vícenásobné použití šablony pro různá vstupní data

13. Pokus o přístup k privátním prvkům datové struktury

14. Iterace přes prvky pole či řezu přímo v šabloně – konstrukce {{range}}

15. Praktická ukázka iterace přes prvky pole či řezu přímo v šabloně

16. Šablona uložená v samostatném souboru – problém s pojmenováním šablony

17. Korektní způsob načtení a použití šablony uložené v samostatném souboru

18. Přímé volání konstruktoru ParseFiles

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

20. Odkazy na Internetu

1. Standardní šablonovací systém jazyka Go

V praxi se velmi často setkáme s požadavkem na tvorbu různých dokumentů, tiskových sestav, výstupních protokolů atd. Všechny tyto typy výstupů většinou mají společnou vlastnost – jsou založeny na nějakém dokumentu s předpřipraveným formátem (například se může jednat o předpis, jak má vypadat objednávka, faktura, protokol s výsledkem měření atd.), do kterého se na k tomu určená místa dosazují hodnoty připravené programem – tedy hodnoty nějakým způsobem dopočítané, hodnoty přečtené z databáze apod. A právě k těmto účelům slouží šablony (template) (což jsou ony předpřipravené dokumenty) a šablonovací systémy, které umožňují propojit šablonu s reálnými daty a vytvořit tak výsledný dokument.

Obrázek 1: Princip činnosti šablonovacího systému.

První šablonovací systémy byly vyvinuty jako součást zařízení určených pro přípravu, editaci a tisk textů. Jednalo se o specializované počítače; typickým příkladem jsou stroje společnosti Wang. Do vytvářených dokumentů bylo možné přidávat i řídicí kódy, které například umožňovaly vkládání obrázků (což nás dnes nezajímá) či byly určeny k takzvanému mail merge, což je systém použitelný například pro tisk obálek, vysvědčení, obchodních dopisů atd. (stejná šablona, odlišný obsah). A právě postupným zobecňováním funkce mail merge vznikly obecnější šablonovací systémy.

*

Obrázek 2: Wang 1220 je jedním z prvních modelů specializovaných zařízení firmy Wang Laboratories určených pro přípravu a editaci textů, jejich ukládání na magnetické pásky (jednotka pro magnetické pásky je umístěná napravo) a dokonce i pro provádění jednoduchého vyplňování formulářů z databáze (mail-merge).

Šablonovací systémy existují pro všechny mainstreamové programovací jazyky. Příkladem může být Python s několika desítkami knihoven. I pro jazyk Go existuje velké množství podobně koncipovaných nástrojů. Dnes se však budeme zabývat nástrojem, který je součástí standardní knihovny jazyka Go. Konkrétně se jedná o balíček nazvaný text/template, jenž je až překvapivě výkonný a přitom rozšiřitelný. Existuje i varianta tohoto balíčku nazvaná html/template, tou se však dnes zabývat nebudeme.

*

Obrázek 3: Na pravé straně klávesnice zařízení Wang 1220 byly umístěny klávesy pro ovládání textového procesoru.

Poznámka: dnes si ukážeme pouze základní způsoby použití balíčku text/template. Některé další možnosti budou zmíněny až v navazujícím článku.
Poznámka: se šablonovacím systémem jazyka Go jsme se již krátce setkali v článku Vývoj síťových aplikací v programovacím jazyku Go (pokračování), v němž jsme si ovšem ani zdaleka neukázali všechny možnosti, které standardní šablonovací systém vývojářům nabízí.

2. Konstrukce šablony

Podívejme se nejdříve na to, jakým způsobem se vytvoří (zkonstruuje) objekt, který představuje šablonu. Posléze budeme tuto šablonu aplikovat, ovšem vzhledem k tomu, že text šablony obsahuje pouze neměnný text, bude výsledkem aplikace šablony původní text. Konstrukce objektu, který představuje šablonu, je snadná. Nejprve zavoláme konstruktor template.New kterému předáme název šablony a následně metodu Template.Parse, které předáme řetězec se šablonou:

// vytvoření nové šablony
tmpl, err := template.New(templateName).Parse(templateFormat)

Vidíme, že musíme znát dvě hodnoty, a to konkrétně jméno šablony a vlastní text (obsah) šablony. V obou případech se jedná o řetězce, takže:

const (
        templateName   = "test"
        templateFormat = "Toto je testovací šablona"
)

Metoda Parse může skončit s chybou, takže je navíc nutné provést kontrolu, zda se zpracování podařilo či nikoli:

if err != nil {
        panic(err)
}

3. Aplikace šablony obsahující pouze konstantní text

Dalším krokem je aplikace šablony, tj. operace, při níž je šablonovacímu systému předána jak již zkonstruovaná šablona, tak i data, která se mají „propsat“ do výsledného textu. Tato aplikace je provedena metodou Template.Execute. Této metodě se předávají dva parametry, a to konkrétně objekt typu io.Writer, do kterého bude výsledný text zapisován a (libovolná) datová struktura, jejíž obsah se má propsat do výsledného textu. Podívejme se nyní na dnešní první demonstrační příklad, v němž je tato operace provedena. Výstup je proveden do os.Stdout, tedy na standardní výstup (terminál):

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateName   = "test"
        templateFormat = "Toto je testovací šablona"
)
 
func main() {
        // vytvoření nové šablony
        tmpl, err := template.New(templateName).Parse(templateFormat)
        if err != nil {
                panic(err)
        }
 
        // aplikace šablony - přepis hodnot + výpis výsledku
        err = tmpl.Execute(os.Stdout, nil)
        if err != nil {
                panic(err)
        }
}

Výsledkem by měl být text, který odpovídá konstantě předávané při konstrukci šablony:

Toto je testovací šablona
Poznámka: tím jsme si mj. ověřili, že v šablonách je možné používat znaky Unicode – což by ovšem v případě programovacího jazyka Go nemělo být nic překvapivého.

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

4. Aplikace šablony metodou ExecuteTemplate

V úvodním demonstračním příkladu byla aplikace šablony provedena metodou Execute, které se předával pouze objekt typu io.Writer a data propisovaná do šablony. Existuje ovšem ještě jedna podobně pojmenovaná metoda – ExecuteTemplate. Této metodě se navíc předává i jméno šablony; volání tedy musí vypadat následovně:

err = tmpl.ExecuteTemplate(os.Stdout, templateName, nil)
if err != nil {
        panic(err)
}

Můžete se ptát, proč vlastně tato metoda existuje. Ve skutečnosti je možné, což si ostatně ukážeme v dalších kapitolách, jediným zavoláním metody ParseFiles načíst a zpracovat větší množství šablon. Každé takové šabloně je potom přiřazeno jméno odvozené od templateName a jména souboru se šablonou. A právě toto jméno lze použít pro rozlišení toho, jakou šablonu v daný okamžik použít.

Poznámka: nejedná se o příliš intuitivní způsob, ovšem pokud se pracuje s jedinou šablonou, můžeme tento koncept ingorovat.

Ve druhém demonstračním příkladu je použit právě tento způsob aplikace šablony:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateName   = "test"
        templateFormat = "Toto je testovací šablona"
)
 
func main() {
        // vytvoření nové šablony
        tmpl, err := template.New(templateName).Parse(templateFormat)
        if err != nil {
                panic(err)
        }
 
        // aplikace šablony - přepis hodnot + výpis výsledku
        err = tmpl.ExecuteTemplate(os.Stdout, templateName, nil)
        if err != nil {
                panic(err)
        }
}

Výsledek po překladu a spuštění:

Toto je testovací šablona

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

5. Získání řetězce s aplikovanou šablonou

Při pohledu na první dva demonstrační příklady jste si mohli všimnout, že výsledný text získaný aplikací šablony není vrácen ve formě běžného řetězce. Výstup je totiž proveden do objektu typu io.Writer, což je nám již velmi dobře známé rozhraní s jedinou metodou Write. Důvod pro toto chování je jednoduchý – šablony totiž mohou obsahovat smyčky a mohou zpracovávat velmi rozsáhlá data (tabulky). Nebylo by tedy optimální vytvářet neustále rostoucí text (což je paměťově i výkonově náročná operace). Namísto toho se části aplikované šablony postupně posílají na výstup právě zmíněnou metodou Write.

V případě, že skutečně vyžadujeme, aby byl výsledkem aplikace šablony běžný řetězec, pomůžeme si pomocným objektem – bufferem. Do bufferu si necháme vypsat výsledek aplikace šablony a následně z bufferu text získáme, což je v Go poměrně idiomatický přístup:

// buffer pro uložení výsledků aplikace šablony
buffer := new(bytes.Buffer)
 
// aplikace šablony - přepis hodnot
err = tmpl.Execute(buffer, nil)
...
...
...
 
// výpis výsledného textu
fmt.Println(buffer.String())

Celý postup je ukázán na následujícím demonstračním příkladu:

package main
 
import (
        "bytes"
        "fmt"
        "text/template"
)
 
const (
        templateName   = "test"
        templateFormat = "Toto je testovací šablona"
)
 
func main() {
        // vytvoření nové šablony
        tmpl, err := template.New(templateName).Parse(templateFormat)
        if err != nil {
                panic(err)
        }
 
        // buffer pro uložení výsledků aplikace šablony
        buffer := new(bytes.Buffer)
 
        // aplikace šablony - přepis hodnot
        err = tmpl.Execute(buffer, nil)
        if err != nil {
                panic(err)
        }
 
        // výpis výsledného textu
        fmt.Println(buffer.String())
}

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

6. Konstrukce šablony s automatickou kontrolou chyb – template.Must

V předchozí kapitole jsme se zmínili o jednom z idiomů, který je v jazyku Go používán. Dalším idiomem je náhrada explicitní kontroly výsledku nějaké operace, přesněji řečeno kontroly, zda operace proběhla v pořádku, za volání nějaké formy funkce Must. V našem konkrétním případě je možné nahradit volání konstruktoru New následovaného voláním metody Parse a kontrolou chyby, tedy tento programový kód:

// vytvoření nové šablony
tmpl, err := template.New(templateName).Parse(templateFormat)
if err != nil {
        panic(err)
}

Za tento kód:

// vytvoření nové šablony
tmpl := template.Must(template.New(templateName).Parse(templateFormat))

V případě chyby bude druhé volání taktéž volat funkci panic.

Úprava prvního demonstračního příkladu tak, aby tuto techniku používal, je triviální:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateName   = "test"
        templateFormat = "Toto je testovací šablona"
)
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.New(templateName).Parse(templateFormat))
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, nil)
        if err != nil {
                panic(err)
        }
}

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

7. Předání dat, které se v šabloně použijí

Po zahřívacích kolech se konečně zaměřme na praktické využití šablon. Ukážeme si, jak je možné do šablony předat data, která se následně při aplikaci šablony použijí. Nejprve se podívejme, jak bude vypadat nová šablona:

templateFormat = "Hello {{.}}"

V této šabloně můžeme vidět použití znaků se speciálním významem – což jsou dvojice složených závorek „{{“ a „}}“. Text, který se nachází mezi těmito oddělovači, je zpracováván odlišně od běžného textu. V našem konkrétním případě je uvnitř oddělovačů použita pouze tečka, což je znak, který reprezentuje vstupní data. V tomto konkrétním případě je celá část šablony {{.}} nahrazena textovou podobou předaných dat. V tom nejjednodušším případě se bude jednat o řetězec:

err := tmpl.Execute(os.Stdout, "world")
if err != nil {
        panic(err)
}

Výsledkem aplikace dat na šablonu tedy bude výsledek:

Hello world

Pro úplnost si ukažme, jak vypadá celý skript, který takto popsanou aplikaci dat na šablonu provede:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateName   = "test"
        templateFormat = "Hello {{.}}"
)
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.New(templateName).Parse(templateFormat))
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, "world")
        if err != nil {
                panic(err)
        }
}

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

8. Vícenásobné použití vstupních dat v šabloně

Data, která jsou předána do šablony, je možné použít vícekrát (což se v praxi často děje). Podívejme se na velmi jednoduchou modifikaci předchozího demonstračního příkladu:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateName   = "test"
        templateFormat = "The {{.}} language is often referred to as Golang, but the proper name is '{{.}}'."
)
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.New(templateName).Parse(templateFormat))
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, "Go")
        if err != nil {
                panic(err)
        }
}

Po spuštění tohoto demonstračního příkladu by se měl vypsat následující text:

The Go language is often referred to as Golang, but the proper name is 'Go'.

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

9. Využití složitější datové struktury v šabloně

V předchozí dvojici demonstračních příkladů jsme v šabloně použili předaná data na jediném místě (popř. opakovaně), ale vždy vcelku. V praxi je však nutné například vyplnit adresu, tabulku s nakoupeným zbožím, výslednou cenu atd. – tedy využít v daném místě šablony jen určitou část předaných dat. Tento problém se v Go řeší tak, že se do šablony předává datová struktura (záznam) a v samotné šabloně se použije „tečková notace“ pro přístup k jednotlivým prvkům šablony.

Předávaná struktura může vypadat například takto:

type Expression struct {
        X int
        Y int
        Z int
}

V šabloně lze využít jednotlivé prvky této struktury:

templateFormat = "Součet {{.X}} + {{.Y}} = {{.Z}}"

Předání dat do šablony je triviální:

// aplikace šablony - přepis hodnot
err := tmpl.Execute(os.Stdout, expression)
if err != nil {
        panic(err)
}

Výsledný skript bude vypadat následovně:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateName   = "test"
        templateFormat = "Součet {{.X}} + {{.Y}} = {{.Z}}"
)
 
type Expression struct {
        X int
        Y int
        Z int
}
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.New(templateName).Parse(templateFormat))
 
        // tyto hodnoty budou použity při aplikaci šablony
        expression := Expression{
                X: 10,
                Y: 20,
                Z: 30,
        }
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, expression)
        if err != nil {
                panic(err)
        }
}

Výsledek po spuštění:

Součet 10 + 20 = 30

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

10. Pokus o předání nekompatibilní datové struktury s odlišnými jmény prvků

V případě, že se do šablony budeme snažit předat nekompatibilní strukturu s odlišnými jmény prvků, dojde k běhové (runtime) chybě. Jinými slovy – chyba je odhalena až po spuštění příkladu, nikoli při jeho překladu. Ostatně si to můžeme vyzkoušet sami překladem a spuštěním dalšího demonstračního příkladu:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateName   = "test"
        templateFormat = "Součet {{.X}} + {{.Y}} = {{.Z}}"
)
 
type User struct {
        FirstName string
        Surname   string
        Born      string
}
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.New(templateName).Parse(templateFormat))
 
        // tyto hodnoty budou použity při aplikaci šablony
        user := User{
                FirstName: "Jára",
                Surname:   "Cimrman",
                Born:      "Böhmen",
        }
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, user)
        if err != nil {
                panic(err)
        }
}

Chyba, která se vypíše po spuštění tohoto příkladu:

Součet panic: template: test:1:10: executing "test" at <.X>: can't evaluate field X in type main.User
 
goroutine 1 [running]:
main.main()
        /home/ptisnovs/src/go-root/article_79/template06.go:33 +0x2a8

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

Poznámka: z tohoto chování je patrné, že program má přístup k informacím o datové struktuře i v době běhu programu – což nebývá u překládaných programovacích jazyků vždy typické.

11. Předání a využití struktury obsahující textové položky

Velmi často se setkáme s tím, že se do šablony předávají textové položky. Práce s nimi je (prozatím) prakticky stejná, jako tomu bylo s celočíselnými položkami, přičemž rozdíly se budeme zabývat příště. V dalším demonstračním příkladu zkontrolujeme, zda je možné v textových položkách používat Unicode – což je v Go dodrženo:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateName   = "test"
        templateFormat = "Uživatel {{.FirstName}} {{.Surname}} born in {{.Born}}"
)
 
type User struct {
        FirstName string
        Surname   string
        Born      string
}
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.New(templateName).Parse(templateFormat))
 
        // tyto hodnoty budou použity při aplikaci šablony
        user := User{
                FirstName: "Jára",
                Surname:   "Cimrman",
                Born:      "Böhmen",
        }
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, user)
        if err != nil {
                panic(err)
        }
}

Výsledek:

Uživatel Jára Cimrman born in Böhmen

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

12. Vícenásobné použití šablony pro různá vstupní data

Šablonu je pochopitelně možné použít vícekrát – a ostatně právě z důvodu znovupoužitelnosti je nutné vytvářet objekt (datovou strukturu), která šablonu představuje. Pokud již totiž je šablona předpřipravena (ve formě objektu), je její vícenásobné použití rychlejší, než neustálé vytváření šablon pro použití jednorázová. V následujícím demonstračním příkladu je ukázáno, jakým způsobem můžeme šablonu aplikovat na větší množství struktur, které jsou uloženy v řezu a postupně zpracovávány v programové smyčce typu for-each:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateName   = "test"
        templateFormat = `Jméno {{.Name}} {{.Surname}}
Popularita {{.Popularity}}
 
`
)
 
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.New(templateName).Parse(templateFormat))
 
        // 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á", 1},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "Motyčka", 8},
                Role{"Eda", "Wasserfall", 3},
                Role{"Přemysl", "Hájek", 10},
        }
 
        // aplikace šablony - přepis hodnot
        for _, role := range roles {
                err := tmpl.Execute(os.Stdout, role)
                if err != nil {
                        panic(err)
                }
        }
}

Po spuštění tohoto demonstračního příkladu by se na standardní výstup měly vypsat následující řádky:

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

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

13. Pokus o přístup k privátním prvkům datové struktury

Jak je v programovacím jazyku Go zvykem, je možné „zvenku“ (tj. nikoli z metody) přistupovat pouze k veřejným (viditelným) prvkům datové struktury a nikoli k prvkům privátním. Rozlišení veřejný/privátní je provedeno na základě prvního znaku prvku datové struktury. Pokud je tento znak verzálkou, bude prvek veřejný, pokud minuskou, pak privátní (přitom se ovšem nemusí jednat o znak z ASCII!).

Můžeme se pochopitelně pokusit použít strukturu s privátními prvky:

type Role struct {
        name       string
        surname    string
        popularity int
}

A v šabloně realizovat přístup k těmto prvkům:

        templateFormat = `Jméno {{.name}} {{.surname}}
Popularita {{.popularity}}
 
`

Ovšem v čase běhu (runtime) dojde k běhové chybě:

$ ./template11
 
Jméno panic: template: test:1:9: executing "test" at <.name>: name is an unexported field of struct type main.Role
 
goroutine 1 [running]:
main.main()
        /home/ptisnovs/src/go-root/article_79/template11.go:41 +0x2dc
Poznámka: opět se tedy jedná o chybu odhalenou až při spuštění programu a nikoli již v době překladu (což by pochopitelně bylo mnohem lepší).

Pro úplnost si ukažme celý příklad, který po spuštění skončí s chybou:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateName   = "test"
        templateFormat = `Jméno {{.name}} {{.surname}}
Popularita {{.popularity}}
 
`
)
 
type Role struct {
        name       string
        surname    string
        popularity int
}
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.New(templateName).Parse(templateFormat))
 
        // 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á", 1},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "Motyčka", 8},
                Role{"Eda", "Wasserfall", 3},
                Role{"Přemysl", "Hájek", 10},
        }
 
        // aplikace šablony - přepis hodnot
        for _, role := range roles {
                err := tmpl.Execute(os.Stdout, role)
                if err != nil {
                        panic(err)
                }
        }
}

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

14. Iterace přes prvky pole či řezu přímo v šabloně – konstrukce {{range}}

Nyní se již dostáváme k mnohem zajímavějším vlastnostem standardní šablonovací knihovny programovacího jazyka Go. Často se totiž dostaneme do situace, kdy je například nutné vytvořit kusovník, fakturu s rozpisem zboží/služeb, protokol s tabulkami s výsledky atd. – vždy se tedy jedná o opakující se údaje se stejným formátem. Již ve dvanácté kapitole jsme si ukázali jedno z možných řešení – použití klasické programové smyčky typu for-each:

// aplikace šablony - přepis hodnot
for _, role := range roles {
        err := tmpl.Execute(os.Stdout, role)
        if err != nil {
                panic(err)
        }
}

Toto řešení však ani zdaleka není ideální, protože nás nutí si rozdělit šablonu do více částí, které se budou aplikovat samostatně. Existuje však i alternativní způsob – deklarovat přímo v šabloně místo, které se má opakovat pro všechny prvky získané ze vstupních dat. Tato možnost skutečně existuje a je založena na použití značek „{{range selektor}}“ a „{{end}}“. Selektorem je myšleno určení opakujících se prvků ve vstupních datech; prozatím zde využijeme tečku (dot). Vše, co je použito mezi značkami „{{range}}“ a „{{end}}“ bude opakováno tolikrát, kolik prvků je nalezeno ve vstupních datech. Přístup k hodnotám těchto prvků je opět proveden s využitím nám již dobře známé tečkové notace, tedy například:

{{range .}}Jméno {{.Name}} {{.Surname}} Popularita {{.Popularity}}
{{end}}

Tato šablona předpokládá, že vstupem bude pole či řez hodnot typu:

struct {
        Name       string
        Surname    string
        Popularity int
}

15. Praktická ukázka iterace přes prvky pole či řezu přímo v šabloně

Povídejme se nyní na praktickou ukázku iterace (opakování) provedeného přes prvky pole či řezu, přičemž toto opakování je předepsáno přímo v šabloně:

const (
        templateFormat = `{{range .}}Jméno {{.Name}} {{.Surname}} Popularita {{.Popularity}}
{{end}}`
)
Poznámka: připomeňme si, že v řetězci zapsaném do mezi dvojici zpětných apostrofů `` lze použít i prázdné řádky atd.

Takto definovaná šablona je použita v následujícím demonstračním příkladu:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateName   = "test"
        templateFormat = `{{range .}}Jméno {{.Name}} {{.Surname}} Popularita {{.Popularity}}
{{end}}`
)
 
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.New(templateName).Parse(templateFormat))
 
        // 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á", 1},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "Motyčka", 8},
                Role{"Eda", "Wasserfall", 3},
                Role{"Přemysl", "Hájek", 10},
        }
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, roles)
        if err != nil {
                panic(err)
        }
}
Poznámka: povšimněte si, že se ve zdrojovém textu tohoto demonstračního příkladu nepoužívá žádná forma programové smyčky.

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

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

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

16. Šablona uložená v samostatném souboru – problém s pojmenováním šablony

Již při pohledu na zdrojový kód předchozího demonstračního příkladu, především na samotný obsah šablony je zřejmé, že použití řetězcových literálů nemusí být tím nejlepším způsobem, jak šablonu definovat a udržovat:

const (
        templateFormat = `{{range .}}Jméno {{.Name}} {{.Surname}} Popularita {{.Popularity}}
{{end}}`
)

Mnohem praktičtější by bylo, aby šablona byla uložena v samostatném textovém souboru, který by mohl mít například tento obsah:

{{range .}}Jméno {{.Name}} {{.Surname}}
Popularita {{.Popularity}}
---
{{end}}

I to je pochopitelně možné, protože šablonu lze načíst z externího souboru metodou Template.ParseFiles. Tato metoda slouží k načtení (obecně) většího množství šablon, které se od sebe odlišují svým jménem, které je určeno (mimo jiné) i jménem souboru obsahujícího šablonu. V případě, že jméno šablony nebude nalezeno, nahlásí se chyba, což je ostatně ukázáno v dalším příkladu:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateName     = "test"
        templateFilename = "template13.txt"
)
 
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.New(templateName).ParseFiles(templateFilename))
 
        println(tmpl)
 
        // 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á", 1},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "Motyčka", 8},
                Role{"Eda", "Wasserfall", 3},
                Role{"Přemysl", "Hájek", 10},
        }
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, roles)
        if err != nil {
                panic(err)
        }
}

Při pokusu o spuštění tohoto příkladu (tedy v runtime) dojde k následující chybě:

panic: template: test: "test" is an incomplete or empty template
 
goroutine 1 [running]:
main.main()
        /home/ptisnovs/src/go-root/article_79/template13.go:37 +0x272
exit status 2

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

17. Korektní způsob načtení a použití šablony uložené v samostatném souboru

Nyní si ukažme korektní způsob načtení a použití šablony uložené v samostatném souboru. Šablonu budeme stále, stejně jako v předchozí kapitole, načítat metodou Template.ParseFiles, které předáme jméno souboru obsahujícího šablonu. Jméno šablony bude v tomto příkladu totožné se jménem tohoto souboru, tedy:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateName     = "template14.txt"
        templateFilename = "template14.txt"
)
 
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
func main() {
        // vytvoření nové šablony
        tmpl := template.Must(template.New(templateName).ParseFiles(templateFilename))
 
        println(tmpl)
 
        // 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á", 1},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "Motyčka", 8},
                Role{"Eda", "Wasserfall", 3},
                Role{"Přemysl", "Hájek", 10},
        }
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, roles)
        if err != nil {
                panic(err)
        }
}

Po spuštění tohoto demonstračního příkladu by se na standardní výstup měly vypsat následující řádky:

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

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

18. Přímé volání konstruktoru ParseFiles

Ve všech předchozích demonstračních příkladech se objekt typu Template vytvářel konstruktorem template.New. První volanou metodou pak byla Parse, popř. ParseFiles:

// vytvoření nové šablony
tmpl := template.Must(template.New(templateName).ParseFiles(templateFilename))
...
...
...
// aplikace šablony - přepis hodnot
err := tmpl.Execute(os.Stdout, roles)

Toto řešení je sice univerzální a umožňuje načtení většího množství šablon, na druhou stranu však vyžaduje práci se jménem šablony. V případě, že budeme chtít pracovat s jedinou šablonou a to navíc bez nutnosti uvádění jejího jména, lze použít určitou zkratku – namísto konstruktoru template.New přímo zavolat funkci template.ParseFiles (což je sice stejné jméno, jako u metody Template.ParseFiles, ale skutečně se jedná o „běžnou“ funkci). Kód se tak nepatrně zjednoduší a především – nebude nutné řešit další jméno, tj. potenciálně další stav aplikace:

// vytvoření nové šablony
tmpl := template.Must(template.ParseFiles(templateFilename))
...
...
...
// aplikace šablony - přepis hodnot
err := tmpl.Execute(os.Stdout, roles)

Úplný zdrojový kód tohoto demonstračního příkladu bude vypadat následovně:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateFilename = "template15.txt"
)
 
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á", 1},
                Role{"Sváťa", "Pulec", 3},
                Role{"Blažej", "Motyčka", 8},
                Role{"Eda", "Wasserfall", 3},
                Role{"Přemysl", "Hájek", 10},
        }
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, roles)
        if err != nil {
                panic(err)
        }
}

Opět se podívejme na výsledek získaný po spuštění tohoto příkladu:

Hacking tip

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

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

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 template01.go vytvoření a aplikace šablony obsahující pouze statický text, kontrola chyby při Parse https://github.com/tisnik/go-root/blob/master/article79/tem­plate01.go
2 template02.go zavolání metody ExecuteTemplate namísto Execute https://github.com/tisnik/go-root/blob/master/article79/tem­plate02.go
3 template03.go zápis výsledného textu do bufferu převedeného na řetězec přes buffer https://github.com/tisnik/go-root/blob/master/article79/tem­plate03.go
4 template04.go konstrukce šablony pomocí template.Must s automatickou kontrolou chyby https://github.com/tisnik/go-root/blob/master/article79/tem­plate04.go
5 template05.go skutečná šablona produkující text na základě předaných dat – jednoduchý text https://github.com/tisnik/go-root/blob/master/article79/tem­plate05.go
6 template06.go vícenásobné použití vstupních dat v šabloně https://github.com/tisnik/go-root/blob/master/article79/tem­plate06.go
7 template07.go skutečná šablona produkující text na základě předaných dat, předání datové struktury https://github.com/tisnik/go-root/blob/master/article79/tem­plate07.go
8 template08.go šablona, na kterou se aplikuje nekompatibilní datová struktura https://github.com/tisnik/go-root/blob/master/article79/tem­plate08.go
9 template09.go textová data, kontrola korektního použití Unicode https://github.com/tisnik/go-root/blob/master/article79/tem­plate09.go
10 template10.go postupná aplikace šablony na data uložená v řezu https://github.com/tisnik/go-root/blob/master/article79/tem­plate10.go
11 template11.go pokus o přístup k prvkům šablony, které jsou privátní https://github.com/tisnik/go-root/blob/master/article79/tem­plate11.go
12 template12.go opakování (range) v šabloně a práce s poli https://github.com/tisnik/go-root/blob/master/article79/tem­plate12.go
13 template13.go šablona uložená v souboru – problém s pojmenováním šablony https://github.com/tisnik/go-root/blob/master/article79/tem­plate13.go
14 template14.go šablona uložená v souboru – korektní příklad https://github.com/tisnik/go-root/blob/master/article79/tem­plate14.go
15 template15.go šablona uložená v souboru – korektní příklad, přímé volání ParseFiles https://github.com/tisnik/go-root/blob/master/article79/tem­plate15.go

20. Odkazy na Internetu

  1. Mail merge
    https://en.wikipedia.org/wi­ki/Mail_merge
  2. Template processor
    https://en.wikipedia.org/wi­ki/Template_processor
  3. Text/template
    https://pkg.go.dev/text/template
  4. Go Template Engines
    https://go.libhunt.com/categories/556-template-engines
  5. Template Engines
    https://reposhub.com/go/template-engines
  6. GoLang Templating Made Easy
    https://awkwardferny.medium.com/golang-templating-made-easy-4d69d663c558
  7. Templates in GoLang
    https://golangdocs.com/templates-in-golang
  8. 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
  9. Ace – HTML template engine for Go
    https://github.com/yosssi/ace
  10. amber
    https://github.com/eknkc/amber
  11. quicktemplate
    https://github.com/valyala/qu­icktemplate