Hlavní navigace

Standardní šablonovací systém jazyka Go (dokončení)

14. 12. 2021
Doba čtení: 25 minut

Sdílet

 Autor: Go lang
V dnešním článku dokončíme popis standardního šablonovacího systému jazyka Go, který je představován knihovnou text/template. Jedná se o až překvapivě výkonný a taktéž rozšiřitelný systém umožňující například volání uživatelských funkcí.

Obsah

1. Standardní šablonovací systém programovacího jazyka Go (dokončení)

2. Naformátování hodnot funkcí fmt.Sprintf volanou přímo ze šablony

3. Podmínky v šablonách

4. Praktické otestování podmínky v šabloně

5. Plná podmínka typu if-then-else v šabloně

6. Volání metod z šablony

7. Pokus o přístup k privátní metodě z šablony

8. Kolony (pipeline) v šabloně

9. Blok with a proměnné v šablonách

10. Definice pojmenované šablony

11. Předání funkce do šablony, vyvolání funkce v šabloně

12. Kombinace předchozích možností – blok with a předání i použití funkcí v šabloně

13. Dvourozměrná pole v šablonách

14. Vytištění tabulky malé násobilky

15. Vylepšení předchozího demonstračního příkladu

16. Přístup k poli, které je uloženo jako prvek datové struktury

17. Závěr

18. Další šablonovací systémy dostupné pro jazyk Go

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

20. Odkazy na Internetu

1. Standardní šablonovací systém programovacího jazyka Go (dokončení)

V závěru úvodního článku o standardním šablonovacím systému programovacího jazyka Go jsme si ukázali, že přímo v šabloně je možné definovat oblast, která se má opakovat pro všechny prvky získané ze vstupních dat. Tato v praxi velmi užitečná funkcionalita je založena na použití značek „{{range selektor}}“ a „{{end}}“. Selektorem je přitom myšleno určení opakujících se prvků ve vstupních datech. Prozatím zde využijeme tečku (dot), ale v navazujících kapitolách si ukážeme, že lze iterovat i přes všechny hodnoty vybraného prvku apod. Vše, co je zapsáno mezi značkami „{{range}}“ a „{{end}}“ (tedy jak běžný text, tak i značky šablony), 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 byla použita v následujícím demonstračním příkladu, s nímž jsme se taktéž seznámili již minule:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateFilename = "template15.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á", 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)
        }
}

S výsledkem:

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 demonstračního příkladu z této kapitoly je dostupný na adrese https://github.com/tisnik/go-root/blob/master/article79/tem­plate15.go.

2. Naformátování hodnot funkcí fmt.Sprintf volanou přímo ze šablony

V mnoha případech by se hodilo hodnoty, které se vkládají do šablony, nějakým způsobem naformátovat, což se týká například numerických hodnot, řetězců s různou délkou vstupu apod. Autoři šablonovacích systémů přistupují k tomuto problému z různých stran, většinou přidáním dalších znaků se speciálním významem do doménově specifického šablonovacího jazyka. V případě standardního šablonovacího systému programovacího jazyka Go je tomu ovšem jinak, protože pro naformátování se většinou používá standardní funkce fmt.Printf přesněji řečeno resp. fmt.Sprintf. Volání této funkce v šabloně se však provádí bez závorek okolo parametrů, například následovně:

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

To například znamená, že následující část šablony:

{{printf "%-15s" .Name}}

Vlastně odpovídá volání:

fmt.Sprintf("%-15s", item.Name)

V případě, že výše uvedenou šablonu použijeme v demonstračním příkladu, bude výstup naformátován tímto způsobem:

--------------------------------------------------------------------
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 demonstračního příkladu z této kapitoly je dostupný na adrese https://github.com/tisnik/go-root/blob/master/article80/tem­plate16.go.

Poznámka: ve skutečnosti je možné takto jednoduše volat jen několik málo funkcí, jejichž jmenné aliasy jsou vypsány v následující tabulce:
Jméno v šabloně Volaná funkce
print fmt.Sprint
printf fmt.Sprintf
println fmt.Sprintln
urlquery přeformátování řetězce tak, aby ho bylo možné použít v URL

3. Podmínky v šablonách

Mnohdy se taktéž setkáme s nutností použít v šablonách podmínku, což znamená, že určitá část textu bude ve výsledku použita pouze při splnění nějaké podmínky (a popř. jiná při nesplnění té samé podmínky). Příkladem může být například šablona, která je součástí dokumentace/nápovědy ke standardnímu šablonovacímu systému jazyka Go:

Dear {{.Name}},
{{if .Attended}}
It was a pleasure to see you at the wedding.
{{else}}
It is a shame you couldn't make it to the wedding.
{{end}}
{{end}}
Best wishes,
Josie

V podmínce se mohou volat funkce, které odpovídají standardním relačním operátorům:

Funkce Odpovídá výrazu
eq arg1 == arg2
ne arg1 != arg2
lt arg1 < arg2
le arg1 <= arg2
gt arg1 > arg2
ge arg1 >= arg2
Poznámka: důležité je vědět, které hodnoty se vyhodnotí jako pravda a které jako nepravda. Za nepravdu jsou považovány hodnoty 0, false, nil a taktéž řetězce, pole, řezy a mapy s nulovou délkou resp. nulovým počtem prvků (což je odlišné od standardní sémantiky jazyka Go!). Ostatní hodnoty jsou považovány za pravdivé.

4. Praktické otestování podmínky v šabloně

Podívejme se nyní na praktický způsob použití šablony s podmínkou. Budeme rozlišovat, zda je popularita role v Cimrmanovských hrách známá či nikoli. Použijeme přitom explicitní test, zda je zapsaná hodnota ostře větší než nula:

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

Zdrojový kód demonstračního příkladu se změní jen nepatrně – pozměníme hodnoty prvků Popularity:

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

5. Plná podmínka typu if-then-else v šabloně

V šabloně lze dále použít úplnou podmínku, tj. určit, který text se má přidat do výsledku ve chvíli, kdy nějaká podmínka je splněna a který text se má naopak přidat v případě nesplnění podmínky. V praxi to může vypadat například následovně – rozlišíme, zda je popularita role známá či neznámá:

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

Výsledek běhu předchozího demonstračního příkladu ve chvíli, kdy se mu předá upravená šablona s plnou podmínkou typu if-then-else:

--------------------------------------------------------------------
Jméno Eliška          Najbrtová       Popularita   4
Jméno Jenny           Suk             Popularita   3
Jméno Anička          Šafářová        Popularita  neznámá
Jméno Sváťa           Pulec           Popularita   3
Jméno Blažej          Motyčka         Popularita   8
Jméno Eda             Wasserfall      Popularita  neznámá
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­plate18.go.

6. Volání metod z šablony

Připomeňme si, že data, která jsou předána ve chvíli, kdy je nutné šablonu aplikovat, jsou z šablony přístupná přes „tečku“ a že je možné přistupovat k atributům či prvkům vstupní datové struktury. V případě polí, řezů či map lze tedy použít iteraci přes všechny prvky:

--------------------------------------------------------------------
{{range .}}...
{{end}}
--------------------------------------------------------------------

Uvnitř této (de facto) smyčky se k prvkům a jejich atributům opět přistupuje přes tečku:

--------------------------------------------------------------------
{{range .}}Jméno {{printf "%-15s" .Name}} {{printf "%-15s" .Surname}}
{{end}}
--------------------------------------------------------------------

Můžeme však zavolat i metodu definovanou pro danou strukturu, a to následovně:

--------------------------------------------------------------------
{{range .}}Jméno {{printf "%-15s" .Name}} {{printf "%-15s" .Surname}} Popularita {{.GetPopularity}}
{{end}}
--------------------------------------------------------------------
Poznámka: v tomto případě se předpokládá, že metoda je bez parametrů a vrací přímo hodnotu, kterou je možné přepsat do šablony – typicky tedy řetězec.

Podívejme se nyní na příklad použití v případě, že pro nám již známou datovou strukturu Role definujeme metodu nazvanou GetPopularity, která je bez parametrů a vrací řetězec:

package main
 
import (
        "fmt"
        "os"
        "text/template"
)
 
const (
        templateFilename = "template19.txt"
)
 
// datový typ, jehož prvky budou vypisovány v šabloně
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
func (role Role) GetPopularity() string {
        if role.Popularity <= 0 {
                return "Nezadáno"
        } else {
                return fmt.Sprintf("%d", role.Popularity)
        }
}
 
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 aplikace této šablony 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á        Popularita Nezadáno
Jméno Sváťa           Pulec           Popularita 3
Jméno Blažej          Motyčka         Popularita 8
Jméno Eda             Wasserfall      Popularita Nezadáno
Jméno Přemysl         Hájek           Popularita 10
 
--------------------------------------------------------------------
Poznámka: vlastně jsme tak přenesli podmínku z šablony do programového kódu, což může být v některých případech lepší řešení.

Ú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­plate19.go.

7. Pokus o přístup k privátní metodě z šablony

Minule jsme si kromě dalších informací řekli i to, že v šabloně není možné přistupovat k privátním atributům, resp. k prvkům datové struktury, tj. k takovým položkám, jejichž jména začínají malým písmenem. Totéž ovšem platí i pro metody – pokud je metoda privátní, tj. když její jméno začíná malým písmenem, nebude možné takovou metodu přímo z šablony zavolat.

Toto chování si můžeme snadno otestovat změnou šablony:

--------------------------------------------------------------------
{{range .}}Jméno {{printf "%-15s" .Name}} {{printf "%-15s" .Surname}} Popularita {{.getPopularity}}
{{end}}
--------------------------------------------------------------------

A nepatrnou modifikací zdrojového kódu demonstračního příkladu:

package main
 
import (
        "fmt"
        "os"
        "text/template"
)
 
const (
        templateFilename = "template20.txt"
)
 
// datový typ, jehož prvky budou vypisovány v šabloně
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
func (role Role) getPopularity() string {
        if role.Popularity <= 0 {
                return "Nezadáno"
        } else {
                return fmt.Sprintf("%d", role.Popularity)
        }
}
 
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)
        }
}

Tento problém nebude zjištěn při překladu (compile time), ale až po spuštění programu (runtime):

--------------------------------------------------------------------
Jméno Eliška          Najbrtová       Popularita panic: template: template20.txt:2:84: executing "template20.txt" at <.getPopularity>: can't evaluate field getPopularity in type main.Role
 
goroutine 1 [running]:
main.main()
        /home/ptisnovs/temp/y/template20.go:46 +0xeb
exit status 2

Ú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­plate20.go.

8. Kolony (pipeline) v šabloně

Ve chvíli, kdy je nutné nějakým způsobem zpracovat určitou datovou položku větším množstvím funkcí, lze využít další velmi zajímavou vlastnost šablonovacího systému programovacího jazyka Go – pro postupné zpracovávání dat je totiž možné definovat kolony (pipeline), které se způsobem zápisu a vlastně i svým chováním podobají klasickým Unixovým kolonám. Kolony se zapisují znakem „|“ a umožňují výsledek jedné operace převést do operace další (což je typicky volání nějaké funkce).

Podívejme se nyní na to, jak může vypadat jednoduchá kolona, v níž hodnotu získanou metodou Role.GetPopularily() (která vrací řetězec) necháme naformátovat funkcí fmt.Sprintf:

.GetPopularity | printf "%10s"
Poznámka: povšimněte si, že návratová hodnota metody je předána druhé funkci jako poslední parametr.

Výše uvedenou kolonu lze snadno zakomponovat do šablony, a to takto:

--------------------------------------------------------------------
{{range .}}Jméno {{printf "%-15s" .Name}} {{printf "%-15s" .Surname}} Popularita {{.GetPopularity | printf "%10s"}}
{{end}}
--------------------------------------------------------------------

Výsledek aplikace šablony na vstupní data, včetně naformátování posledního sloupce funkcí fmt.Sprintf:

--------------------------------------------------------------------
Jméno Eliška          Najbrtová       Popularita          4
Jméno Jenny           Suk             Popularita          3
Jméno Anička          Šafářová        Popularita   Nezadáno
Jméno Sváťa           Pulec           Popularita          3
Jméno Blažej          Motyčka         Popularita          8
Jméno Eda             Wasserfall      Popularita   Nezadáno
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­plate21.go.

9. Blok with a proměnné v šablonách

Další zajímavou a pro složitější šablony i užitečnou vlastností je podpora pro definici bloků v šabloně, přičemž v rámci bloku je možné použít (a to přímo v šabloně) proměnné. Blok začíná značkou obsahující slovo with a končí značkou {{end}}, tedy například následovně:

{{with ...}}
...
...
...
{{end}}

V bloku with lze definovat proměnné, například jim přiřadit hodnotu nějakého datového prvku, hodnotu výsledku volání metody atd.:

{{with $x := .VolanáMetoda ...}}
...
...
...
{{end}}

Podívejme se nyní, jak lze upravit předchozí příklad, v němž byla použita kolona (pipeline). Úprava bude spočívat v tom, že hodnotu vrácenou metodou Role.GetPopularity uložíme do proměnné x platné v rámci bloku a posléze ji předáme funkci printf (tedy ve skutečnosti funkci fmt.Sprintf). Výsledná šablona bude vypadat takto:

--------------------------------------------------------------------
{{range .}}Jméno {{printf "%-15s" .Name}} {{printf "%-15s" .Surname}} Popularita {{with $x := .GetPopularity}}{{printf "%10s" $x}}{{end}}
{{end}}
--------------------------------------------------------------------

Výsledek aplikace této šablony by měl být totožný s předchozím demonstračním příkladem, o čemž se ostatně můžeme velmi snadno přesvědčit:

--------------------------------------------------------------------
Jméno Eliška          Najbrtová       Popularita          4
Jméno Jenny           Suk             Popularita          3
Jméno Anička          Šafářová        Popularita   Nezadáno
Jméno Sváťa           Pulec           Popularita          3
Jméno Blažej          Motyčka         Popularita          8
Jméno Eda             Wasserfall      Popularita   Nezadáno
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­plate22.go.

Alternativně je možné šablonu napsat s využitím kolony v bloku, tedy následovně:

--------------------------------------------------------------------
{{range .}}Jméno {{printf "%-15s" .Name}} {{printf "%-15s" .Surname}} Popularita {{with $x := .GetPopularity}}{{$x | printf "%10s"}}{{end}}
{{end}}
--------------------------------------------------------------------

Ú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­plate23.go.

10. Definice pojmenované šablony

Další vlastností standardního šablonovacího systému programovacího jazyka Go je podpora pro definici pojmenované šablony, kterou je posléze možné použít v jiné šabloně. Pojmenovaná šablona se vytváří s využitím značky block; nutné je přitom uvést jméno šablony i celou kolonu určující, jaká data se budou zpracovávat. Přitom každé vyvolání šablony může teoreticky pracovat s různými daty:

{{block "jméno šablony" kolona/pipeline}} libovolný obsah {{end}}

Tento zápis se rozloží na definici šablony a na její zavolání:

{{define "jméno šablony"}} libovolný obsah {{end}}
{{template "jméno šablony" kolona/pipeline}}

Ukažme si nyní základní způsob použití pojmenované šablony v nepatrně upraveném demonstračním příkladu:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateValue = `Roles:{{block "roles" .}}{{"\n"}}{{range .}}{{println "-" .Name "\t" .Surname}}{{end}}{{end}}`
)
 
// 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.New("template").Parse(templateValue))
 
        // 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 po spuštění je nepatrně odlišný od předchozích příkladů:

Roles:
- Eliška         Najbrtová
- Jenny          Suk
- Anička         Šafářová
- Sváťa          Pulec
- Blažej         Motyčka
- Eda    Wasserfall
- Přemysl        Hájek
Poznámka: pro úplnost by bylo vhodné naformátovat první sloupec na předem zadaný počet znaků.

Ú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­plate24.go.

11. Předání funkce do šablony, vyvolání funkce v šabloně

Představme si, že je požadováno, aby se ve výsledném dokumentu tvořeném šablonou vypsala všechna jména předaná v řezu:

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

Přitom by se jména měla vypsat na jediný řádek a oddělena by měla být čárkou (navíc by šablona měla být funkční i pro prázdný vstup):

Names: Eliška Najbrtová, Jenny Suk, Anička Šafářová, Sváťa Pulec, Blažej Motyčka, Eda Wasserfall, Přemysl Hájek

Samozřejmě je možné použít značku range a vhodným způsobem zařídit, aby se za posledním prvkem již čárka nevypisovala, ovšem existuje i jednodušší a především hotové a ověřené řešení – použít funkci strings.join, která je určena přesně pro provedení této operace. Chceme být tedy schopni zavolat funkci strings.join stejně, jako již umíme volat funkci fmt.Sprintf:

templateValue = `Names: {{join . ", "}}`

I to je ve standardním šablonovacím systému jazyka Go možné, protože při konstrukci šablony je možné předat mapu (libovolných) funkcí, včetně jejich jmen – ty se mohou lišit od skutečných jmen funkcí, protože jména v šablonách jsou vyhodnocována v době běhu (runtime) a nikoli v době překladu:

// mapa funkcí použitých v šabloně
funcs := template.FuncMap{"join": strings.Join}

Předání jmen funkcí při konstrukci šablony:

tmpl := template.Must(template.New("template").Funcs(funcs).Parse(templateValue))

Samotná šablona se nyní zredukuje na volání příslušné funkce s předáním dat:

templateValue = `Names: {{join . ", "}}`

Pro úplnost si ukažme úplný zdrojový kód takto upraveného demonstračního příkladu:

package main
 
import (
        "os"
        "strings"
        "text/template"
)
 
const (
        templateValue = `Names: {{join . ", "}}`
)
 
func main() {
        // mapa funkcí použitých v šabloně
        funcs := template.FuncMap{"join": strings.Join}
 
        // vytvoření nové šablony
        tmpl := template.Must(template.New("template").Funcs(funcs).Parse(templateValue))
 
        // tyto hodnoty budou použity při aplikaci šablony
        roles := []string{
                "Eliška Najbrtová",
                "Jenny Suk",
                "Anička Šafářová",
                "Sváťa Pulec",
                "Blažej Motyčka",
                "Eda Wasserfall",
                "Přemysl Hájek",
        }
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, roles)
        if err != nil {
                panic(err)
        }
}

Po spuštění si můžeme velmi snadno ověřit, že výsledky odpovídají zadání (a to i pro prázdný vstup):

Names: Eliška Najbrtová, Jenny Suk, Anička Šafářová, Sváťa Pulec, Blažej Motyčka, Eda Wasserfall, Přemysl Hájek

Ú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­plate25.go.

12. Kombinace předchozích možností – blok with a předání i použití funkcí v šabloně

V dalším demonstračním příkladu zkombinujeme některé možnosti, které byly popsány v předchozích kapitolách. Zejména budeme z šablony volat dvojici funkcí (konkrétně funkce asNames a standardní funkci strings.Join a navíc použijeme i blok with, ve kterém vytvoříme a naplníme proměnnou names. Jedná se skutečně o kombinaci již známých postupů, takže si bez dalšího podrobnějšího popisu ukážeme, jak bude vypadat šablona i zdrojový kód demonstračního příkladu:

package main
 
import (
        "os"
        "strings"
        "text/template"
)
 
const (
        templateValue = `Names: {{with $names := asNames .}}{{join $names ", "}}{{end}}`
)
 
// datový typ, jehož prvky budou vypisovány v šabloně
type Role struct {
        Name       string
        Surname    string
        Popularity int
}
 
// převod rolí na řez se jmény rolí
func asNames(roles []Role) []string {
        var r []string
        for _, role := range roles {
                r = append(r, role.Name)
        }
        return r
}
 
func main() {
        // mapa funkcí použitých v šabloně
        funcs := template.FuncMap{
                "asNames": asNames,
                "join":    strings.Join}
 
        // vytvoření nové šablony
        tmpl := template.Must(template.New("template").Funcs(funcs).Parse(templateValue))
 
        // 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ýsledkem běhu tohoto příkladu bude stejný výstup, jaký byl ukázán v předchozí kapitole, tedy:

Names: Eliška, Jenny, Anička, Sváťa, Blažej, Eda, Přemysl

Ú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­plate26.go.

13. Dvourozměrná pole v šablonách

Doposud jsme do šablon předávali buď jednoduché (skalární) hodnoty, záznamy (struct) nebo pole resp. řezy (slice). Pochopitelně nám však nic nebrání v použití dvourozměrných polí, která jsou v určitých oblastech dominantním datovým typem (ostatně viz paralelně běžící miniseriál o „array programmingu“). Práce  dvourozměrnými poli typicky vede k použití zanořených značek „{{range}}“, což vlastně znamená, že se interně pracuje s dvojicí vnořených programových smyček. To si ostatně ukážeme v navazující trojici kapitol.

14. Vytištění tabulky malé násobilky

V dalším demonstračním příkladu se pokusíme s využitím šablony vytisknout tabulku malé násobilky. Vstupními daty je v tomto případě dvourozměrná tabulka s malou násobilkou, která je v jazyce Go reprezentována běžným dvourozměrným polem:

// tabulka s malou násobilkou
var multiplyTable [N][N]int

V případě, že nejsou zadány žádné speciální požadavky na formát jednotlivých řádků tabulky, může být šablona zredukována na pouhou jednu smyčku:

templateValue = `{{range .}}{{.}}
{{end}}`

Je tomu tak z toho důvodu, že v rámci šablony lze vytisknout obsah jednorozměrného pole (neboli vektoru) – uvnitř smyčky je v „tečce“ uložen vždy právě celý řádek tabulky.

Úplný zdrojový kód tohoto příkladu by mohl vypadat následovně:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateValue = `{{range .}}{{.}}
{{end}}`
)
 
func main() {
        const N = 10
 
        // tabulka s malou násobilkou
        var multiplyTable [N][N]int
 
        // naplnění tabulky
        for j := 0; j < N; j++ {
                for i := 0; i < N; i++ {
                        multiplyTable[j][i] = (i + 1) * (j + 1)
                }
        }
 
        // vytvoření nové šablony
        tmpl := template.Must(template.New("multiply_table").Parse(templateValue))
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, multiplyTable)
        if err != nil {
                panic(err)
        }
}

A výsledek získaný po spuštění sice není příliš pěkný, ovšem alespoň získáme všechny prvky tabulky s malou násobilkou:

[1 2 3 4 5 6 7 8 9 10]
[2 4 6 8 10 12 14 16 18 20]
[3 6 9 12 15 18 21 24 27 30]
[4 8 12 16 20 24 28 32 36 40]
[5 10 15 20 25 30 35 40 45 50]
[6 12 18 24 30 36 42 48 54 60]
[7 14 21 28 35 42 49 56 63 70]
[8 16 24 32 40 48 56 64 72 80]
[9 18 27 36 45 54 63 72 81 90]
[10 20 30 40 50 60 70 80 90 100]

Ú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­plate27.go.

15. Vylepšení předchozího demonstračního příkladu

Výsledek předchozího příkladu ve skutečnosti nebyl příliš pěkný, protože hodnoty ve sloupcích nebyly zarovnány. Z tohoto důvodu je nutné tabulku vytisknout poněkud odlišným způsobem – pomocí vnořených smyček. Nejprve budeme pro totožná vstupní data, jako tomu bylo v předchozím příkladu, procházet všemi řádky:

{{range .}}
...
...
...
{{end}}

Uvnitř vnější smyčky odpovídá „tečka“ celému řádku, tedy jednorozměrnému vektoru. Nic nám tedy nezabraňuje postupně procházet i prvky tohoto vektoru (nyní ovšem bez odřádkování):

{{range .}}{{range .}}...{{end}}
{{end}}`

Nyní nám již zbývá hodnoty vektoru vhodným způsobem naformátovat a oddělit od sebe (mezerou). Pro malou násobilku postačuje naformátování hodnot na tři místa:

{{range .}}{{range .}}{{printf "%3d" .}} {{end}}
{{end}}`

Celý postup je použit v dalším, dnes již předposledním, demonstračním příkladu, jehož zdrojový kód vypadá následovně:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateValue = `{{range .}}{{range .}}{{printf "%3d" .}} {{end}}
{{end}}`
)
 
func main() {
        const N = 10
 
        // tabulka s malou násobilkou
        var multiplyTable [N][N]int
 
        // naplnění tabulky
        for j := 0; j < N; j++ {
                for i := 0; i < N; i++ {
                        multiplyTable[j][i] = (i + 1) * (j + 1)
                }
        }
 
        // vytvoření nové šablony
        tmpl := template.Must(template.New("multiply_table").Parse(templateValue))
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, multiplyTable)
        if err != nil {
                panic(err)
        }
}

Nyní bude výsledek odlišný od předchozího příkladu, protože všechny hodnoty v jednotlivých sloupcích budou zarovnány na čtyři znaky:

  1   2   3   4   5   6   7   8   9  10
  2   4   6   8  10  12  14  16  18  20
  3   6   9  12  15  18  21  24  27  30
  4   8  12  16  20  24  28  32  36  40
  5  10  15  20  25  30  35  40  45  50
  6  12  18  24  30  36  42  48  54  60
  7  14  21  28  35  42  49  56  63  70
  8  16  24  32  40  48  56  64  72  80
  9  18  27  36  45  54  63  72  81  90
 10  20  30  40  50  60  70  80  90 100

Ú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­plate28.go.

16. Přístup k poli, které je uloženo jako prvek datové struktury

Pole se nemusí do šablony předávat přímo, ale může být součástí složitější datové struktury – typicky záznamu (struct, record). Podívejme se nyní na jednoduchou variantu, v níž je použit záznam obsahující jako svůj (jediný) prvek právě dvourozměrné pole:

type MultiplyTable struct {
        Values [N][N]int
}

Práce s touto strukturou je triviální, pouze postačuje změnit šablonu z této podoby:

{{range .}}{{range .}}{{printf "%3d" .}} {{end}}
{{end}}`

Na:

{{range .Values}}{{range .}}{{printf "%3d" .}} {{end}}
{{end}}`

Nic dalšího není v šabloně nutné modifikovat:

package main
 
import (
        "os"
        "text/template"
)
 
const (
        templateValue = `{{range .Values}}{{range .}}{{printf "%3d" .}} {{end}}
{{end}}`
)
 
const N = 10
 
type MultiplyTable struct {
        Values [N][N]int
}
 
func main() {
 
        // tabulka s malou násobilkou
        var multiplyTable MultiplyTable
 
        // naplnění tabulky
        for j := 0; j < N; j++ {
                for i := 0; i < N; i++ {
                        multiplyTable.Values[j][i] = (i + 1) * (j + 1)
                }
        }
 
        // vytvoření nové šablony
        tmpl := template.Must(template.New("multiply_table").Parse(templateValue))
 
        // aplikace šablony - přepis hodnot
        err := tmpl.Execute(os.Stdout, multiplyTable)
        if err != nil {
                panic(err)
        }
}

Výsledky získané po spuštění tohoto příkladu by měly být totožné s příkladem předchozím, tedy:

  1   2   3   4   5   6   7   8   9  10
  2   4   6   8  10  12  14  16  18  20
  3   6   9  12  15  18  21  24  27  30
  4   8  12  16  20  24  28  32  36  40
  5  10  15  20  25  30  35  40  45  50
  6  12  18  24  30  36  42  48  54  60
  7  14  21  28  35  42  49  56  63  70
  8  16  24  32  40  48  56  64  72  80
  9  18  27  36  45  54  63  72  81  90
 10  20  30  40  50  60  70  80  90 100

Ú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­plate29.go.

17. Závěr

Minule a dnes jsme si popsali velkou část funkcionality standardního balíčku text/template. Jak (doufejme) bylo z příkladů patrné, jedná se o poměrně propracovaný šablonovací systém, který je navíc rozšiřitelný díky tomu, že je možné přímo volat metody datové struktury předané do šablony a taktéž je možné zaregistrovat prakticky libovolné množství uživatelských funkcí, které je možné ze šablony přímo volat. Zapomenout nesmíme na podporu proměnných, kolon (pipeline), podmínek, smyček atd. Jednou z nevýhod tohoto systému je fakt, že všechny kontroly struktury šablony jsou provedeny až v čase běhu, takže zde není možné využít kontroly prováděné překladačem jazyka Go (a jeho typovým systémem). Samotný balíček text/template je ještě více rozpracován v dalším standardním balíčku html/template, kterému bude věnován samostatný článek.

18. Další šablonovací systémy dostupné pro jazyk Go

Pro programovací jazyk Go vzniklo i poměrně velké množství dalších šablonovacích systémů, které se od sebe odlišují funkcionalitou, podporou různých výstupních formátů, použitým značkovacím jazykem, mírou NIH syndromu atd. V následující tabulce jsou uvedeny ty nejznámější šablonovací systémy, tj. systémy s největším množstvím „hvězdiček“ na GitHubu:

skoleni

# Název šablonovacího systému
0 ace
1 amber
2 damsel
3 ego
4 extemplate
5 fasttemplate
6 gofpdf
7 gospin
8 goview
9 hero
10 jet
11 kasia
12 liquid
13 maroto
14 mustache
15 pongo2
16 quicktemplate
17 raymond
18 Razor
19 Soy
20 sprig
21 velvet

K vybraným šablonovacím systémům se ještě vrátíme v některém z dalších dílů seriálu o programovacím jazyce Go.

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

Zdrojové kódy všech minule i 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
       
16 template16.go naformátování hodnot funkcí fmt.Printf volanou přímo ze šablony https://github.com/tisnik/go-root/blob/master/article80/tem­plate16.go
17 template17.go praktické otestování podmínky v šabloně https://github.com/tisnik/go-root/blob/master/article80/tem­plate17.go
18 template18.go plná podmínka typu if-then-else v šabloně https://github.com/tisnik/go-root/blob/master/article80/tem­plate18.go
19 template19.go volání metod z šablony https://github.com/tisnik/go-root/blob/master/article80/tem­plate19.go
20 template20.go pokus o přístup k privátní metodě z šablony https://github.com/tisnik/go-root/blob/master/article80/tem­plate20.go
21 template21.go kolony (pipeline) v šabloně https://github.com/tisnik/go-root/blob/master/article80/tem­plate21.go
22 template22.go blok with a proměnné v šablonách https://github.com/tisnik/go-root/blob/master/article80/tem­plate22.go
23 template23.go alternativa k předchozímu demonstračnímu příkladu https://github.com/tisnik/go-root/blob/master/article80/tem­plate23.go
24 template24.go definice pojmenované šablony https://github.com/tisnik/go-root/blob/master/article80/tem­plate24.go
25 template25.go předání funkce do šablony, vyvolání funkce v šabloně https://github.com/tisnik/go-root/blob/master/article80/tem­plate25.go
26 template26.go kombinace předchozích možností – blok with a předání i použití funkcí v šabloně https://github.com/tisnik/go-root/blob/master/article80/tem­plate26.go
27 template27.go vytištění tabulky malé násobilky https://github.com/tisnik/go-root/blob/master/article80/tem­plate27.go
28 template28.go vylepšení předchozího demonstračního příkladu https://github.com/tisnik/go-root/blob/master/article80/tem­plate28.go
29 template29.go přístup k poli, které je uloženo jako prvek datové struktury https://github.com/tisnik/go-root/blob/master/article80/tem­plate29.go

20. Odkazy na Internetu

  1. Awesome Go
    https://awesome-go.com/
  2. Template Engines for Go
    https://awesome-go.com/#template-engines
  3. Mail merge
    https://en.wikipedia.org/wi­ki/Mail_merge
  4. Template processor
    https://en.wikipedia.org/wi­ki/Template_processor
  5. Text/template
    https://pkg.go.dev/text/template
  6. Go Template Engines
    https://go.libhunt.com/categories/556-template-engines
  7. Template Engines
    https://reposhub.com/go/template-engines
  8. GoLang Templating Made Easy
    https://awkwardferny.medium.com/golang-templating-made-easy-4d69d663c558
  9. Templates in GoLang
    https://golangdocs.com/templates-in-golang
  10. 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
  11. Ace – HTML template engine for Go
    https://github.com/yosssi/ace
  12. amber
    https://github.com/eknkc/amber
  13. quicktemplate
    https://github.com/valyala/qu­icktemplate
  14. Šablonovací systém ace
    https://github.com/yosssi/ace
  15. Šablonovací systém amber
    https://github.com/eknkc/amber
  16. Šablonovací systém damsel
    https://github.com/dskinner/damsel
  17. Šablonovací systém ego
    https://github.com/benbjohnson/ego
  18. Šablonovací systém extemplate
    https://github.com/dannyvan­kooten/extemplate
  19. Šablonovací systém fasttemplate
    https://github.com/valyala/fas­ttemplate
  20. Šablonovací systém gofpdf
    https://github.com/jung-kurt/gofpdf
  21. Šablonovací systém gospin
    https://github.com/m1/gospin
  22. Šablonovací systém goview
    https://github.com/foolin/goview

Autor článku

Pavel Tišnovský vystudoval VUT FIT a v současné době pracuje ve společnosti Red Hat, kde vyvíjí nástroje pro OpenShift.io.