Goでテンプレートを使用する方法

はじめに

データを整形された出力、テキストレポート、またはHTMLページで表示する必要がありますか?Goテンプレートを使用することで、それが可能です。どんなGoプログラムも、text/templateまたはhtml/templateパッケージ(いずれもGo標準ライブラリに含まれています)を使ってデータをきれいに表示することができます。

両方のパッケージは、テキストのテンプレートを作成し、データを渡して好みに合わせた書式でドキュメントを作成することができます。テンプレート内では、データをループして、条件によってドキュメントに含めるアイテムと表示方法を決定することができます。このチュートリアルでは、両方のテンプレートパッケージの使用方法を説明します。最初に、text/templateを使用して、ループや条件文、カスタム関数を使ってデータをプレーンテキストのレポートに変換する方法を示します。次に、同じデータをhtml/templateを使用して、コードインジェクションがないHTMLドキュメントに変換します。

前提条件

このチュートリアルを始める前に、Goをインストールする必要があります。自分のオペレーティングシステムに適したチュートリアルを読んでください。

  • How To Install Go On Ubuntu 20.04
  • How To Install Go and Set Up a Local Programming Environment on macOS
  • How To Install Go and Set Up a Local Programming Environment on Windows 10

Go言語についての実務知識も必要です。具体的には、struct(構造体)の作成やstructメソッドの使用方法を含みます。

始めましょう。

ステップ1:テキスト/テンプレートのインポート

いくつかの犬のデータについて簡単なレポートを生成したいと考えています。以下のように表示したいです。 (If you prefer using the polite language, replace “いくつかの” with “いくつかの” and “考えています” with “考えております”.)

---
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関数を含んでいるため、go runを使用して実行可能です。text/templateの標準ライブラリパッケージをインポートしているため、テンプレートの作成とレンダリングが可能です。また、osパッケージもインポートされており、ターミナルへの出力に使用されます。

ステップ2 — テンプレートデータの作成

テンプレートを作成する前に、テンプレートに渡すためのデータを作成しましょう。import文の下とmain()の前に、Petという名前の構造体を定義します。この構造体には、ペットの名前、性別、絶育の有無(Intact)、年齢、そして品種のフィールドが含まれます。pets.goを編集して、この構造体を追加してください。

ペットに行く
. . .
type Pet struct {
	Name   string
	Sex    string
	Intact bool
	Age    string
	Breed  string
}
. . .

今、main()関数の本体で、2匹の犬に関するデータを保持するための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を押し、変更を確認してください。

このテンプレートを実行すると、ただ「まだ何もありません。」と表示されるだけですが、テキスト/テンプレートが機能していることを証明するために、データを渡してテンプレートを実行しましょう。main()関数のdogsスライスの後に、以下を追加してください。

ペットが行く
	. . .
	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を使用して新しいテンプレートを作成し、その結果のテンプレートに対してParseFilesを呼び出して最小限のテンプレートファイルを解析しています。エラーをチェックした後、新しいテンプレートのExecuteメソッドを呼び出し、os.Stdoutを渡して完成したレポートをターミナルに出力し、またdogsスライスも渡します。最初の引数には、io.Writerインターフェースを実装した任意のものを渡すことができますので、例えばレポートをファイルに書き込むことができます。後でその方法を見ていきます。

完全なプログラムは以下のようになるべきです。 (Kanzen na puroguramu wa, kore no you ni narubeki desu.)

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 — テンプレートの作成

テンプレートは単なるUTF-8のプレーンテキストですが、それだけではありません。最終的な出力に変更されずに表示される静的なテキストが含まれていますが、それに加えて、テンプレートエンジンへの命令であるアクションも含まれています。これらのアクションは、テンプレート内のデータを操作する方法や出力に含める内容をテンプレートエンジンに伝えるためのものです。アクションは二重中カッコで囲まれており、開始タグと終了タグの間に書かれます—{{ <アクション> }}—そして、カーソルで示されるドット(.)を介してデータにアクセスします。

テンプレートに渡されるデータは何でもありえますが、スライス、配列、あるいはマップなどを渡すことが一般的です。今回はテンプレートにおいて、あなたの犬のスライスを処理してみましょう。

スライスを繰り返す

Goのコードでは、スライスを反復処理するために、forループの開始文内でrangeを使用することができます。テンプレートでは、同じ目的のためにrangeアクションを使用しますが、構文が異なります。forはありませんが、ループを終了するendが追加されています。

「pets.tmpl」を開き、次の内容で置き換えてください。

ペット.tmpl
{{ range . }}
---
(Pet will appear here...)
{{ end }}

ここでは、rangeアクションは1つの引数を取ります。それはカーソル(.)であり、それは犬の全スライスを指します。ループは一番下に{{ end }}で閉じられます。ループの本体では、いくつかの静的なテキストを出力していますが、まだ犬に関する情報はありません。

pets.tmplを保存して、再びpets.goを実行してください。

  1. go run pets.go

 

Output

— (Pet will appear here…) — (Pet will appear here…)

スライスに2匹の犬がいるため、静的なテキストは2回出力されます。さあ、このテキストをより有用な静的テキストに置き換えて、犬のデータを追加しましょう。

フィールドを表示する。

このテンプレートでは、range内に.を渡すと、ドットはスライス全体を参照しますが、rangeループの各イテレーション内では、ドットはスライスの現在のアイテムを参照します。これにより、裸のドットだけを使用して、各ペットのエクスポートされたフィールドにアクセスすることができます。スライスのインデックスを参照する必要はありません。

フィールドを表示するには、波括弧で囲んでドットを前に付けるだけで簡単です。pets.tmpl を開いて、その内容を次のものに置き換えてください。

ペット。tmpl
{{ range . }}
---
Name:  {{ .Name }}

Sex:   {{ .Sex }}

Age:   {{ .Age }}

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

今後、ペット.goは2匹の犬それぞれについて5つのフィールドのうちの4つを印刷します。各フィールドにはラベルも付けられます。(5番目のフィールドについては後で触れます。)

プログラムを保存して再度実行してください。

  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

いい感じですね。さて、第5のフィールドを表示するために条件論理を使う方法を見てみましょう。

条件文を使用する

テンプレートに{{ .Intact }}を追加して「無傷」フィールドを含めなかった理由は、読み手にとって使い勝手が悪くなるからです。例えば、犬に関するサマリーで「無傷: false」と記載されていたら、獣医の請求書を想像してみてください。このフィールドを真偽値として保存することは効率的かもしれませんが、このフィールドに「無傷」は性別中立的な名前です。最終報告書では、このフィールドをif-elseアクションを用いて異なる表示方法にすることができます。

再びpets.tmplを開き、ここに示された強調部分を追加してください。

ペットのテンプレート
{{ range . }}
---
Name:  {{ .Name }}

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

Age:   {{ .Age }}

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

以下の内容を日本語で自然な言い方に言い換えます。1つのオプションで結構です:
テンプレートは現在、Intactフィールドがtrueかどうかをチェックし、trueの場合は(intact)と表示し、そうでない場合は(fixed)と表示します。しかし、それよりももっと良い方法があります。テンプレートをさらに編集して、fixedの代わりに去勢または避妊済みの犬の性別に関連する用語を表示するようにしましょう。元のelse文の中にネストされたif文を追加します。

ペット.tmpl
{{ 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

私たちは2匹の犬を飼っていますが、「Intact」の表示には3つの可能な場合があります。すべての3つの場合をカバーするために、pets.goのスライスにもう1匹の犬を追加しましょう。pets.goを編集して、スライスに3匹目の犬を追加してください。

ペットたちは行く。
. . .
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 (<=)などです。これらの関数およびテンプレート関数を呼び出す方法は2つあります。

    1. 関数名を書き、その後に1つ以上のパラメータをスペースで区切って記述します。上記の例では、eq .Sex “Female” のように使用します。

 

    最初に1つのパラメータを書き、続けてパイプ(|)を書き、その後に関数名、そして必要なパラメータを追加します。これはUnixコマンドライン上のコマンドパイプと似ており、コマンドラインと同様に、パイプライン内で複数の関数呼び出しを連結して使用し、1つの呼び出しの出力を次の呼び出しの入力として渡すことができます。

したがって、テンプレート内のeq比較は、eq .Sex “Female” と書かれているかわりに、.Sex | eq “Female” とも書くことができます。これらの二つの表現は同等です。

レポートの冒頭に犬の数を表示するために、len関数を使用しましょう。pets.tmplを開き、以下を冒頭に追加してください。

ペットの.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のどんな関数でも、単一の値を返すか、もし2番目の値がエラータイプであれば、テンプレートで利用できるということです。

テンプレートでGo関数を使用する方法。

犬のスライスを受け取り、最後の要素だけを表示するテンプレートを作りたいとします。テンプレート内では、スライスの一部分を取得するために、GoのmySlice[x:y]と同様に動作する組み込み関数であるスライス関数を使用することができます。たとえば、3つの要素からなるスライスの最後の要素を取得するためには、{{ slice . 2 }}と書くことができますが、スライス関数はアイテムではなく別のスライスを返します。つまり、{{ slice . 2 }}はslice[2:]と同等です(sliceの2番目以降を取得するもので、sliceの2番目の要素を取得するものではありません)。この関数は複数のインデックスを受け取ることもできますが(たとえば、{{ slice . 0 2 }}はスライスslice[0:2]を取得します)、ここでは使用しません。

しかし、テンプレート内でスライスの最後のインデックスを参照する方法はありますか? len関数は利用できますが、スライス内の最後のアイテムのインデックスはlen – 1であり、残念ながらテンプレート内で算術演算を行うことはできません。

それはカスタム関数が非常に便利な場所です。整数をデクリメントする関数を作成し、その関数をテンプレートで利用できるようにしましょう。

しかし、それをする前に、新しいテンプレートを作りましょう。新しいファイル「lastPet.tmpl」を開き、以下を貼り付けてください。

最後のペット.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関数の呼び出しは、定義し、テンプレートに渡すカスタム関数を参照しています。pets.goのmain()関数内で、次の変更を行ってください(下線部を参照してください)。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を宣言しています。キーはテンプレートで利用可能な関数の名前であり、値はその関数自体です。ここでの1つの関数であるdecは、非常に短いためにインラインで提供される無名関数です。これは整数を受け取り、1を引いた結果を返します。

その後、テンプレートファイル名を変更しています。最後に、ParseFileを呼び出す前にTemplate.Funcsを呼び出し、先ほど定義したfuncMapを渡します。FuncsメソッドはParseFilesの前に呼び出す必要があります。

コードを実行する前に、テンプレート内の範囲アクションで何が起こっているかを理解しましょう。

テンプレートの最後のペット.go
{{- range (len . | dec | slice . ) }}

あなたは、犬のスライスの長さを取得し、それをカスタムの減算関数に渡して1を引き、そしてそれを前に話したスライス関数の第二パラメータとして渡しています。したがって、3匹の犬のスライスの場合、範囲アクションは {{- range (slice . 2) }} と同等です。

ペットを保存して実行する: pets.go を保存して実行してください。

go run pets.go
Output

— Name: Bruce Wayne Sex: Male (neutered) Age: 3 years, 8 months Breed: Chihuahua

いいですね。最後の1匹だけでなく、最後の2匹を表示したい場合はどうなりますか?lastPet.tmplを編集し、パイプライン内にもう1つのdec呼び出しを追加します。

最後のペット.tmpl
{{- 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関数を改善する方法を想像できるでしょう。パラメータを受け取り、その名前を変更することで、dec | decの代わりにminus 2を呼び出すことができるようになります。

今、あなたがゼファーのような雑種犬を異なる方法で表示したかった場合、スラッシュをアンパサンドで置き換えることができます。テンプレート内で使用するために、自分自身で関数を書く必要はありません。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を編集してこの関数を使用します。

最後のペット.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

ゼファーの種類は、スラッシュの代わりにアンパサンドが含まれています。

あなたはテンプレートではなく、pets.goの中でその文字列を操作することができましたが、データの表示はコードではなくテンプレートの役割です。

実際には、一部の犬のデータには既にプレゼンテーションが含まれていることがありますが、それは望ましくないかもしれません。品種フィールドでは、スラッシュを使って複数の品種を1つの文字列に詰め込んでいます。この文字列のパターンは、データ入力者が異なるフォーマットをデータベースに取り込む可能性を引き起こすかもしれません。例えば、Labrador/Poodle、Labrador & Poodle、Labrador、Poodle、Labrador-Poodleミックスなどです。このフォーマットの曖昧さを避け、品種による検索をより柔軟にし、表示を容易にするために、Breedを文字列のスライス([]string)として保存する方が良いかもしれません。そうすることで、テンプレート内でstrings.Join関数を使用して、すべての品種を表示し、.Breedフィールドに追加の注釈(純血種または雑種)を表示することができます。

Note

自分で試してみてください
これらの変更を実装するために、コードとテンプレートを修正してみてください。終了したら、「ソリューション」をクリックして作業を確認してください。

ソリューション
pets.go
. . .
type Pet struct {
Name string
Sex string
Intact bool
Age string
Breed []string
}

func main() {
dogs := []Pet{
{
Name: “ジュジュベ”,
. . .
Breed: []string{“ジャーマンシェパード”, “ピットブル”},
},
{
Name: “ゼファー”,
. . .
Breed: []string{“ジャーマンシェパード”, “ボーダーコリー”},
},
{
Name: “ブルース・ウェイン”,
. . .
Breed: []string{“チワワ”},
},
}
funcMap := template.FuncMap{
“dec”: func(i int) int { return i – 1 },
“replace”: strings.ReplaceAll,
“join”: strings.Join,
}
. . .
} // end main

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

Name: {{ .Name }}

Sex: {{ .Sex }} ({{ if .Intact }}遺伝子破壊されていない{{ else }}{{ if (“Female” | eq .Sex) }}避妊手術を受けた{{ else }}去勢された{{ end }}{{ end }})

Age: {{ .Age }}

Breed: {{ join .Breed ” & ” }} ({{ if len .Breed | eq 1 }}純血種{{ else }}雑種{{ end }})
{{ end -}}

最後に、この同じ犬のデータをHTMLドキュメントに変換して、なぜテンプレートの出力がHTMLの場合は常にhtml/templateパッケージを使用するべきかを確認しましょう。

ステップ5 — HTMLテンプレートの作成

コマンドラインツールは、text/templateを使用して出力を整然と表示することがあります。また、他のバッチプログラムはそのデータから整った構造のファイルを作成するためにそれを使用するかもしれません。しかし、Goのテンプレートは一般的に、ウェブアプリケーションのHTMLページをレンダリングするために使用されます。たとえば、人気のあるオープンソースの静的サイトジェネレーターであるHugoは、text/templateとhtml/templateの両方をテンプレートの基盤として使用しています。

HTMLには、通常のテキストにはないいくつかの特別な課題があります。それは要素を角カッコで囲むために角括弧を使用(

)、実体をマークするためにアンパサンドを使用( )、およびタグ属性を角引用符で囲むために引用符を使用()します。テンプレートに挿入されるデータのいずれかがこれらの文字を含んでいる場合、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という名前を付ける。

ペットの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に渡し、作業の終了時にファイルを閉じます。

HTMLファイルを生成するために、pets.goを実行してください。その後、ブラウザでこのローカルのウェブページを開いてください。

gotcha.png

ブラウザは注入されたスクリプトを実行しました。そのため、特にテンプレートデータのソースを完全に信頼できない場合には、テキスト/テンプレートパッケージを使用してHTMLを生成しない方が良い理由です。

HTML文字列内のHTML文字をエスケープする以外に、html/templateパッケージはtext/templateと同じように動作し、同じベース名(template)を持っています。つまり、テンプレートのインジェクションを安全にするには、text/templateのインポートをhtml/templateに置き換えるだけです。pets.goを編集して、今すぐその作業を行ってください。

ペットが行く
package main

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

ファイルを保存して、最後にもう一度実行し、pets.htmlを上書きします。それからブラウザでHTMLファイルをリフレッシュしてください。

gotcha-sanitized.png

html/templateパッケージは、注入されたスクリプトをウェブページ上では単なるテキストとしてレンダリングしました。テキストエディタでpets.htmlを開いて(またはブラウザでページのソースを表示して)、最初の犬であるジュジューブを見てください。

ペットの.html
. . .
<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パッケージは、ジュジューブの名前の角括弧と引用符の文字、そして品種のアンパサンドを置換しました。

結論

Goのテンプレートは、任意のデータに対してテキストをラッピングするための便利なツールです。コマンドラインツールでの出力のフォーマットや、ウェブアプリケーションでのHTMLの描画など、さまざまな場面で使用することができます。このチュートリアルでは、Goの組み込みテンプレートパッケージを使用して、同じデータから整形されたテキストやHTMLページを印刷しました。これらのパッケージの使用方法について詳しく学ぶには、text/templateとhtml/templateのドキュメントを参照してください。

コメントを残す 0

Your email address will not be published. Required fields are marked *