The Missing Structured Containers in Go
A few days ago, I welcomed the experiment of Rangefunc in Go. This sparked some interesting discussions with some fellow developers. As a Python developer for many years, I’ve come to appreciate the use of iterators. I really missed it in Go.
The Missing Ordering
Most languages handle maps/dictionaries or associative arrays similarly. However, as software continues to evolve, more languages are introducing the concept of a structured associative array. As an example, Python introduced some time ago ordered dictionaries in their collection types, Java uses SorterdMap, Rust uses BTreeMap Conversely, Go is lagging, which is understandable (sometimes) considering the language’s primary goal and focus.
The Problem
With modern programming’s increasing complexity, there is a natural demand for ordering among associative containers when necessary. For that reason, there has been an evolution of ordered containers in the Go community. The most prominent examples include OrderedMap and OrderedMap . To better illustrate the problem, let’s assume you are parsing an OpenAPI schema with the lovely libopenapi Go library. The library uses a custom ordered map, which is smooth until you need to range because, natively, this is the way.
func testOrderedMapIteration(oas *v3.Document) {
for pathName, pathItem := range oas.Paths.PathItems {
slog.Info("Introspecting path", "pathName", pathName, "pathItem", pathItem)
}
}
This example produces the following error:
cannot range over oas.Paths.PathItems (variable of type *"github.com/pb33f/libopenapi/orderedmap".Map[string, *"github.com/pb33f/libopenapi/datamodel/high/v3".PathItem])
Considering Go’s compiled nature, the above is expected, but it does not make sense, especially if you need a range-based third-party solution. This is already a map, with the only difference from a normal map being ordered. You must create a slice to maintain order using range (e.g., templates).
func GetPaths(oasPaths *v3.Paths) []map[string]*v3.PathItem {
paths := []map[string]*v3.PathItem{}
for path := oasPaths.PathItems.First(); path != nil; path = path.Next() {
paths = append(paths, map[string]*v3.PathItem{
path.Key(): path.Value(),
})
}
return paths
}
And then iterate:
func testOrderedMapIteration(oas *v3.Paths) {
for _, paths := range TestPaths(oas.Paths) {
for pathName, path := range paths {
slog.Info("Path name", "pathName", pathName)
slog.Info("Path summary", "summary", path.Summary)
}
}
}
The solution
One solution to the problem is introducing ordered maps/structures within Go, yet the problem is simplified with the introduction of yield in Go.
func GetPaths(oasPaths *v3.Paths) func(yield func(string, *v3.PathItem) bool) {
iter := func(yield func(string, *v3.PathItem) bool) {
for path := oas.Model.Paths.PathItems.First(); path != nil; path = path.Next() {
if !yield(path.Key(), path.Value()) {
return
}
}
}
return iter
}
yield
removes the requirement of converting the ordered map to a slice to iterate over it.
Conclusion
The dynamic nature of interfaces is changing the world, and languages should be open to adopting this, even partially because of their static nature.
Introducing ordered structures in Go would be more efficient than “abusing” the introduction of yield
, as most cases require the ability to iterate over structured containers instead of repeating boilerplate code.