Skip to content

Instantly share code, notes, and snippets.

@vindard
Last active April 21, 2021 19:44
Show Gist options
  • Save vindard/6cc0a91ff8a221db2d24167655edc304 to your computer and use it in GitHub Desktop.
Save vindard/6cc0a91ff8a221db2d24167655edc304 to your computer and use it in GitHub Desktop.

Working with map objects in GoLang

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).

2nd attempt: progress?

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.

3rd attempt: mixed success

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.

Success

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment