Open-source project
elliotchance/orderedmap avatar
elliotchance/orderedmap

orderedmap keeps insertion order without giving up map performance

🔃 An ordered map in Go with amortized O(1) for Set, Get, Delete and Len.

1,028 stars78 forksGoMIT

At a glance

What is it?
The elliotchance/orderedmap library is a Go ordered map with amortized O(1) Set, Get, Delete and Len, built from a standard map plus a trimmed down linked list, and it exposes the Go 1.23 range-over-function iterators. It is small, MIT licensed, and its main sharp edge is that mutating the map while iterating it has undefined results.
Who is it for?
orderedmap is worth adopting when the order of your map matters, for example a stable display order, a queue that survives deletion, or a JSON object whose key order you do not want to lose, and it costs you one dependency and no API surprises.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 14 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A map and a trimmed down linked list

Go's built in map is fast and gives you average O(1) lookups, but it has no order. Range over a map and the iteration order is unspecified, and it is randomised on purpose to stop code from depending on it. That is the right default and it is exactly the problem this library exists to solve.

The implementation is two structures rather than one. Internally an OrderedMap uses the composite type map combined with a trimmed down linked list to maintain the order. The map holds the values and gives the O(1) lookup, and the linked list holds the ordering and gives the ability to walk the entries forwards and backwards, and to unlink an entry in constant time when it is deleted.

That design is why the complexity claim is amortized rather than worst case. The map operations are the usual average O(1) hash table behaviour, and the linked list work is O(1) per operation, so the amortization comes from the data structure rather than from occasional expensive rebuilds. The documentation claims amortized O(1) for Set, Get, Delete and Len, and that is the whole selling point.

The audience is Go developers who need order and do not want to write the linked list themselves. The classic situations are a map whose key order must be stable for display or for serialisation, a structure where you insert and delete frequently and want to walk what is left in insertion order, and a priority queue that has to preserve the order of equal priorities.

The library is deliberately narrow. It is one type, with no serialisation, no generics beyond the two type parameters, and no extras.

Getting the package and building a map

The module path for the current line is github.com/elliotchance/orderedmap/v3, and the import is the only setup step, since the package is a plain Go module with no code generation and no init function to wire up.

The basic usage from the documentation is short enough to read in one go:

go
import "github.com/elliotchance/orderedmap/v3"

func main() {
	m := orderedmap.NewOrderedMap[string, any]()

	m.Set("foo", "bar")
	m.Set("qux", 1.23)
	m.Set("123", true)

	m.Delete("qux")
}

Two things about that snippet are worth pausing on. The type parameters are the key type and the value type, and the constructor is generic, so an OrderedMap of string to any fixes its key and value types at compile time. And the values are heterogeneous in the example, 1.23 and true alongside a string, which is the any value type doing what any does.

The important semantic point is that Set on an existing key does not move it to the end. The order is insertion order, and a key that is set again keeps its original position. That is the behaviour you want for a stable display list, and it is also the behaviour that surprises people coming from an implementation that treats a re-Set as a re-insert.

Delete removes the key and unlinks it from the list, which is the operation that justifies keeping a list rather than sorting on demand.

Four iterators, and the rule about mutating during a walk

Since Go 1.23 the language has range over function, and the library takes advantage of it. The documentation lists four methods that return iterators: AllFromFront, AllFromBack, Keys and Values. Iterating from the front gives you the oldest to newest order:

go
// Iterate through all elements from oldest to newest:
for key, value := range m.AllFromFront() {
	fmt.Println(key, value)
}

The iterators are safe to use bidirectionally and will return nil once they go beyond the first or last item. That is a slightly unusual contract to state, and it matters for the manual iteration style described further down, where you walk with an element pointer rather than a range loop.

The warning is the one to remember. If the map is changing while the iteration is in flight it may produce unexpected behavior. The documentation does not promise a snapshot, does not promise that you will skip an element, and does not promise a panic. It simply says the result may be unexpected, which in practice means you should not add or delete keys while a range over the map is running.

That is a design decision rather than an oversight. A snapshot iterator would need to copy the whole structure on every walk, which would defeat the reason to use a map based structure in the first place. The cost of the design is that concurrent modification during iteration is on the caller to avoid, and the cost of the alternative would be paid on every iteration, which is the more common operation.

The documentation also does not say anything about using an OrderedMap from several goroutines at once. There is no mention of locking, so the reasonable reading is that it is not safe for concurrent use, and the same applies to the underlying map.

slices.Collect and maps.Collect convert the iterators back

The iterators are designed to compose with the standard library rather than to replace it, and the documentation shows two conversions that cover most cases.

If you want the keys or the values as a slice, you use slices.Collect with the iterator from Keys or Values:

go
fmt.Println(slices.Collect(m.Keys())
// [A B C]

And if you want a plain unordered map back from the ordered one, maps.Collect on the iterator from AllFromFront does it:

go
fmt.Println(maps.Collect(m.AllFromFront())
// [A:1 B:2 C:3]

The second example is a good sanity check on what the library is for. If your code ends with maps.Collect, you have paid for the ordering and then thrown it away, which is fine as an occasional conversion but a sign that the ordered map is not doing work in that path.

The conversion is also the honest way to snapshot the contents. Because mutating during an iteration is undefined, taking a copy with slices.Collect before you start changing things is the pattern that keeps your code predictable.

The library also exposes the raw elements. You can walk the map with Front and Next, and in reverse with Back and Prev:

go
// Iterate through all elements from oldest to newest:
for el := m.Front(); el != nil; el = el.Next() {
    fmt.Println(el.Key, el.Value)
}

// You can also use Back and Prev to iterate in reverse:
for el := m.Back(); el != nil; el = el.Prev() {
    fmt.Println(el.Key, el.Value)
}

This is the style to use when you need the element pointer itself, for instance to remove the current element as you walk, and it is the reason the iterators are documented as returning nil past the ends.

Three major versions for three Go version floors

The version policy here is unusually explicit, and it is the first thing to check before you import anything. The documentation puts it as a note.

v3 requires Go 1.23, and the note says it is the version to use if you are on a current toolchain. If you need to support Go 1.18 to 1.22, you can use v2. And if you need Go 1.17 or below, you can use v1. The reason v2 exists at all is stated: v2 requires Go 1.18 for generics.

That last clause is the interesting one, because generics arrived in Go 1.18, so the version numbering here tracks the language, not just the library API. A v1 user is a pre-generics user, and their ordered map cannot have type parameters at all, which means the API is probably untyped or uses a different shape.

The repository layout matches. There are separate v2 and v3 directories, each a self-contained version of the package, alongside the root which is the v1 code, with orderedmap.go and list.go as the implementation files and orderedmap_test.go beside them. The root go.mod declares the module as github.com/elliotchance/orderedmap with a go 1.12 directive, which is a leftover of the v1 era and tells you the v1 line predates modules being mandatory.

For a new project on a current toolchain, v3 is the only sensible choice. The value of keeping the older lines is for projects stuck on an old Go version, and the cost of the arrangement is that each line needs its own maintenance, and the top level README documents the current one.

A related detail for anyone using a dependency management tool: the module path carries the major version suffix, so importing v3 means importing github.com/elliotchance/orderedmap/v3, and the v2 import path carries its own suffix. Getting that wrong is a compile error rather than a subtle bug, which is a good outcome.

A small library with a small licence and a moving release line

The licence is MIT, in the LICENSE file at the root of the repository, and the repository is not archived with the last push on 2026-10-09.

The release history shows a small project with a clear line. v2.7.0 and v3.1.0 were both published on 2025-01-02, which is the pattern of a maintainer releasing the old and new lines on the same day when the API split, and v3.1.1 followed on 2026-07-11. A patch release more than a year after the last feature release suggests a library that has found its shape and is not churning.

The dependency situation is minimal, which is what you want from a container type. The root go.mod requires only github.com/stretchr/testify for the tests, and the repository ships an orderedmap_test.go, so the test suite is present in the tree rather than only in a separate branch.

The upgrade cost is therefore low, and the risk is not in the code. It is in the version floor, because moving a project to v3 means moving the project to Go 1.23, and that is a bigger change than the library. A team on Go 1.21 can stay on v2 indefinitely, and the v2 line is not going to be removed without a major version bump, but it is also not where new work happens.

The other thing to weigh is what the library does not do. There is no serialisation to JSON, no thread safety wrapper, no support for the sync primitives, no way to change the order of existing keys other than rebuilding, and no documented guarantee about behaviour under concurrent modification. For most uses that is exactly right. A library that did all of those would be a different project with a different cost.

Editorial conclusion

orderedmap is worth adopting when the order of your map matters, for example a stable display order, a queue that survives deletion, or a JSON object whose key order you do not want to lose, and it costs you one dependency and no API surprises. It is the wrong choice for concurrent use, since the documentation says nothing about locking and mutating a map during an in-flight iteration may produce unexpected behaviour, and it is unnecessary if you only need order once at the end, because sorting a slice of keys does the same job. Before you adopt it, check the version you import against your Go toolchain, because v3 requires Go 1.23, v2 covers Go 1.18 to 1.22 and v1 covers Go 1.17 and below, and note that the newest tag is v3.1.1 from 2026-07-11 while the repository was pushed to on 2026-10-09.

Frequently asked questions

How do I create an ordered map in Go with the orderedmap package?

Import github.com/elliotchance/orderedmap/v3 and call orderedmap.NewOrderedMap[string, any](), then use Set, Get, Delete and Len. The type parameters are the key type and the value type, so the key and value types are fixed at compile time.

Which Go version does orderedmap v3 need?

v3 requires Go 1.23 or newer. The documentation says to use v2 if you need to support Go 1.18 to 1.22, and v1 if you need Go 1.17 or below, because v2 is the line that requires Go 1.18 for generics.

How do I iterate over an ordered map in Go?

The library returns Go 1.23 iterators from AllFromFront, AllFromBack, Keys and Values, so you can use a range loop over m.AllFromFront() for key and value pairs. You can also walk it manually with Front and Next, or Back and Prev, and the iterators return nil once they go past the first or last item.

Can I modify the ordered map while iterating it?

The documentation warns that if the map is changing while the iteration is in flight it may produce unexpected behavior, so adding or deleting keys during a range over the map is not safe. If you need a stable view, collect the keys or values first with slices.Collect and work on the copy.

How is the ordered map implemented?

Internally an OrderedMap uses the composite type map combined with a trimmed down linked list to maintain the order, which is how it keeps amortized O(1) for Set, Get, Delete and Len. The implementation lives in orderedmap.go and list.go in the repository, with the versioned copies under the v2 and v3 directories.

Official sources

  1. elliotchance/orderedmap on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/elliotchance-orderedmap.svg)](https://hysenlabs.com/projects/elliotchance-orderedmap)

Was this useful?