こんにちは。KOUKIです。とある企業でGo言語を使った開発業務に従事しています。
Go言語は私の大好きな言語なのですが、日本ではあまり流行っておりません^^;そのため、啓蒙活動の一環で、Go言語の素晴らしい機能を本ブログで紹介しています。
今回は、Go言語のhtml/templateパッケージを使ってWebアプリを作った時に得た知見を紹介しようと思います。
<目次>
紹介したいこと
本記事では、テンプレートの共通化について触れたいと思います。
Go言語のhtml/templateパッケージは、任意の形式のファイルからHTML文章を生成してくれる便利なツールです。しかも、テンプレートという形で処理するので、プログラムで生成したデータをテンプレートに渡して、画面に表示することもできます。
Djangoを触ったことがある人ならテンプレートシステムを使った経験があると思いますが、その機能と同じです。
Go言語で作成したWebサーバーでクライアントからリクエストを受け取る -> テンプレートから描画するデータを作成 -> 画面に返す、みたいなことをGo言語の基本機能だけで実装できます。
このテンプレートには、基本的にはHTML文章を記述するわけですが、共通部分が多くでてきます。例えば、DOCTYPEやheadタグなどです。
重複コードはプログラムのメンテナンス性を著しく下げるので、共通部分を一つのファイルに括り出して、他のテンプレートから参照できるようにします。
ワークスペースの準備
作業に必要なファイルを用意してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
mkdir helloworld cd helloworld/ go mod init helloworld touch main.go mkdir handlers touch handlers/handlers.go mkdir render touch render/render.go mkdir templates touch templates/about.page.tmpl touch templates/home.page.tmpl touch templates/base.layout.tmpl mkdir config touch config/config.go |
ざっとWebアプリを作る
最初に、簡単なWebアプリを作ります。
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// main.go package main import ( "fmt" "helloworld/handlers" "net/http" ) const portNumber = ":8080" func main() { http.HandleFunc("/", handlers.Home) http.HandleFunc("/about", handlers.About) fmt.Println(fmt.Printf("Starting appllication on port %s\n", portNumber)) _ = http.ListenAndServe(portNumber, nil) } |
render.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// render.go package render import ( "fmt" "html/template" "net/http" ) func RenderTemplate(w http.ResponseWriter, tmpl string) { parsedTemplate, _ := template.ParseFiles("./templates/" + tmpl) err := parsedTemplate.Execute(w, nil) if err != nil { fmt.Println("Error parsingg template:", err) return } } |
handlers.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// handlers.go package handlers import ( "helloworld/render" "net/http" ) func Home(w http.ResponseWriter, r *http.Request) { render.RenderTemplate(w, "home.page.tmpl") } func About(w http.ResponseWriter, r *http.Request) { render.RenderTemplate(w, "about.page.tmpl") } |
home.page.tmpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<!-- home.page.tmpl --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous"> <title>Home Page</title> </head> <body> <div class="container"> <div class="row> <div class="col> <h1>Tis is the home page</h1> <p>This is some text</p> </div> </div> </div> </body> </html> |
about.page.tmpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<!-- about.page.tmpl --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous"> <title>About Page</title> </head> <body> <div class="container"> <div class="row> <div class="col> <h1>Tis is the about page</h1> </div> </div> </div> </body> </html> |
home.page.tmplとabouut.page.tmplが、テンプレートファイルです。
ご覧の通り、重複する箇所が多くあると思います。
動作確認
次のコマンドで、アプリを動かします。
1 |
go run main.go |
ブラウザから「http://localhost:8080/」にアクセスします。

同様に「http://localhost:8080/about」にアクセスします。

こんな感じで、Go言語の機能だけでUIを作り、ルーティングさせることが可能です。
テンプレートの共通化に立ちはだかる問題
テンプレートの共通箇所をbase.layout.tmplに移します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<!-- base.layout.tmpl --> {{define "base"}} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous"> <title> {{block "title" .}} {{end}} </title> </head> <body> {{block "content" .}} {{end}} </body> </html> {{end}} |
defineやblockなどは、テンプレート構文です。
1 2 |
{{define "base"}} {{block "content" .}} |
その他のテンプレートは、以下のように修正しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!-- home.page.tmpl --> {{template "base" .}} {{define "title"}} Home {{end}} {{define "content"}} <div class="container"> <div class="row> <div class="col> <h1>Tis is the home page</h1> <p>This is some text</p> </div> </div> </div> {{end}} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<!-- about.page.tmpl --> {{template "base" .}} {{define "title"}} About {{end}} {{define "content"}} <div class="container"> <div class="row> <div class="col> <h1>Tis is the about page</h1> </div> </div> </div> {{end}} |
共通箇所を括り出すことで、だいぶスッキリしました。
しかし、このコードは、ページ読み込み時に以下のエラーを出力します。
1 2 |
Error parsing template: html/template:home.page.tmpl:1:11: no such template "base" |
base.layout.tmplが読み込めていないようですね^^;
ページも真っ白になります。

解決方法
先に解決したコードを以下に記載します。
render.goを以下のように書き換えてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
// render.go package render import ( "bytes" "fmt" "html/template" "log" "net/http" "path/filepath" ) var functions = template.FuncMap{} func RenderTemplate(w http.ResponseWriter, tmpl string) { tc, err := CreateTemplateCache() if err != nil { log.Fatal(err) } t, ok := tc[tmpl] if !ok { log.Fatal(err) } buf := new(bytes.Buffer) _ = t.Execute(buf, nil) _, err = buf.WriteTo(w) if err != nil { fmt.Println("Error writing template to browser", err) } } func CreateTemplateCache() (map[string]*template.Template, error) { myCache := map[string]*template.Template{} pages, err := filepath.Glob("./templates/*.page.tmpl") if err != nil { return myCache, err } for _, page := range pages { name := filepath.Base(page) ts, err := template.New(name).Funcs(functions).ParseFiles(page) if err != nil { return myCache, err } matches, err := filepath.Glob("./templates/*.layout.tmpl") if err != nil { return myCache, err } if len(matches) > 0 { ts, err = ts.ParseGlob("./templates/*.layout.tmpl") if err != nil { return myCache, err } } myCache[name] = ts } return myCache, nil } |
このコードでは、home/aboutテンプレートを先に読み込み、それらにbaseテンプレートをマージして画面出力しています。
この処理で、問題なく表示されるようになります。


解説
ソースコードの解説に移ります。
CreateTemplateCache関数
CreateTemplateCache関数の中で、最初にキャッシュを定義しています。
1 |
myCache := map[string]*template.Template{} |
myCacheには、テンプレートのファイル名とパースしたテンプレート情報が入ります。
1 |
map[about.page.tmpl:0xc0001862a0 home.page.tmpl:0xc000186a80] |
次に、home/aboutのテンプレートを読みこみます。
1 |
pages, err := filepath.Glob("./templates/*.page.tmpl") |
これで、templatesフォルダ配下にある「*.page.tmpl」パターンに一致したファイルを全て取得しています。
1 |
[templates/about.page.tmpl templates/home.page.tmpl] |
取得したファイル情報一つ一つにbaseをマージするので、pagesをループします。
1 2 3 |
for _, page := range pages { ... } |
forループの中では、最初にファイル名を取得します。
1 |
name := filepath.Base(page) |
1 2 3 4 |
// templates/about.page.tmpl templates/home.page.tmplからファイル名を取得 home.page.tmpl about.page.tmpl |
次に、取得した名前を元に新しいテンプレートインスタンスを作成します。
1 |
ts, err := template.New(name).Funcs(functions).ParseFiles(page) |
「template.New」は、渡したファイル名で新しいテンプレートを作ります。
「Funcs」は、引数に指定したfunctionの実行結果をテンプレートに埋め込みます。今回は、特に埋め込むものもなかったので、以下のように空の「FuncMap」を渡しています。
1 |
var functions = template.FuncMap{} |
FuncMapには、関数をMapに持たせることができるようです。
1 2 3 4 5 |
// 例 funcMap := template.FuncMap{ "add": func(a, b int) int { return a + b }, "sub": func(a, b int) int { return a - b }, } |
ParseFiles(page)の処理は、引数に渡したテンプレート情報を新しく作成したテンプレートインスタンスにマージするようです。
このようにして、tsにはhome/aboutのパースされたテンプレート情報が格納されます。
そして、最後にhome/aboutのテンプレートとbaseのテンプレートのマージを「ParseGlob」で行います。
1 2 |
// tsにはhome/aboutのパース情報が入っている ts, err = ts.ParseGlob("./templates/*.layout.tmpl") |
RenderTemplate関数
RenderTemplate関数では、最初にCreateTemplateCache関数を呼びます。
RenderTemplate関数の引数にはテンプレートのパスが渡されるので、以下のようにしてキャッシュからリクエストに応じたテンプレート情報を取り出せます。
1 2 3 |
// tmplにはtemplates/about.page.tmpl // templates/home.page.tmplの情報が入る t, ok := tc[tmpl] |
テンプレートインスタンスは「Execute」メソッドを持っており、bufferにテンプレート情報を吐き出します。
1 2 |
buf := new(bytes.Buffer) _ = t.Execute(buf, nil) |
そして、このバッファをResponseWriterに書き込めば、Web上で表示できるようになります。
1 |
, err = buf.WriteTo(w) |
これで完成です。
おわりに
作ろうと思えば、Go言語だけでWebアプリケーションをサクッと作れてしまいます。
しかもGo言語の文法は平易で、覚えやすく、並行処理プログラミングも簡単にマスターできるので、初心者にも大変おすすめな言語です^^
日本に広く普及して、Go言語エンジニアの価値も上がることを祈っています。
それでは、また!
おまけ
ここからは、文字通りおまけです。
キャッシュの導入
キャッシュ機能を導入してみましょう。
現時点ではページがリクエストされる度にCreateTemplateCache関数が実行され、テンプレートが読み込まれます。
例えば、Aboutページを表示します。

この状態で、about.page.tmplにh2タグをつけて、アプリケーションを再起動しないでページを再表示してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<!-- about.page.tmpl --> {{template "base" .}} {{define "title"}} About {{end}} {{define "content"}} <div class="container"> <div class="row> <div class="col> <h1>Tis is the about page</h1> <h2>Confirm Cache</h2> </div> </div> </div> {{end}} |

「Confirm Cache」が表示されましたね。これは、毎回テンプレートを読み込んでいるからです。この動作をキャッシュを使って回避します。
コンフィグファイルを定義
アプリケーション全体で使うconfig設定をconfig.goに実装します。
1 2 3 4 5 6 7 8 9 |
// config.go package config import "html/template" type AppConfig struct { UseCache bool TemplateCache map[string]*template.Template } |
このように設定ファイルを用意すると実装がスマートになります。
UseCacheは、キャッシュのON/OFFを決めるパラメータです。この値がONの場合は、キャッシュが有効になります。
TemplateCacheには、キャッシュされたテンプレートのインスタンス情報が入ります。
render.goの書き換え
render.goに先ほど定義したコンフィグを内包するNewTemplates関数を定義し、RenderTemplate関数を書き換えてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// render.go package render import ( "bytes" "fmt" "helloworld/config" "html/template" "log" "net/http" "path/filepath" ) ... var app *config.AppConfig func NewTemplates(a *config.AppConfig) { app = a } func RenderTemplate(w http.ResponseWriter, tmpl string) { var tc map[string]*template.Template if app.UseCache { tc = app.TemplateCache } else { tc, _ = CreateTemplateCache() } t, ok := tc[tmpl] if !ok { log.Fatal("Could not get template from template cache") } buf := new(bytes.Buffer) _ = t.Execute(buf, nil) _, err := buf.WriteTo(w) if err != nil { fmt.Println("Error writing template to browser", err) } } |
RenderTemplate関数内では、キャッシュのON/OFFによって、読み込まれるテンプレートインスタンスを切り替えています。
handlers.goの書き換え
handlers.goは、全体的に書き換えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// handlers.go package handlers import ( "net/http" "helloworld/config" "helloworld/render" ) type Repository struct { App *config.AppConfig } func NewRepo(a *config.AppConfig) *Repository { return &Repository{ App: a, } } func (m *Repository) Home(w http.ResponseWriter, r *http.Request) { render.RenderTemplate(w, "home.page.tmpl") } func (m *Repository) About(w http.ResponseWriter, r *http.Request) { render.RenderTemplate(w, "about.page.tmpl") } |
ここでは「App *config.AppConfig」を特に使っていませんが、アプリケーション全体の設定という位置付けなので、情報として持たせています。
main.goの修正
main.goを以下のように修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
package main import ( "fmt" "helloworld/config" "helloworld/handlers" "helloworld/render" "log" "net/http" ) const portNumber = ":8080" func main() { var app config.AppConfig tc, err := render.CreateTemplateCache() if err != nil { log.Fatal("cannot create template") } app.TemplateCache = tc // キャッシュON app.UseCache = false repo := handlers.NewRepo(&app) render.NewTemplates(&app) http.HandleFunc("/", repo.Home) http.HandleFunc("/about", repo.About) fmt.Println(fmt.Printf("Starting appllication on port %s\n", portNumber)) _ = http.ListenAndServe(portNumber, nil) } |
ここでは、他のファイルに実装したNew関数を読み込んでいます。
そして、「app.UseCache = true」にすることでキャッシュが有効になります。
コメントを残す
コメントを投稿するにはログインしてください。