Skip to main content
  1. Articles/

Golang Release 1.21: maps

·1573 words·8 mins· loading · loading ·
Marko Milojevic
Author
Marko Milojevic
Software engineer and architect. Golang and LLM enthusiast. Awful chess player, gym rat, harmonica newbie and cat lover.
New features in Golang - This article is part of a series.
Part 1: This Article

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
#

New features in Golang - This article is part of a series.
Part 1: This Article

Related

Golang Tutorial: Contract Testing with PACT

·2678 words·13 mins· loading · loading
My favorite part of software development is writing tests, whether they are unit tests or integration tests. I enjoy the process immensely. There’s a certain satisfaction in creating a test case that uncovers a function’s failure. It brings me joy to discover a bug during development, knowing that I’ve fixed it before anyone encounters it in a test environment or, worse, in production. Sometimes, I stay up late just to write more tests; it’s like a hobby. I even spent around 30 minutes on my wedding day writing unit tests for my personal project, but don’t tell my wife! The only thing that used to bother me was dealing with integration issues between multiple Microservices. How could I ensure that two Microservices, each with specific versions, wouldn’t face integration problems? How could I be certain that a new version of a Microservice didn’t break its API interface, rendering it unusable for others? This information was crucial to have before launching extensive scenarios in our end-to-end testing pipeline. Otherwise, we’d end up waiting for an hour just to receive feedback that we’d broken the JSON schema. Then, one day in the office, I heard a rumor that we were planning to use Contract Testing. I quickly checked the first article I found, and I was amazed. It was a breakthrough. Contract Testing # There are many excellent articles about Contract testing, but the one I like the most is from Pactflow. Contract testing ensures that two parties can communicate effectively by testing them in isolation to verify if both sides support the messages they exchange. One party, known as the Consumer, captures the communication with the other party, referred to as the Provider, and creates the Contract. This Contract serves as a specification for the expected requests from the Consumer and the responses from the Provider. Application code automatically generates Contracts, typically during the unit testing phase. Automatic creation ensures that each Contract accurately reflects the latest state of affairs. Contract testing After the Consumer publishes the Contract, the Provider can use it. In its code, likely within unit tests, the Provider conducts Contract verification and publishes the results. In both phases of Contract testing, we work solely on one side, without any actual interaction with the other party. Essentially, we are ensuring that both parties can communicate with each other within their separate pipelines. As a result, the entire process is asynchronous and independent. If either of these two phases fails, both the Consumer and Provider must collaborate to resolve integration issues. In some cases, the Consumer may need to adapt its integrational code, while in others, the Provider may need to adjust its API.

Golang Tutorial: Generics with Gorm

·1741 words·9 mins· loading · loading
After months and years of talking, trying things out, and testing, we’ve finally reached a big moment in our favorite programming language. The new Golang version, 1.18, is here. We knew it would bring significant changes to Go’s codebase, even before Generics was officially released. For a long time, when we wanted to make our code more general and abstract, we used code generators in Go. Learning what the “Go way” of doing things was challenging for many of us, but it also led to many breakthroughs. It was worth the effort. Now, there are new possibilities on the horizon. Many new packages have emerged, giving us ideas on how we can improve the Go ecosystem with reusable code that makes life easier for all of us. This inspiration led me to create a small proof of concept using the Gorm library. Now, let’s take a look at it. Source code # When I wrote this article, it relied on a GitHub Repository. The code served as a Go library proof of concept, with my intention to continue working on it. However, it was not yet suitable for production use, and I had no plans to offer production support at that time. You can find the current features by following the link, and below, there is a smaller sample snippet. Example Usage package main import ( "github.com/ompluscator/gorm-generics" // some imports ) // Product is a domain entity type Product struct { // some fields } // ProductGorm is DTO used to map Product entity to database type ProductGorm struct { // some fields } // ToEntity respects the gorm_generics.GormModel interface func (g ProductGorm) ToEntity() Product { return Product{ // some fields } } // FromEntity respects the gorm_generics.GormModel interface func (g ProductGorm) FromEntity(product Product) interface{} { return ProductGorm{ // some fields } } func main() { db, err := gorm.Open(/* DB connection string */) // handle error err = db.AutoMigrate(ProductGorm{}) // handle error // initialize a new Repository with by providing // GORM model and Entity as type repository := gorm_generics.NewRepository[ProductGorm, Product](db) ctx := context.Background() // create new Entity product := Product{ // some fields } // send new Entity to Repository for storing err = repository.Insert(ctx, &product) // handle error fmt.Println(product) // Out: // {1 product1 100 true} single, err := repository.FindByID(ctx, product.ID) // handle error fmt.Println(single) // Out: // {1 product1 100 true} } Why have I picked ORM for PoC? # Coming from a background in software development with traditional object-oriented programming languages like Java, C#, and PHP, one of the first things I did was search Google for a suitable ORM for Golang. Please forgive my inexperience at the time, but that’s what I was expecting. It’s not that I can’t work without an ORM. It’s just that I don’t particularly like how raw MySQL queries appear in the code. All that string concatenation looks messy to me. On the other hand, I always prefer to dive right into writing business logic, with minimal time spent thinking about the underlying data storage. Sometimes, during the implementation, I change my mind and switch to different types of storage. That’s where ORMs come in handy.

Golang Tutorial: Unit Testing with Mocking

·3513 words·17 mins· loading · loading
Unit testing has always been my thing, almost like a hobby. There was a time when I was obsessed with it, and I made sure that all my projects had at least 90% unit test coverage. You can probably imagine how much time it can take to make such a significant change in the codebase. However, the result was worth it because I rarely encountered bugs related to business logic. Most of the issues were related to integration problems with other services or databases. Adding new business rules was a breeze because there were already tests in place to cover all the cases from before. The key was to ensure that these tests remained successful in the end. Sometimes, I didn’t even need to check the entire running service; having the new and old unit tests pass was sufficient. Once, while working on a personal project, I had to write unit tests to cover numerous Go structs and functions—more than 100 in total. It consumed my entire weekend, and late on a Sunday night, before heading out on a business trip the next day, I set an alarm clock to wake me up. I had hardly slept that night; it was one of those restless nights when you dream but are also aware of yourself and your surroundings. My brain was active the entire time, and in my dreams, I kept writing unit tests for my alarm clock. To my surprise, each time I executed a unit test in my dream, the alarm rang. It continued ringing throughout the night. And yes, I almost forgot to mention, for two years, we had zero bugs in production. The application continued to fetch all the data and send all the emails every Monday. I don’t even remember my Gitlab password anymore. Unit Testing and Mocking (in general) # In Martin Fowler’s article, we can identify two types of unit tests: Sociable unit tests, where we test a unit while it relies on other objects in conjunction with it. For example, if we want to test the UserController, we would test it along with the UserRepository, which communicates with the database. Solitary unit tests, where we test a unit in complete isolation. In this scenario, we would test the UserController, which interacts with a controlled, mocked UserRepository. With mocking, we can specify how it behaves without involving a database.