I recently had to use a bunch of nested maps in Go and struggled way too long to get them “working”. And obviously there wasn’t a lot of good information to be found via $popular_search_engine. So i thought this might make a good blog post.
What gave me the proper nudge in the correct direction of thinking correctly was a Google Groups posting from 2012: https://groups.google.com/g/golang-nuts/c/PoRkoN84KKU/m/7330L9NulHQJ
TL;DR
You need to actually initialize every inner map seperatly if you want to use it! For more details you should read the rest ;-)
A simple example and the attempt of an explaination
Lets create a simple example to demonstrate the issue at hand:
$ go mod init nested_maps
go: creating new go.mod: module nested_maps
$ touch nested_maps.go
Now open nested_maps.go
in your favorite editor and paste in the following piece of code:
package main
import (
"log"
"encoding/json"
)
type people struct {
Group string
Name string
Age string
}
type groups struct {
Groups []people
}
func main() {
var a = []byte(`{"Groups":[
{"Group": "first", "Name": "Stanislaus", "Age": "23"},
{"Group": "second", "Name": "Mechthild", "Age": "13"},
{"Group": "third", "Name": "Gunhilde", "Age": "42"}
]}`)
var peopleA groups
err := json.Unmarshal(a, &peopleA)
if err != nil {
log.Fatal(err)
}
// here we assume at first that we create our nested map
jsonData := make(map[string]map[string]string)
for _, v := range peopleA.Groups {
jsonData[v.Group][v.Name] = v.Age
}
}
What we try to do here:
jsonData := make(map[string]map[string]string)
for _, v := range peopleA.Groups {
jsonData[v.Group][v.Name] = v.Age
}
is to put a JSON array, where each array element contains three key-value pairs that define a group name, the persons name and their age, into a nested map. The intention for this might be that we think processing the data via a nested map is nice. Or whatever.
While this compiles perfectly fine, running the created binary will result in this runtime error:
$ ./nested_maps
panic: assignment to entry in nil map
goroutine 1 [running]:
main.main()
/home/flart/temp/nested_maps/nested_maps.go:34 +0x1e8
My n00bish reaction at first was “WTF? I created the map with make()
. It’s not nil!”
But obviously i was terribly wrong.
What happens here is that Go creates the outer map but nothing else. And this makes perfect sense since it can’t possibly know how many inner maps we might want later on. It’s our job to take care of that.
A solution for that is extending our for
loop like this:
// here's our nested map
jsonData := make(map[string]map[string]string)
for _, v := range peopleA.Groups {
// here we check if the inner map exists
_, ok := jsonData[v.Group]
if !ok {
// if the inner map doesn't exist, we create it with make()
jsonData[v.Group] = make(map[string]string)
}
// now we can safely fill up the nested map
jsonData[v.Group][v.Name] = v.Age
}
The comments hopefully speak for themselves, but anyways:
What we added is a simple check if the inner map to which we want write data already exists.
This is done with a simple map lookup, which in Go returns not only the potential value
but also a boolean value on which we can do a simple check with if
. So if we get a false
back, we just create the inner map with a make()
and all is good.
Here is the full example which executes without a runtime error:
package main
import (
"log"
"encoding/json"
)
type people struct {
Group string
Name string
Age string
}
type groups struct {
Groups []people
}
func main() {
var a = []byte(`{"Groups":[
{"Group": "first", "Name": "Stanislaus", "Age": "23"},
{"Group": "second", "Name": "Mechthild", "Age": "13"},
{"Group": "third", "Name": "Gunhilde", "Age": "42"}
]}`)
var peopleA groups
err := json.Unmarshal(a, &peopleA)
if err != nil {
log.Fatal(err)
}
// here's our nested map
jsonData := make(map[string]map[string]string)
for _, v := range peopleA.Groups {
// here we check if the inner map exists
_, ok := jsonData[v.Group]
if !ok {
// if the inner map doesn't exist, we create it with make()
jsonData[v.Group] = make(map[string]string)
}
// now we can safely fill up the nested map
jsonData[v.Group][v.Name] = v.Age
}
}
Hope this helps somebody else at some point in the future :-)