Golang Maps

Using Maps in Golang

In golang, the map type is an unordered collection of key-values and is commonly referred to as a hash table, associative array, or dictionary.

Maps have the following properties:

  • Each key can exist only once (no duplicates)
  • Keys are unordered (the order of keys may differ each time you access the map)
  • Key lookups and modifications are fast

Maps are a useful data structure as they allow us to store complex information in a way that can be easily and quickly created, accessed, and modified.

Creating Maps

Nil Maps

The simplest map you can create is an uninitialized nil map:

var myMap map[string]string


However, if you try to assign a new key-value to an uninitialized nil map, you will get an error:

var myMap map[string]string
myMap["hi"] = "hello"
panic: assignment to entry in nil map


While you cannot assign a new map key to the nil map, you can overwrite the map by assigning a new map literal to the variable:

var myMap map[string]string
myMap = map[string]string{
    "hi": "hello",
}
fmt.Println(myMap)
map[hi:hello]

Empty Maps

nil maps are not useful if you wish to add or modify keys in more than one operation. Because of this, it is common to initialize an empty non nil map that will let you assign keys and values. There are a couple of options for this: make, and map literals:

// using make
myMap := make(map[string]string)

// using map literal
myMap := map[string]string{}

myMap["hi"] = "hello"
fmt.Println(myMap)
map[hi:hello]

Map Literals

Map literals are a popular option because they allow you to initialize a map and populate it in one command. You can then add or modify keys directly:

myMap := map[string]string{
    "hi": "hello",
}
myMap["bye"] = "goodbye"
fmt.Println(myMap)
map[bye:goodbye hi:hello]

Map primitives

Maps support any combination of golang primitives:

mapBoolBool := map[bool]bool{
    true: true,
    false: false,
}
fmt.Println("mapBoolBool:", mapBoolBool)

mapBoolInt := map[bool]int{
    true: 1,
    false: 0,
}
fmt.Println("mapBoolInt:", mapBoolInt)

mapBoolString := map[bool]string{
    true: "yes",
    false: "no",
}
fmt.Println("mapBoolString:", mapBoolString)

mapIntBool := map[int]bool{
    1: true,
    2: false,
    3: true,
}
fmt.Println("mapIntBool:", mapIntBool)

mapIntInt := map[int]int{
    1: 6,
    2: 9,
    3: 1,
}
fmt.Println("mapIntInt:", mapIntInt)

mapIntString := map[int]string{
    506: "five hundred six",
    301: "three hundred one",
}
fmt.Println("mapIntString:", mapIntString)

mapStringBool := map[string]bool{
    "yes": true,
    "no": false,
    "yup": true,
    "nope": false,
}
fmt.Println("mapStringBool:", mapStringBool)

mapStringInt := map[string]int{
    "one": 1,
    "two": 2,
}
fmt.Println("mapStringInt:", mapStringInt)
mapBoolBool: map[false:false true:true]
mapBoolInt: map[false:0 true:1]
mapBoolString: map[false:no true:yes]
mapIntBool: map[1:true 2:false 3:true]
mapIntInt: map[1:6 2:9 3:1]
mapIntString: map[301:three hundred one 506:five hundred six]
mapStringBool: map[no:false nope:false yes:true yup:true]
mapStringInt: map[one:1 two:2]

Nested maps and interface{}

Often, it is necessary to have nested maps where the value for a key is a map itself:

mapStringMapStringString := map[string]map[string]string{
    "key1" : map[string]string{
        "subkey1": "subvalue1",
        "subkey2": "subvalue2",
    },
    "key2" : map[string]string{
        "subkey1": "subvalue1",
        "subkey2": "subvalue2",
    },   
}
fmt.Println(mapStringMapStringString)
map[key1:map[subkey1:subvalue1 subkey2:subvalue2] key2:map[subkey1:subvalue1 subkey2:subvalue2]]


However, there are a number of problems with the approach above:

  • The long winded map notation would get pretty unwieldy if you had multi-level nested maps (e.g. map[string]map[string]map[string]map[string]string)
  • This schema is very rigid - what if you have a mixture of values where some are string, some are int, some are slices, and some are map?
  • What if you are storing a map where you don’t know the structure ahead of time or it changes?

For these reasons, you will generally see the use of the empty interface{} because all golang primitives and data structures satisfy the empty interface:

// a mixture of different value types
myMap := map[string]interface{}{
    "key1": "value1", // string
    "key2": 2, // int
    "key3": map[string]string{ // map
        "subkey1": "subvalue1",
    },
    "key4": []string{ // slice
        "one",
        "two",
        "three",
    },
}
fmt.Println(myMap)
map[key1:value1 key2:2 key3:map[subkey1:subvalue1] key4:[one two three]]

Type Assertions

It is important to note that if you do decide to use interface{}, you will not be able to perform any operations with variables of a different type unless you use type assertions:

myMap := map[string]interface{}{
    "key1": "value1",
    "key2": 2,
}

// attempt to perform arithmetic without type assertion
numberOne := myMap["key2"] // interface type
var numberTwo int = 2 // int type
myAnswer := numberOne + numberTwo
invalid operation: numberOne + numberTwo (mismatched types interface {} and int)

myMap := map[string]interface{}{
    "key1": "value1",
    "key2": 2,
}

// attempt to perform arithmetic with type assertion
numberOne := myMap["key2"] // interface type
var numberTwo int = 2 // int type
myAnswer := numberOne.(int) + numberTwo
fmt.Println("myAnswer", myAnswer)
myAnswer 4

Reading Maps

Key Lookup

Once you’ve created a map, you can access lookup keys and values by key name:

myMap := map[string]interface{}{
    "key1": "value1",
    "key2": 2,
}
fmt.Println("The value of key1 is", myMap["key1"])
fmt.Println("The value of key2 is", myMap["key2"])
The value of key1 is value1
The value of key2 is 2

Iteration

A common pattern with maps is to iterate through the keys in order to read values. Note that the returned keys in the output are unordered:

myMap := map[string]interface{}{
    "key1": "value1",
    "key2": 2,
    "key3": map[string]string{
        "subkey1": "subvalue1",
    }, 
    "key4": []string{
        "one",
        "two",
        "three",
    },
}

for key,value := range myMap {
    fmt.Println("key is", key)
    fmt.Println("value is", value)
}
key is key3
value is map[subkey1:subvalue1]
key is key4
value is [one two three]
key is key1
value is value1
key is key2
value is 2

Checking for Key

If a key does not exist, then golang will assign the “default” value for that type to the value for the key:

myBoolMap := map[string]bool{
    "key1": true,
}

myIntMap := map[string]int{
    "key1": 1,
}

myStringMap := map[string]string{
    "key1": "value1",
}

fmt.Println("myBoolMap key2:", myBoolMap["key2"]) // default bool is false
fmt.Println("myIntMap key2:", myIntMap["key2"]) // default int is 0
fmt.Println("myStringMap key2:", myStringMap["key2"]) // default string is ""
myBoolMap key2: false
myIntMap key2: 0
myStringMap key2: 


To check if a key exists, you can use the map lookup syntax:

myBoolMap := map[string]bool{
    "key1": true,
}

myIntMap := map[string]int{
    "key1": 1,
}

myStringMap := map[string]string{
    "key1": "value1",
}

var value interface{}
var ok bool

value, ok = myBoolMap["key1"]
fmt.Println("does myBoolMap key1 exist?", ok)
fmt.Println("myBoolMap key1:", value)

value, ok = myIntMap["key1"]
fmt.Println("does myIntMap key1 exist?", ok)
fmt.Println("myIntMap key1:", value)

value, ok = myStringMap["key1"]
fmt.Println("does myStringMap key1 exist?", ok)
fmt.Println("myStringMap key1:", value)

value, ok = myBoolMap["key2"]
fmt.Println("does myBoolMap key2 exist?", ok)
fmt.Println("myBoolMap key2:", value)

value, ok = myIntMap["key2"]
fmt.Println("does myIntMap key2 exist?", ok)
fmt.Println("myIntMap key2:", value)

value, ok = myStringMap["key2"]
fmt.Println("does myStringMap key2 exist?", ok)
fmt.Println("myStringMap key2:", value)
does myBoolMap key1 exist? true
myBoolMap key1: true
does myIntMap key1 exist? true
myIntMap key1: 1
does myStringMap key1 exist? true
myStringMap key1: value1
does myBoolMap key2 exist? false
myBoolMap key2: false
does myIntMap key2 exist? false
myIntMap key2: 0
does myStringMap key2 exist? false
myStringMap key2: 

Modifying Maps

Key reference

You can modify a key value directly and/or add new ones:

myMap := map[string]bool{
    "key1": false,
}

fmt.Println("key1:", myMap["key1"])

myMap["key1"] = true
myMap["key2"] = false
fmt.Println("key1:", myMap["key1"])
fmt.Println("key2:", myMap["key2"])
key1: false
key1: true
key2: false

Iteration

We can also iterate to modify and/or create keys:

myMap := map[string]bool{
    "key1": true,
}

for i := 1; i <= 3; i++ {
    keyName := fmt.Sprintf("key%d", i)
    myMap[keyName] = false
}

for key,value := range myMap {
    fmt.Println("key is", key)
    fmt.Println("value is", value)
}
key is key2
value is false
key is key3
value is false
key is key1
value is false

Additional Reading


https://bitfieldconsulting.com/golang/map-string-interface

https://golangdocs.com/maps-in-golang

https://yourbasic.org/golang/maps-explained/