Как да използвате шаблони в Go


Въведение

Трябва ли да представите някои данни в добре форматиран изход, текстови отчети или HTML страници? Можете да направите това с Go шаблони. Всяка програма на Go може да използва пакета html/template — и двата включени в стандартната библиотека на Go — за да представя спретнато данните.

И двата пакета ви позволяват да пишете текстови шаблони и да предавате данни в тях, за да изобразите документ, форматиран по ваш вкус. В рамките на шаблоните можете да преминете през данните и да използвате условна логика, за да решите кои елементи да включите в документа и как да се показват. Този урок ще ви покаже как да използвате и двата шаблонни пакета. Първо ще използвате text/template, за да изобразите някои данни в отчет с обикновен текст, като използвате цикли, условна логика и персонализирани функции. След това ще използвате html/template, за да изобразите същите данни в HTML документ, който е без инжектиране на код.

Предпоставки

Преди да започнете този урок, трябва само да инсталирате Go. Прочетете подходящия урок за вашата операционна система:

  • Как да инсталирате Go On Ubuntu 20.04
  • Как да инсталирате Go и да настроите локална среда за програмиране на macOS
  • Как да инсталирате Go и да настроите локална среда за програмиране на Windows 10

Ще ви трябват и работни познания по езика Go, включително използването на struct методи.

Да започваме.

Стъпка 1 — Импортиране на текст/шаблон

Да предположим, че искате да генерирате прост отчет за някои данни, които имате за кучета. Искате да го покажете така:

---
Name:  Jujube

Sex:   Female (spayed)

Age:   10 months

Breed: German Shepherd/Pitbull

---
Name:  Zephyr

Sex:   Male (intact)

Age:   13 years, 3 months

Breed: German Shepherd/Border Collie

Това е отчетът, който ще генерирате с помощта на пакета text/template. Маркираните елементи са вашите данни, а останалото е статичен текст от шаблона. Шаблоните живеят или като низове във вашия код, или в свои собствени файлове заедно с вашия код. Те съдържат шаблонен статичен текст, преплетен с условни изрази (т.е. if/else), оператори за контрол на потока (т.е. цикли) и извиквания на функции, всички обвити в {{. . .}} маркери. Ще прехвърлите някои данни във вашия шаблон, за да изобразите окончателен документ като този по-горе.

За да започнете, отворете работното пространство на Go (go env GOPATH) и направете нова директория за този проект:

  1. cd `go env GOPATH`
  2. mkdir pets
  3. cd pets

Използвайки nano или любимия си текстов редактор, отворете нов файл с име pets.go и поставете това в:

  1. nano pets.go
package main

import (
	"os"
	"text/template"
)

func main() {
}

Този файл декларира, че е в пакета main и съдържа функция main, което означава, че може да се изпълнява чрез go run. Той импортира стандартния библиотечен пакет text/template, за да ви позволи да напишете и изобразите шаблон, и os, който ще се използва за отпечатване на терминала.

Стъпка 2 — Създаване на шаблонни данни

Преди да напишем шаблон, нека създадем някои данни, които да предадем в шаблона. Под операторите import и преди main() дефинирайте структура, наречена Pet, която съдържа полета за Name на домашен любимец , Пол, дали домашният любимец е стерилизиран/кастриран (Интактен), Възраст и Порода. Редактирайте pets.go и добавете тази структура:

. . .
type Pet struct {
	Name   string
	Sex    string
	Intact bool
	Age    string
	Breed  string
}
. . .

Сега, в тялото на функцията main(), създайте част от Pets, за да съхранявате данни за две кучета:

. . .
func main() {
	dogs := []Pet{
		{
			Name:   "Jujube",
			Sex:    "Female",
			Intact: false,
			Age:    "10 months",
			Breed:  "German Shepherd/Pitbull",
		},
		{
			Name:   "Zephyr",
			Sex:    "Male",
			Intact: true,
			Age:    "13 years, 3 months",
			Breed:  "German Shepherd/Border Collie",
		},
	}
} // end main

Тези данни ще бъдат предадени на вашия шаблон, за да изобразите окончателен отчет. Разбира се, данните, които подавате към вашите шаблони, могат да идват отвсякъде: вашата база данни, API на трета страна и т.н. За този урок най-лесно е просто да поставите някои примерни данни в кода си.

Сега нека видим как да рендираме — или изпълним, в терминологията на тези пакети — шаблон.

Стъпка 3 — Изпълнение на шаблон

В тази стъпка ще видите как да използвате text/template за генериране на завършен документ от шаблон, но всъщност няма да напишете полезен шаблон до стъпка 4.

Създайте празен текстов файл с име pets.tmpl с малко статичен текст:

Nothing here yet.

Запазете шаблона и излезте от редактора. Ако използвате nano, натиснете CTRL+X, след това Y и ENTER, за да потвърдите промените си.

Въпреки че изпълнението на този шаблон просто ще отпечата \Тук още няма нищо., нека предадем вашите данни и изпълним шаблона само за да докажем, че text/template работи. Добавете следното във вашия main() функция след среза кучета:

	. . .
	var tmplFile = “pets.tmpl”
	tmpl, err := template.New(tmplFile).ParseFiles(tmplFile)
	if err != nil {
		panic(err)
	}
	err = tmpl.Execute(os.Stdout, dogs)
	if err != nil {
		panic(err)
	}
} // end main

В този кодов фрагмент вие използвате Template.New, за да създадете нов Template, след което извиквате ParseFiles на получения шаблон, за да анализирате вашия минимален шаблонен файл. След като проверите за грешки, вие извиквате метода Execute на новия шаблон, като подавате os.Stdout, за да отпечатате готовия отчет на терминала, и също така предавате вашия кучета парче. За първия аргумент можете да подадете всичко, което имплементира интерфейса io.Writer, което означава, че можете да напишете отчета си във файл, например. Ще видим как да го направим по-късно.

Пълната програма трябва да изглежда така:

package main

import (
	"os"
	"text/template"
)

type Pet struct {
	Name   string
	Sex    string
	Intact bool
	Age    string
	Breed  string
}

func main() {
	dogs := []Pet{
		{
			Name:   "Jujube",
			Sex:    "Female",
			Intact: false,
			Age:    "10 months",
			Breed:  "German Shepherd/Pitbull",
		},
		{
			Name:   "Zephyr",
			Sex:    "Male",
			Intact: true,
			Age:    "13 years, 3 months",
			Breed:  "German Shepherd/Border Collie",
		},
	}
	var tmplFile = “pets.tmpl”
	tmpl, err := template.New(tmplFile).ParseFiles(tmplFile)
	if err != nil {
		panic(err)
	}
	err = tmpl.Execute(os.Stdout, dogs)
	if err != nil {
		panic(err)
	}
} // end main

Запазете програмата, след което я стартирайте с go run:

  1. go run pets.go
Output
Nothing here yet.

Програмата все още не отпечатва вашите данни, но поне кодът работи чисто. Сега нека напишем шаблон.

Стъпка 4 — Писане на шаблон

Шаблонът е само действия, които са инструкции към механизма за шаблони, които му казват как да премине през предадените данни и какво да включи в изхода. Действията са обвити в двойка отварящи и затварящи двойни фигурни скоби—{{ }}—и осъществяват достъп до данните чрез курсора, обозначени с точка (.).

Данните, предадени в шаблон, могат да бъдат абсолютно всякакви, но е обичайно да се предадат в срез, масив или карта - нещо, което може да се повтори. Нека накараме шаблона да премине през вашия срез кучета.

Лупинг над парче

В кода на Go можете да използвате range в началния оператор на цикъл for, за да обхождате срез. В шаблоните използвате действието range за същата цел, но има различен синтаксис: няма for, но има добавен end, за да затворите цикъла.

Отворете pets.tmpl и заменете съдържанието му със следното:

{{ range . }}
---
(Pet will appear here...)
{{ end }}

Действието обхват приема тук един аргумент: курсора (.), който се отнася до целия срез кучета. Цикълът е затворен с {{ end }} в долната част. В тялото на цикъла отпечатвате някакъв статичен текст и все още нищо за кучетата.

Запазете pets.tmpl и стартирайте отново pets.go:

  1. go run pets.go
Output
--- (Pet will appear here...) --- (Pet will appear here...)

Статичният текст се отпечатва два пъти, тъй като във вашия отрязък има две кучета. Сега нека заменим това с малко по-полезен статичен текст, заедно с данните за кучето.

Показване на поле

Когато предавате . към range в този шаблон, точката се отнася за целия срез, но в рамките на всяка итерация на цикъла range, точката се отнася към текущия елемент в среза. Това ви позволява да получите достъп до експортираните полета на всеки домашен любимец, като използвате само голата точка. Не е необходимо да препращате към индексите на срезове.

Показването на поле е толкова просто, колкото да го увиете във фигурни скоби и да поставите точката пред него. Отворете pets.tmpl и заменете съдържанието му с това:

{{ range . }}
---
Name:  {{ .Name }}

Sex:   {{ .Sex }}

Age:   {{ .Age }}

Breed: {{ .Breed }}
{{ end }}

Сега pets.go ще отпечата четири от пет полета за всяко от двете кучета, включително някои етикети за полетата. (Ще стигнем до петото поле след малко.)

Запазете и стартирайте програмата отново:

  1. go run pets.go
Output
--- Name: Jujube Sex: Female Age: 10 months Breed: German Shepherd/Pitbull --- Name: Zephyr Sex: Male Age: 13 years, 3 months Breed: German Shepherd/Border Collie

Изглежда добре. Сега нека видим как да използваме някаква условна логика, за да покажем това пето поле.

Използване на условия

Причината, поради която не включихме полето Intact чрез добавяне на {{ .Intact }} към шаблона, е, че това не би било много удобно за четене. Представете си, че сметката ви за ветеринарен лекар пише Intact: false в резюмето за вашето куче. Въпреки че може да е ефективно да съхранявате това поле като булево, а не като низ, и Intact е добро неутрално по пол име за това поле, можем да го покажем по различен начин в нашия окончателен отчет, като използваме if -друго действие.

Отворете отново pets.tmpl и добавете маркираната част, показана тук:

{{ range . }}
---
Name:  {{ .Name }}

Sex:   {{ .Sex }} ({{ if .Intact }}intact{{ else }}fixed{{ end }})

Age:   {{ .Age }}

Breed: {{ .Breed }}
{{ end }}

Сега шаблонът проверява дали полето Intact е вярно и отпечатва (intact), ако е така, или (fixed), ако не. Но можем и по-добре от това. Нека редактираме допълнително шаблона, за да отпечатаме специфичните за пола термини за фиксирано куче — кастрирано или кастрирано — вместо фиксирано. Добавете вложен if в оригиналния else:

{{ range . }}
---
Name:  {{ .Name }}

Sex:   {{ .Sex }} ({{ if .Intact }}intact{{ else }}{{ if (eq .Sex "Female") }}spayed{{ else }}neutered{{ end }}{{ end }})

Age:   {{ .Age }}

Breed: {{ .Breed }}
{{ end }}

Запазете шаблона и стартирайте pets.go:

  1. go run pets.go
Output
--- Name: Jujube Sex: Female (spayed) Age: 10 months Breed: German Shepherd/Pitbull --- Name: Zephyr Sex: Male (intact) Age: 13 years, 3 months Breed: German Shepherd/Border Collie

Имаме две кучета, но три възможни случая за показване на непокътнати. Нека добавим още едно куче към частта в pets.go, за да покрием и трите случая. Редактирайте pets.go и добавете трето куче към среза:

. . .
func main() {
	dogs := []Pet{
		{
			Name:   "Jujube",
			Sex:    "Female",
			Intact: false,
			Age:    "10 months",
			Breed:  "German Shepherd/Pitbull",
		},
		{
			Name:   "Zephyr",
			Sex:    "Male",
			Intact: true,
			Age:    "13 years, 3 months",
			Breed:  "German Shepherd/Border Collie",
		},
		{
			Name:	"Bruce Wayne",
			Sex:	"Male",
			Intact:	false,
			Age:	"3 years, 8 months",
			Breed:	"Chihuahua",
		},
	}
. . .

Сега запазете и стартирайте pets.go:

  1. go run pets.go
Output
--- Name: Jujube Sex: Female (spayed) Age: 10 months Breed: German Shepherd/Pitbull --- Name: Zephyr Sex: Male (intact) Age: 13 years, 3 months Breed: German Shepherd/Border Collie --- Name: Bruce Wayne Sex: Male (neutered) Age: 3 years, 8 months Breed: Chihuahua

Страхотно - изглежда според очакванията.

Сега нека обсъдим функциите на шаблона, като функцията eq, която току-що използвахте.

Използване на шаблонни функции

Заедно с eq, има други функции за сравняване на стойности на полета и връщане на булево: gt (>), ne (!=), le (<=) и т.н. Можете да извикате тези функции — и всяка шаблонна функция — по един от двата начина:

  1. Напишете името на функцията, последвано от един или повече параметри с интервал между всеки един. Ето как сте използвали eq по-горе: eq .Sex \Female.
  2. Напишете първо един параметър, последван от тръба (|), след това името на функцията, последвано от други параметри. Това е подобно на тръбопровод, предаване на изхода от едно повикване като вход на следващото повикване и т.н.

Така че докато сравнението eq във вашия шаблон е написано като eq .Sex \Female, то може да бъде написано и като .Sex | eq \Female . Тези два израза са еквивалентни.

Нека използваме функцията len, за да покажем броя на кучетата в горната част на вашия отчет. Отворете pets.tmpl и добавете следното най-отгоре:

Number of dogs: {{ . | len -}}

{{ range . }}
. . .

Може също да сте написали {{ len. -}}.

Обърнете внимание на тирето (-) до затварящите двойни фигурни скоби. Това предотвратява отпечатването на нови редове (\n) след действие. Можете също да добавите тире към отварящите двойни фигурни скоби ({{-), за да потиснете новия ред преди действие.

Запазете шаблона и стартирайте pets.go:

  1. go run pets.go
Output
Number of dogs: 3 --- Name: Jujube Sex: Female (spayed) Age: 10 months Breed: German Shepherd & Pitbull --- Name: Zephyr Sex: Male (intact) Age: 13 years, 3 months Breed: German Shepherd & Border Collie --- Name: Bruce Wayne Sex: Male (neutered) Age: 3 years, 8 months Breed: Chihuahua

Поради тирето в {{ . | len -}}, няма празни редове между етикета Брой кучета и първото куче.

Може би сте забелязали, че списъкът с вградени функции в документацията text/template е доста малък. Добрата новина е, че можете да направите всяка Go функция достъпна за вашите шаблони, стига да връща или една стойност, или две стойности, ако втората е от тип грешка.

Използване на Go функции в шаблони

Да предположим, че искате да напишете шаблон, който взема част от кучета и отпечатва само последното. Можете да получите подмножество от срез в шаблони, като използвате вградената функция slice, която работи подобно на mySlice[x:y] в Go. Можете да напишете {{ slice . 2 }}, за да получите последния елемент от секция от три елемента, въпреки че функцията slice връща друг срез, а не елемент. Тоест {{ slice . 2 }} е еквивалентен на slice[2:], а не на slice[2]. (Функцията може също да вземе повече от един индекс, напр. {{ slice . 0 2 }}, за да получи среза slice[0:2], но няма да използва това тук.)

Но как можете да посочите последния индекс на срез във вашия шаблон? Функцията len е налична, но последният елемент в секция има индекс len - 1 и за съжаление не можете да правите аритметика в шаблони.

Това е мястото, където персонализираната функция е толкова полезна. Нека напишем функция, която намалява цяло число и направим тази функция достъпна за нашия шаблон.

Но преди да направим това, нека направим нов шаблон. Отворете нов файл lastPet.tmpl и поставете следното:

{{- range (len . | dec | slice . ) }}
---
Name:  {{ .Name }}

Sex:   {{ .Sex }} ({{ if .Intact }}intact{{ else }}{{ if ("Female" | eq .Sex) }}spayed{{ else }}neutered{{ end }}{{ end }})

Age:   {{ .Age }}

Breed: {{ .Breed }}
{{ end -}}

Извикването на функцията dec на първия ред препраща към персонализираната функция, която ще дефинирате и предадете към шаблона. В рамките на функцията main() в pets.go направете следните промени (маркирани) под среза dogs и преди извикването на tmpl.Execute():

	. . .
	funcMap := template.FuncMap{
		"dec": func(i int) int { return i - 1 },
	}
	var tmplFile = “lastPet.tmpl”
	tmpl, err := template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile)
	if err != nil {
		panic(err)
	}
	. . .

Първо, вие декларирате FuncMap, което е карта на функциите: ключовете са имената на функциите, предоставени на вашите шаблони, а стойностите са самите функции. Вашата единствена функция тук, dec, е анонимна функция, предоставена вградена, защото е толкова кратка. Взема цяло число, изважда единица от него и връща резултата.

След това променяте името на вашия шаблонен файл. И накрая, вмъквате извикване на Template.Funcs преди извикването на ParseFile, като му предавате funcMap, който току-що дефинирахте. Методът Funcs трябва да бъде извикан преди ParseFiles.

Преди да стартирате кода си, нека разберем какво се случва в действието range във вашия шаблон:

{{- range (len . | dec | slice . ) }}

Получавате дължината на вашия кучета срез, предавате го на вашата персонализирана функция dec, за да извадите едно, и след това го предавате като втори параметър на slice функция, обсъдена по-рано. Така че за срез с три кучета действието range е еквивалентно на {{- range (slice . 2) }}.

Запазете pets.go и го стартирайте:

go run pets.go
Output
--- Name: Bruce Wayne Sex: Male (neutered) Age: 3 years, 8 months Breed: Chihuahua

Изглежда добре. Ами ако искате да покажете последните две кучета вместо само последното? Редактирайте lastPet.tmpl и добавете друго извикване към dec в конвейера:

{{- range (len . | dec | dec | slice . ) }}
. . .

Запазете файла и стартирайте отново pets.go:

go run pets.go
Output
--- Name: Zephyr Sex: Male (intact) Age: 13 years, 3 months Breed: German Shepherd/Border Collie --- Name: Bruce Wayne Sex: Male (neutered) Age: 3 years, 8 months Breed: Chihuahua

Вероятно можете да си представите как да подобрите функцията dec, като вземете параметър и промените името му, така че да можете да извикате минус 2 вместо dec | дек.

Сега кажете, че искате да покажете кучета от смесени породи като Zephyr по различен начин, като замените наклонената черта с амперсанд. Не е нужно да пишете своя собствена функция, за да направите това, защото пакетът strings има такава и можете да вземете назаем функция от всеки пакет, която да използвате във вашите шаблони. Редактирайте pets.go, за да импортирате пакета strings и добавете една от неговите функции към funcMap:

package main

import (
	"os"
	"strings"
	"text/template"
)
. . .
func main() {
	. . .
	funcMap := template.FuncMap{
		"dec":     func(i int) int { return i - 1 },
		"replace": strings.ReplaceAll,
	}
	. . .
} // end main

Сега импортирате пакета strings и добавяте неговата функция ReplaceAll към вашата funcMap под името replace. Сега редактирайте lastPet.tmpl, за да използвате тази функция:

{{- range (len . | dec | dec | slice . ) }}
---
Name:  {{ .Name }}

Sex:   {{ .Sex }} ({{ if .Intact }}intact{{ else }}{{ if ("Female" | eq .Sex) }}spayed{{ else }}neutered{{ end }}{{ end }})

Age:   {{ .Age }}

Breed: {{ replace .Breed “/” “ & ” }}
{{ end -}}

Запазете файла и го стартирайте още веднъж:

  1. go run pets.go
Output
--- Name: Zephyr Sex: Male (intact) Age: 13 years, 3 months Breed: German Shepherd & Border Collie --- Name: Bruce Wayne Sex: Male (neutered) Age: 3 years, 8 months Breed: Chihuahua

Породата на Zephyr вече съдържа амперсанд вместо наклонена черта.

Можеше да манипулираш този низ в pets.go вместо в твоя шаблон, но представянето на данни е работа на шаблоните, а не на кода.

Всъщност някои от данните за кучетата вече съдържат известна презентация - и може би не трябва. Полето Порода събира няколко породи в един низ, поставяйки ги наклонена черта. Този модел с един низ може да покани въвеждащите данни да въведат различни формати в базата данни: Лабрадор/пудел, Лабрадор и пудел, Лабрадор, пудел, Смес лабрадор-пудел и т.н. Може да е по-добре да съхранявате Breed като част от низове ([]string) вместо низ за избягвайте тази двусмисленост на формата и за да направите търсенето по порода по-гъвкаво и представянето му по-лесно. След това можете да използвате функцията strings.Join във вашите шаблони, за да отпечатате всички породи, плюс допълнителна бележка до полето .Breed ((чистокръвен) или (смесена порода)).

Опитайте да промените своя код и шаблони, за да приложите тези промени. Когато приключите, щракнете върху Решение по-долу, за да проверите работата си.

. . .
type Pet struct {
	Name   string
	Sex    string
	Intact bool
	Age    string
	Breed  []string
}

func main() {
	dogs := []Pet{
		{
			Name:   "Jujube",
			. . .
			Breed:  []string{"German Shepherd", "Pit Bull"},
		},
		{
			Name:   "Zephyr",
			. . .
			Breed:  []string{"German Shepherd", "Border Collie"},
		},
		{
			Name:   "Bruce Wayne",
			. . .
			Breed:  []string{"Chihuahua"},
		},
	}
	funcMap := template.FuncMap{
		"dec":     func(i int) int { return i - 1 },
		"replace": strings.ReplaceAll,
		"join":    strings.Join,
	}
	. . .
} // end main
{{- range (len . | dec | dec | slice . ) }}
---
Name:  {{ .Name }}

Sex:   {{ .Sex }} ({{ if .Intact }}intact{{ else }}{{ if ("Female" | eq .Sex) }}spayed{{ else }}neutered{{ end }}{{ end }})

Age:   {{ .Age }}

Breed: {{ join .Breed " & " }} ({{ if len .Breed | eq 1 }}purebred{{ else }}mixed breed{{ end }})
{{ end -}}

В заключение, нека рендираме същите тези данни за кучета в HTML документ и да видим защо винаги трябва да използвате пакета html/template, когато изходът от вашите шаблони е HTML.

Стъпка 5 — Писане на HTML шаблон

Инструмент на командния ред може да използва текст/шаблон за спретнато отпечатване на изход, а друга пакетна програма може да го използва за създаване на добре структурирани файлове от някои данни. Но Go шаблоните обикновено се използват и за изобразяване на HTML страници за уеб приложения. Популярният генератор на статични сайтове с отворен код Hugo, например, използва както text/template, така и html/template като основа за своите шаблони.

HTML идва с няколко специални предизвикателства, които обикновеният текст няма. Той използва ъглови скоби за обвиване на елементи (<td>), амперсанд за маркиране на обекти ( ) и знаци в кавички за обвиване на атрибути на тагове (). Ако някои от данните, които вашият шаблон вмъква, съдържат тези знаци, използването на пакета text/template може да доведе до неправилно образуван HTML или по-лошо, инжектиране на код.

Не е така с пакета html/template. Този пакет избягва тези проблемни знаци, като вмъква техните безопасни HTML еквиваленти (обекти). Амперсанд във вашите данни става &, лява ъглова скоба става < и т.н.

Нека използваме същите данни за кучета от по-рано, за да създадем HTML документ, но първо ще продължим да използваме text/template, за да покажем неговата опасност.

Отворете pets.go и добавете следния маркиран текст към полето Име на Jujube:

        . . .
	dogs := []Pet{
		{
			Name:   "<script>alert(\"Gotcha!\");</script>Jujube",
			Sex:    "Female",
			Intact: false,
			Age:    "10 months",
			Breed:  "German Shepherd/Pit Bull",
		},
		{
			Name:   "Zephyr",
			Sex:    "Male",
			Intact: true,
			Age:    "13 years, 3 months",
			Breed:  "German Shepherd/Border Collie",
		},
		{
			Name:   "Bruce Wayne",
			Sex:    "Male",
			Intact: false,
			Age:    "3 years, 8 months",
			Breed:  "Chihuahua",
		},
	}
        . . .

Сега създайте HTML шаблон в нов файл, наречен petsHtml.tmpl:

<p><strong>Pets:</strong> {{ . | len }}</p>
{{ range . }}
<hr />
<dl>
	<dt>Name</dt>
	<dd>{{ .Name }}</dd>
	<dt>Sex</dt>
	<dd>{{ .Sex }} ({{ if .Intact }}intact{{ else }}{{ if (eq .Sex "Female") }}spayed{{ else }}neutered{{ end }}{{ end }})</dd>
	<dt>Age</dt>
	<dd>{{ .Age }}</dd>
	<dt>Breed</dt>
	<dd>{{ replace .Breed “/” “ & ” }}</dd>
</dl>
{{ end }}

Запазете HTML шаблона. Преди да стартираме pets.go, трябва да редактираме променливата tmpFile, но нека редактираме и програмата, за да изведе шаблона във файл вместо в терминала. Отворете pets.go и добавете маркирания код във функцията main():

	. . .
	funcMap := template.FuncMap{
		"dec":     func(i int) int { return i - 1 },
		"replace": strings.ReplaceAll,
	}
	var tmplFile = "petsHtml.tmpl"
	tmpl, err := template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile)
	if err != nil {
		panic(err)
	}
	var f *os.File
	f, err = os.Create("pets.html")
	if err != nil {
		panic(err)
	}
	err = tmpl.Execute(f, dogs)
	if err != nil {
		panic(err)
	}
	err = f.Close()
	if err != nil {
		panic(err)
	}
} // end main

Отваряте нов файл, наречен pets.html, като го предавате (вместо os.Stdout) на tmpl.Execute, след което затваря файла, когато приключи.

Сега стартирайте go run pets.go, за да генерирате HTML файла. След това отворете тази локална уеб страница в браузъра си:

Браузърът е изпълнил инжектирания скрипт. Ето защо никога не трябва да използвате пакета text/template за генериране на HTML, особено когато не можете напълно да се доверите на източника на данните на вашите шаблони.

Освен екранирането на HTML знаци в данните, пакетът html/template работи точно като text/template и има същото основно име (template), което означава, че всичко, което трябва да направите, за да направите вашия шаблон безопасен за инжектиране, е да замените импортирането на text/template с html/template. Редактирайте pets.go и направете това сега:

package main

import (
	"os"
	"strings"
	"html/template"
)
. . .

Запазете файла и го стартирайте за последен път, като презапишете pets.html. След това опреснете HTML файла във вашия браузър:

Пакетът html/template е изобразил инжектирания скрипт само като текст в уеб страницата. Отворете pets.html във вашия текстов редактор (или вижте изходния код на страницата в браузъра си) и вижте първото куче Jujube:

. . .
<dl>
        <dt>Name</dt>
        <dd>&lt;script&gt;alert(&#34;Gotcha!&#34;);&lt;/script&gt;Jujube</dd>
        <dt>Sex</dt>
        <dd>Female (spayed)</dd>
        <dt>Age</dt>
        <dd>10 months</dd>
        <dt>Breed</dt>
        <dd>German Shepherd &amp; Pit Bull</dd>
</dl>
. . .

Пакетът html замени ъгловите скоби и кавичките в името на Jujube, както и амперсанда в породата.

Заключение

Go Templates са удобен инструмент за обвиване на всеки текст около всякакви данни. Можете да ги използвате в инструментите на командния ред за форматиране на изхода, във вашите уеб приложения за изобразяване на HTML и др. В този урок използвахте вградените шаблонни пакети на Go, за да отпечатате добре форматиран текст и HTML страница от едни и същи данни. За да научите повече за това как да използвате тези пакети, вижте документацията за html/template.