paint-brush
Some Things You Should Know About Maps in Goby@therealone
479 reads
479 reads

Some Things You Should Know About Maps in Go

by Denis LarionovAugust 18th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Once memory is allocated for a map it cannot be freed. Maps are always increasing in Go. They can never decrease. If you have an excessive amount of elements in a map and want to free space, you can create a new map, copy the elements into it, and there is no need to set the previous map to nil.
featured image - Some Things You Should Know About Maps in Go
Denis Larionov HackerNoon profile picture


I would like to provide some insights about optimizing code efficiency and memory usage by sharing facts about maps in Go.


Once memory is allocated for a map it cannot be freed

func mapDelete() {
	v := "some string"

	a := make(map[int]string)

	printMemStats("Before Map Add")

	for i := 0; i < 1000000; i++ {
		a[i] = v
	}

	runtime.GC()
	printMemStats("After Map Add 1000000")

	for i := 0; i < 1000000; i++ {
		delete(a, i)
	}

	runtime.GC()
	printMemStats("After Map Delete 1000000")

	fmt.Printf("%v", a)
}

func printMemStats(mag string) {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	fmt.Printf("%v:memory = %vKB, GC Times = %v\n", mag, m.Alloc/1024, m.NumGC)
}


Let`s run the code:

Before Map Add:memory = 154KB, GC Times = 0
After Map Add 1000000:memory = 56737KB, GC Times = 7
After Map Delete 1000000:memory = 56740KB, GC Times = 8
map[]


Wait what?


We delete all elements from the map, and now we can see it is empty but go allocates an extra 3KB. The thing is - the key and value are reset to zero when the delete operation on the map is executed, not freeing any space.


Maps are always increasing in Go. They can never decrease


If you have an excessive amount of elements in a map and want to free space, you can create a new map, copy the elements into it, and there is no need to set the previous map to nil.


The garbage collector will delete the map if it is not used again in the program:

func mapDeleteWithCopy() {
	v := "some string"

	a := make(map[int]string)

	printMemStats("Before Map Add")

	for i := 0; i < 1000000; i++ {
		a[i] = v
	}

	runtime.GC()
	printMemStats("After Map Add 100000")

	//Just copy the values to the new map
	b := make(map[int]string)
	for i := 0; i < 1000; i++ {
		b[i] = a[i]
	}

	runtime.GC()
	printMemStats("After New Map Copy 1000")

	fmt.Printf("%s", b[0])
}

func printMemStats(mag string) {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	fmt.Printf("%v:memory = %vKB, GC Times = %v\n", mag, m.Alloc/1024, m.NumGC)
}


As you can see, now we use much less memory:

Before Map Add:memory = 155KB, GC Times = 0
After Map Add 100000:memory = 56737KB, GC Times = 7
After New Map Copy 1000:memory = 218KB, GC Times = 8
some string



Use struct{} instead of bool as default value

Let`s compare the memory consumption of these two examples:

func boolDefault() {
	mp := make(map[int]bool)
	for i := 0; i < 10000000; i++ {
		mp[i] = true
	}

	printMemStats("Bool value map")
}
Bool value map:memory = 308583KB, GC Times = 8



func structDefault() {
	mp := make(map[int]struct{})
	for i := 0; i < 10000000; i++ {
		mp[i] = struct{}{}
	}

	printMemStats("Struct{} value map")
}
Struct{} value map:memory = 280515KB, GC Times = 8


As you can see, we saved 28 megabytes simply by replacing bool with struct{}. And in most cases, this will be enough for our code.


Maps are references

When you pass a map to a function or assign it to another variable, you are actually passing a reference to the underlying map data.


This means any changes made to the map inside the function will affect the original map:

func passByReference() {
	mp := make(map[int]string)

	mp[0] = "the dress is blue with black"

	changeElems(mp)

	fmt.Printf("%v", mp)
}

func changeElems(mp map[int]string) {
	mp[0] = "the dress is white with gold"
}


Running this code, we get:

map[0:the dress is white with gold]


As we can see the value of the original map is changed. But what if we need to pass the map as a copy, so the code will not change the original map? We need to copy it. But a simple copy will have no effect:

func copyMap() {
	mp := make(map[int]string)

	mp[0] = "the dress is blue with black"

	mp2 := mp

	changeElems(mp2)

	fmt.Printf("%v", mp)
}


The original map is changed:

map[0:the dress is white with gold]


What we need is deep copy:

func deepCopyMap() {
	mp := make(map[int]string)

	mp[0] = "the dress is blue with black"

	deepCopiedMap := make(map[int]string, len(mp))
	// Copy from the original map to the new map
	for key, value := range mp {
		deepCopiedMap[key] = value
	}

	changeElems(deepCopiedMap)

	fmt.Printf("%v", mp)
}


Now we can see, that the original map is not changed:

map[0:the dress is blue with black]


Conclusion

I tried to explore the interesting details about maps in Go, hoping that it will help you write code more efficiently. In the future, I plan to release more articles about this topic and other things that I find useful for writing efficient code.