Golang Release 1.21: maps

- 8 minutes read - 1568 words

New package in Go standard library that makes easier to work with maps.

Not too long ago, we witnessed a new release of our favorite programming language. The Go team didn’t disappoint us once again. They introduced numerous new features, including updates to the tool command to support backward and forward compatibility. As always, the standard library has received new updates, and the first one we’ll explore in this article is the new maps package.

The new package offers only five new functions (two additional ones were removed from the package: Values and Keys), but they provide significant value. All of them rely on Generics, a feature introduced in Go version 1.18, which has opened up possibilities for many new features. The map package clearly provides new tools for Go maps. In this particular case, it introduces new functions for checking map equality, deleting items from maps, copying items into maps, and cloning maps.

Let’s dive into each of them.

Equal and EqualFunc

First, let’s examine the pair of functions used to check map equality: Equal and EqualFunc. The first one is a straightforward function that checks the equality of two provided maps as function arguments. The second one allows you to pass an additional argument that defines how you plan to examine the equality of values inside the maps. Here are their signatures:

Function Equal

func Equal[M1, M2 ~map[K]V, K, V comparable](m1 M1, m2 M2) bool

Function EqualFunc

func EqualFunc[M1 ~map[K]V1, M2 ~map[K]V2, K comparable, V1, V2 any](m1 M1, m2 M2, eq func(V1, V2) bool) bool

The Equal function is easier to understand. It simply defines two generic types, M1 and M2, which represent maps of two other generic types, K and V. Obviously, K is for map keys, and it allows any comparable value. The second type is V, representing map values, and it also allows being of a comparable type.

The EqualFunc function is slightly more complicated. First, it doesn’t assume that the values in the maps are of the same type, nor do they have to be comparable. For that reason, it introduces an additional argument, which is an equality function for the values in the maps. This way, we can compare two maps that have the same keys but not the same values, and we can define the logic for comparing if they are equal.

Simple usage of Equal function

first := map[string]string{
    "key1": "value1",
}
second := map[string]string{
    "key1": "value1",
}
fmt.Println(maps.Equal(first, second))
// Output:
// true

third := map[string]string{
    "key1": "value1",
}
fourth := map[string]string{
    "key1": "wrong",
}
fmt.Println(maps.Equal(third, fourth))
// Output:
// false

In the example above, there are no surprises. We use four maps to test the Equal function. In the first case, two maps are equal, but in the second case, their values are not the same. The following example is also easy.

Additional usage of Equal function

func main() {
	first := map[string]string{
		"key1": "value1",
	}
	second := map[string]string{
		"key1": "value1",
		"key2": "value2",
	}
	fmt.Println(maps.Equal(first, second))
	// Output:
	// false

	third := map[string]string{
		"key1": "value1",
	}
	fourth := map[string]string{
		"key1": string([]rune{'v', 'a', 'l', 'u', 'e', '1'}),
	}
	fmt.Println(maps.Equal(third, fourth))
	// Output:
	// true
}

But what will happen if we pass the second map whose types don’t match the types of the first one? Let us check that:

Function Equal and different types

first := map[string]string{
    "key1": "true",
}
second := map[string]interface{}{
    "key1": true,
}
fmt.Println(maps.Equal(first, second))
// Output:
// M2 (type map[string]interface{}) does not satisfy ~map[K]V

This case doesn’t even compile. In order to use the function Equal, we need to ensure that both maps are of the same types for their keys and values. And now, this is the case where we can employ the second function, EqualFunc:

Function EqualFunc and different types

first := map[string]string{
    "key1": "true",
    "key2": "7",
}
second := map[string]interface{}{
    "key1": true,
    "key2": 7,
}
fmt.Println(maps.EqualFunc(first, second, func(v1 string, v2 interface{}) bool {
    return v1 == fmt.Sprint(v2)
}))
// Output:
// true

With the EqualFunc function, we can provide a third argument, a functions that we can use to compare values of two maps. If the keys are equal (and all keys must be present in both maps), our equality function will be called with values belonging to the same key in both maps. Notice that the types of arguments in the equality function match the types of the maps’ values (v1 is of type string, like the values of the first map, and v2 is of type interface{}, like the values of the second map).

Now, with these two functions, we are in a position to check the equality of two maps using the standard algorithm (where both key and value pairs must be equal), or we can provide our own algorithm for checking values’ equality (as long as the keys are equal by type and value).

Clone and Copy

The next pair of functions we want to explore are Clone and Copy. Obviously, just by looking at their names, we can guess what they do: Clone creates an exact clone of the existing map, while Copy copies all key-value pairs from one map to another. Let’s examine their signatures:

Function Clone

func Clone[M ~map[K]V, K comparable, V any](m M) M

Function Copy

func Copy[M1 ~map[K]V, M2 ~map[K]V, K comparable, V any](dst M1, src M2)

The Clone function expects one argument that should be of type M, which is a map and returns a map of exactly the same type. This map type must have a key of type K, which is comparable, and can have any type V for values. The Copy function has the same expectations; it handles the M1 and M2 type, which both represent a map with K type for keys (comparable) and V type for values (any).

Let us now check the examples for Clone function:

Simple usage of function Clone

first := map[string]string{
    "key1": "value1",
    "key2": "value2",
}
cloned := maps.Clone(first)
fmt.Println(cloned)
// Output:
// map[key1:value1 key2:value2]

first["key1"] = "value1-change"
fmt.Println(first)
// Output:
// map[key1:value1-change key2:value2]

fmt.Println(cloned)
// Output:
// map[key1:value1 key2:value2]

In the code snippet above, we can see that the Clone function indeed creates a new instance of a map with the same underlying types for keys and values as the original one. To ensure that we do not get a reference to the original map (as all maps are passed as references), the example also confirms that the cloned map is completely independent of its original. We demonstrated this by changing the original map, which did not affect the cloned one.

Simple usage of function Copy

first := map[string]string{
    "key1": "value1-first",
    "key2": "value2-first",
}

second := map[string]string{
    "key1": "value1-second",
    "key3": "value3-second",
}
maps.Copy(second, first)

fmt.Println(second)
// Output:
// map[key1:value1-first key2:value2-first key3:value3-second]

In the example above, we can see how the Copy function works. It copies all key-value pairs from one map into the other. If both the source and destination maps have the same key, the value in the destination will be overridden by the value from the source map. Additionally, if the destination already contains some keys that are not defined in the source, their values will remain intact. The Copy function obviously relies on having both the source and destination maps of the same underlying types for keys and values, as copying data between incompatible types is not possible in Go.

DeleteFunc

The last function in the new map package is DeleteFunc. We can already assume what this method does by comparing its name to some of the functions we checked previously in the article. However, let’s first examine its signature:

Function DeleteFunc

func DeleteFunc[M ~map[K]V, K comparable, V any](m M, del func(K, V) bool)

In the previous example, the DeleteFunc function also handles the type M, which represents a map of key-value pairs, where the types are K for keys (comparable) and V for values (any). Additionally, besides expecting an argument of type M, it also requires a deletion function argument. This deletion function expects arguments of types K and V to determine if a map item should be deleted. Let’s examine the following example:

Simple usage of function DeleteFunc

holder := map[string]string{
    "key1": "value1",
    "key2": "value2",
    "key3": "wrong",
    "key4": "wrong",
}

maps.DeleteFunc(holder, func(k string, v string) bool {
    return v == "wrong"
})

fmt.Println(holder)
// Output:
// map[key1:value1 key2:value2]

As we can see in the example above, the deletion function criteria are based only on the values (in this case, if the value is equal to the string “wrong”). The result is the original map with all keys permanently deleted. But how can we manage to delete all keys? Perhaps something like the code snippet below:

Clear all with function DeleteFunc

holder := map[string]string{
    "key1": "value1",
    "key2": "value2",
    "key3": "wrong",
    "key4": "wrong",
}

maps.DeleteFunc(holder, func(string, string) bool {
    return true
})

fmt.Println(holder)
// Output:
// map[]

In this example, the deletion function simply returns true for all key-value pairs, effectively deleting all keys from the original map. Besides this approach, we can also clear complete map by using builtin clear function:

Clear function

holder := map[string]string{
    "key1": "value1",
    "key2": "value2",
    "key3": "wrong",
    "key4": "wrong",
}

clear(holder)

fmt.Println(holder)
// Output:
// map[]

Conclusion

New version of Golang, 1.21, delivered many new updates, affecting standard library as well. In this article we checked how functions from maps packages work. Those new methods give us now possibility to easily check equality of maps, clone and copy them, and delete their items.

Useful Resources

comments powered by Disqus