Go 1.23’s Iter package.

Writing a custom iterator that returns a map’s values in a constant order.

Cheikh seck
5 min read1 day ago

Maps are notoriously known for changing their iteration order during each range loop. “When iterating over a map with a range loop, the iteration order is not specified and is not guaranteed to be the same from one iteration to the next.” The solution proposed by the Go team was to define a separate data structure that explicitly specifies the order of iteration; this may be a slice or array with the keys in the desired order.

Source DALL-E

In this post, we’ll look at a recent approach to solving the iteration order problem; as well as understand how to define a really basic iterator with the iter package.

Overview

The problem at hand is that I have a map with scores tied to a unique user ID. The user IDs are defined in incremental nature, where the first ID would be 1 , the second 2 and so on.

For this solution, I will define a custom type with the underlying type being map[string]int. Since I’ve defined this type, I can define a method named SortedKeys that will return the Seq2 (Seek 2) type from the iter package. This method can then be passed as the range operand and thus guaranteeing the order of iteration on each call.

It’s important to note that the “tried and proven” solution of defining a separate data structure will be hidden within the SortedKeys method. Let’s start with the custom type.

CustomMapType

Prior to defining the type, I’ll need a utility function to sort the map keys into a string array. Since I’m not the first person to have this issue, I prompted Chat GPT to write this code for me. Here is the function outlined in this paragraph:

func SortMapKeysByNumericalOrder(m map[string]int) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}

sort.Slice(keys, func(i, j int) bool {
num1, _ := strconv.Atoi(keys[i])
num2, _ := strconv.Atoi(keys[j])
return num1 < num2
})

return keys
}

Function SortMapKeysByNumericalOrder will sort a map’s numerical string keys from lowest to greatest. The expectation here is that the map keys are strings that can be parsed as numbers with Go’s strconv package.

Next, I’ll define the custom map type:

type CustomMapType map[string]int

Just like that, I’ve defined a type that inherits all of the properties of a map. Go is great.

Next, I need to add the SortedKeys method. Here is the initial version of the method:

func (cm CustomMapType) SortedKeys() iter.Seq2[string, int] {
// this will return a compile error
}

Before going any further, let’s look at the Seq2 type from the iter package. Seq2 is a generic type that is ideal for maps as it can return the key and value of a map entry. Here is the type definition:

Seq2[K, V any] func(yield func(K, V) bool)

According to the type definition, the SortedKeys method should return the following func(yield func(string, int) bool) (to match the return type specified by the SortedKeys method). Here is the updated code:

func (cm CustomMapType) SortedKeys() iter.Seq2[string, int] {

return func(yield func(string, int) bool) {

}
}

Although the compilation issues are solved, the previous code will not return any values because I’m not calling the yield function. Here is the following code in action:

Yield

When I first saw the signature of the Seq2 type, I thought that Go had a new keyword. Turns out it's the name of the function used to send values back to the range loop (how cool is that!). Here is a revised version of the SortedKeys method with the yield function being called:

func (cm CustomMapType) SortedKeys() iter.Seq2[string, int] {

sortedKeys := SortMapKeysByNumericalOrder(cm)

return func(yield func(string, int) bool) {
for _, k := range sortedKeys {
// this will result in a compilation warning
// as the loop will never stop.
yield(k, cm[k])
}
}
}

In the revised code, I’m calling the utility function SortMapKeysByNumericalOrder to sort the keys and then using the yield function to return the values back to the range loop calling the SortedKeys method. The first parameter of the yield function becomes the key within the calling range loop, and the second parameter is the value.

However, we’re not out of the woods yet; this is due to the fact that my custom Seq2 function should return early when the value returned by the yield function is false. yield will return false if the range loop is interrupted and so, it is important to stop calling the yield function after the loop has exited. Here is the revised code:

func (cm CustomMapType) SortedKeys() iter.Seq2[string, int] {

sortedKeys := SortMapKeysByNumericalOrder(cm)

return func(yield func(string, int) bool) {
for _, k := range sortedKeys {
// yield function will run
// during cond. statement.
if !yield(k, cm[k]) {
return
}
}
}
}

Putting it together

With most aspects of the program defined, I’ll need a main function that ties the functionality together. The program will be simple, I will:

  • Initialize a variable with type CustomMapType and populate it with some scores.
  • Add a range loop with the SortedKeys method as the range operand and print each key-value pair to confirm if the map iteration order is consistent.

Here is the code for the main function:

func main() {

cm := CustomMapType{
"2": 500,
"1": 1000,
"3": 400,
"6": 300,
"0.1": 200,
}

for k, v := range cm.SortedKeys() {
fmt.Println(k, v)
}

}

Here is the full code in action:

Conclusion

The example provided in this post is quite simple and offers some insight into the yield function and how it passes values to a calling range loop. One of the benefits I see for library providers is it will give you more control over the experience you provide to your end users; it also abstracts away the code required to sort map keys into the desired order.

Another perceived benefit I can foresee is drastically reducing memory occupied by arrays; this can be achieved by fetching (during iteration) and passing memory intense values through the yield function. I have yet to test this theory.

Thank you for reading, you can find more information about the sources used for this post below:

Sources

Go maps in action (2013) — https://go.dev/blog/maps

Iter Package — https://pkg.go.dev/iter

Go Playground session — https://go.dev/play/p/ky36Y6KCf6R

--

--

Cheikh seck
Cheikh seck

Written by Cheikh seck

I write about Go. Hire me to write Go for you or your organization cheikhshift@proton.me

No responses yet