I find Go templates to be very confusing. Maybe I am dumb, but I constantly find it difficult to get my html/templates
working properly in a web application. There are a ton of stupid tutorials on the Internet, but they all fail to cover some basic cases:
ParseFiles()
) while handling a request. Good for dev debugging, but there must be another way.To explain the second one a bit more, when you have a template language that has an "include" function for loading more templates you could use this pattern:
In header.tmpl
:
<head>
<title>Boo!</title>
</head>
<body>
<div id="main">
In footer.tmpl
:
</div>
</body>
In page1.tmpl
:
[ include "header.tmpl" ]
<h1>Page 1 main content</h1>
[ include "footer.tmpl" ]
In page2.tmpl
:
[ include "header.tmpl" ]
<h1>Page 2 main content</h1>
[ include "footer.tmpl" ]
You get to re-use the header and footer templates, but see how the <div id="main">
is split between the header and footer? If you change the header to increase the depth of the main content you must remember to also change the footer. A much better pattern would be:
In layout.tmpl
:
<head>
<title>Boo!</title>
</head>
<body>
<div id="main">
[ include "content.tmpl" ]
</div>
</body>
However you want content.tmpl
to have different content depending on the url... Perhaps you can make this a variable, if supported by the templating language:
<head>
<title>Boo!</title>
</head>
<body>
<div id="main">
[ include ${content} ]
</div>
</body>
Then in your routes you set $content
depending on what page you are on. But if your template language doesn't support this kind of thing then you are forced to render your $content
into a variable, and then pass that variable into the layout.tmpl
template as a value: YUCK!!
Let's see if we can just get a really basic template running with a test.html
file:
<head>
<title>Hello!</title>
</head>
<body>
<h1>Hello!</h1>
</body>
And a main.go
:
package main
import (
"os"
"html/template"
)
func main() {
t := template.Must(template.New("name").ParseFiles("test.html"))
err = t.Execute(os.Stdout, nil)
if err != nil {
panic(err)
}
}
And when you run it:
panic: template: "name" is an incomplete or empty template
goroutine 1 [running]:
main.main()
/go-templates/main.go:15 +0xee
exit status 2
Wait, what?? My code looks like every other tutorial example out there... Let's drop the template.New("name")
part:
package main
import (
"os"
"html/template"
)
func main() {
t := template.Must(template.ParseFiles("test.html"))
err := t.Execute(os.Stdout, nil)
if err != nil {
panic(err)
}
}
Ok that worked. So what's the difference between these two:
template.New("name").ParseFiles("test.html")
template.ParseFiles("test.html")
The documentation for html/template
seems to skip a lot of information, until you realise that html/template
has the same API as text/template
. Sure enough, text/template
documentation covers all the basics. Somewhere in that documentation (fairly far down the page) we see this:
Associated templates
Each template is named by a string specified when it is created. Also, each
template is associated with zero or more other templates that it may invoke
by name; such associations are transitive and form a name space of
templates.
A template may use a template invocation to instantiate another associated
template; see the explanation of the "template" action above. The name must
be that of a template associated with the template that contains the
invocation.
Which reads like it was written by a robot, but let's try to unpack it a bit:
{{define "name"}} ... {{end}}
, and "call" them within templates {{template "name"}}
A little bit down we see this great example:
{{define "T1"}}ONE{{end}}
{{define "T2"}}TWO{{end}}
{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
{{template "T3"}}
So internally in the template object we can see names being defined (T1
,T2
..) and then rendered by name. Let's play with this a bit by putting this into a file define.tmpl
, and trying a few things:
t := template.Must(template.ParseFiles("define.tmpl"))
t.Execute(os.Stdout, nil)
Prints ONE TWO
as expected. Now let's do something a bit weirder using ExecuteTemplate
:
t := template.Must(template.ParseFiles("define.tmpl"))
err := t.ExecuteTemplate(os.Stdout, "T1", nil)
Prints ONE
only and ignores the rest of the templates! So we start to understand a little bit more about what is going on internally. What if we try to go back to our original example and try to get it to work:
t := template.Must(template.New("name").ParseFiles("test.html"))
err = t.ExecuteTemplate(os.Stdout, "test.html", nil)
So this works... But still, what is the purpose of using template.New("name")
at all? The answer can be seen with this example template.tmpl
:
{{define "banana"}}
BANANA!
{{end}}
{{define "hurrah"}}
HURRAH!
{{end}}
Hmmm, hello...
And this main:
func main() {
t1 := template.Must(template.New("banana").ParseFiles("template.tmpl"))
t1.Execute(os.Stdout, nil)
t2 := template.Must(template.New("hurrah").ParseFiles("template.tmpl"))
t2.Execute(os.Stdout, nil)
t3 := template.Must(template.New("banana").ParseFiles("template.tmpl"))
t3.ExecuteTemplate(os.Stdout, "hurrah", nil)
t4 := template.Must(template.New("banana").ParseFiles("template.tmpl"))
t4.ExecuteTemplate(os.Stdout, "template.tmpl", nil)
}
Prints this (extra newlines removed):
BANANA!
HURRAH!
HURRAH!
Hmmm, hello...
Do some guru-meditation on that and you see:
template.New("name")
means "name" will be the default executed template when you use Execute()
ExecuteTemplate()
and passing a nameParseFiles("template.html")
creates an internal name template.html
representing the parts outside a define
If you visualise the internal state of the t1
template object it might look something like this:
t1 == {
"default_template": "banana",
"chunks": {
"banana": "BANANA!",
"hurrah": "HURRAH!",
"template.tmpl": "Hmmm, hello...",
}
}
Probably an incorrect model, but more or less. Regardless, passing a different name to New()
just changes the default_template
. Assuming all that is true we should be able to break a template object by having multiple definitions of the same thing:
file1.tmpl:
{{define "banana"}}
file1 banana
{{end}}
file2.tmpl:
{{define "banana"}}
file2 banana
{{end}}
With:
t1 := template.Must(template.New("banana").ParseFiles("file1.tmpl, "file2.tmpl"))
t1.Execute(os.Stdout, nil)
Prints:
file2 banana
Either my internal mental model is wrong, or file2.tmpl
just silently overrides the definition of "banana". This can be confirmed by modifying file1.tmpl
slightly:
{{define "banana"}}
file1 banana
{{end}}
{{template "banana"}}
And changing the code:
t1 := template.Must(template.New("banana").ParseFiles("file1.tmpl, "file2.tmpl"))
t1.ExecuteTemplate(os.Stdout, "file1.tmpl", nil)
Prints:
file2 banana
Can you see why? Guru-meditation if not.
Phew! With a much better understanding of how the template system works, is it possible to get to the ideal:
<head>
<title>Boo!</title>
</head>
<body>
<div id="main">
[ different content template per page ]
</div>
</body>
Actually, none of what we discovered helps here. It does not seem possible to do this:
{{template $name}}
Where name is a variable passed in. If we try something else:
In page1.tmpl
:
{{define "content"}}
Page 1 content
{{end}}
In page2.tmpl
:
{{define "content"}}
Page 2 content
{{end}}
In layout.tmpl
:
<body>
{{template "content"}}
</body>
If we ParseFiles("page1.tmpl", "page2.tmpl", "layout.tmpl")
then the page2 content will override page1 :-/. There is only one thing for it unfortunately: have multiple template objects:
templates := make(map[string]*template.Template)
templates["page1"] = template.ParseFiles("page1.tmpl", "layout.tmpl");
templates["page2"] = template.ParseFiles("page2.tmpl", "layout.tmpl");
Then, later depending on the page:
templates["page1"].ExecuteTemplate(os.Stdout, "layout.tmpl", nil)
Most of the bullshit you read in the Internet about Go templates just completely gloss over these details. I suspect the authors of these "tutorial" pages don't actually understand what is happening and are just after the clicks. The Go documentation for html/template
hints that you should read text/template
but it should probably be more specific about that. It is also not that great at explaining what is going on internally.