This short post is to explore a sticking point I came across when trying to implement a familiar pattern (from Python) in GoLang. In a nutshell, the idea was to loop over a map data structure to execute some lines of code instead of manually duplicating these lines.
var (
homeTemplate *template.Template
contactTemplate *template.Template
)
homeTemplate, err = template.ParseFiles("views/home.gohtml")
if err != nil {
panic(err)
}
contactTemplate, err = template.ParseFiles("views/contact.gohtml")
if err != nil {ParseFiles
panic(err)
}
From working with Python, my first temptation would be to do something like the following:
var (
homeTemplate *template.Template
contactTemplate *template.Template
)
// . . .
// Handler functions declared here using
// declared template.Template variables
// . . .
var viewsMap = map[string]*template.Template{
"views/home.gohtml": homeTemplate,
"views/contact.gohtml": contactTemplate,
}
for viewPath, templateObj := range viewsMap {
templateObj, err = template.ParseFiles(viewPath)
if err != nil {
panic(err)
}
}
Right off the bat this didn't work because GoLang threw a declared but not used
error for the templateObj
variable I unpacked from the map via the range
keyword.
Original question: variables are unpacked by value in a for-range loop on a map; and they somehow aren't intended to be assigned to?
Eventual answer: nope, this error was because the
templateObj
variable wasn't being used outside of it just being assigned and re-assigned to
This meant that I couldn't use the values via an unpacked variable directly the templateObj
variable was being instantiated as a new reference inside the loop, was being re-assigned, and then was garbage collected once the loop went out of scope without ever updating the values in the original map (makes sense because of lexical scoping and passing-by-value).
My second idea was to not unpack the map values, and to simply use them in place by referencing them using their keys.
var (
homeTemplate *template.Template
contactTemplate *template.Template
)
// . . .
// Handler functions declared here using
// declared template.Template variables
// . . .
var viewsMap = map[string]*template.Template{
"views/home.gohtml": homeTemplate,
"views/contact.gohtml": contactTemplate,
}
for viewPath := range viewsMap {
viewsMap[viewPath], err = template.ParseFiles(viewPath)
if err != nil {
panic(err)
}
}
This got a bit further along since I no longer had the declared but not used
error.
It broke later on though since the map got created by values instead of references to the objects. This meant that the ParseFiles
call wrote values to the map's version of templates and the originally initialized objects were left as nil
in the actual handler functions.
I then decided to just work with the map's version of template objects and not use previously declared template objects.
var viewsMap = map[string]*template.Template{
"views/home.gohtml": &template.Template{},
"views/contact.gohtml": &template.Template{},
}
// . . .
// Handler functions declared here using
// map key references to template.Template objects
// . . .
for viewPath := range viewsMap {
viewsMap[viewPath], err = template.ParseFiles(viewPath)
if err != nil {
panic(err)
}
}
This finally worked! It meant though that the map was now the source-of-truth for template objects instead of independently declared global variables.
It still nagged me that I couldn't figure out how to get my first version of things working to simply roll the 2 separate ParseFiles
blocks into a single map and for-range loop without touching any of the other code.
On reviewing one solution to my problem shared by @inancgumus I noticed a snippet of code where he declared a map's value type he was using as a pointer and then dereferenced the value to assign it within a for-range loop. Since the values I was using (*template.Template
) were already pointers it wasn't immediately clear to me how I could dereference these to assign further values to them until I decided to try doing pointers to these already-pointers.
var (
homeTemplate *template.Template
contactTemplate *template.Template
)
// . . .
// Handler functions declared here using
// declared template.Template variables
// . . .
var viewsMap = map[string]**template.Template{
"views/home.gohtml": &homeTemplate,
"views/contact.gohtml": &contactTemplate,
}
for viewPath, templateObj := range viewsMap {
*templateObj, err = template.ParseFiles(viewPath)
if err != nil {
panic(err)
}
}
This change worked perfectly and allowed me to achieve exactly what I was originally trying to do.
Insight: pointers to objects that are already pointers (a double-asterisk) are possible
Insight: to assign new values to objects in a
map
, pass them as references instead and do the assignment to their memory location (dereference them)
So it looks like the pattern I was originally trying to apply from coding in Python did end up working exactly as expected, except that I needed to know a bit more about pointers, dereferencing and passing variables around in data structures before I was able to finally replicate it properly.
Caveat: this was an exercise more in understanding how the Go language structure and syntax works, and less about writing conventionally acceptable Go code.