如何按顺序遍历 Golang 中的 map?

100

请看下面的地图

var romanNumeralDict map[int]string = map[int]string{
  1000: "M",
  900 : "CM",
  500 : "D",
  400 : "CD",
  100 : "C",
  90  : "XC",
  50  : "L",
  40  : "XL",
  10  : "X",
  9   : "IX",
  5   : "V",
  4   : "IV",
  1   : "I",
}
我希望您能够按键的大小顺序循环遍历此映射表。
  for k, v := range romanNumeralDict {
    fmt.Println("k:", k, "v:", v)
  }

然而,这将打印出

k: 1000 v: M
k: 40 v: XL
k: 5 v: V
k: 4 v: IV
k: 900 v: CM
k: 500 v: D
k: 400 v: CD
k: 100 v: C
k: 90 v: XC
k: 50 v: L
k: 10 v: X
k: 9 v: IX
k: 1 v: I
有没有一种方法可以按键的大小顺序打印它们,所以我想像这样循环遍历这个映射。
k:1
K:4
K:5
K:9
k:10

etc...


你需要循环遍历,将这些成对的数据添加到一个切片中,并对该切片进行排序。 - Chris Pfohl
请参见 https://dev59.com/a2ct5IYBdhLWcg3wa8w_。 - Ray Toal
14
根据 spec 的规定,“map 的迭代顺序未指定并且不能保证与下一次迭代相同。” Go 的作者甚至有意随机化迭代顺序(即使用随机数生成器,以使每个范围语句产生不同的顺序),以免任何人错误地依赖于任何迭代顺序。(如果您依赖于每次顺序都不同会发生什么?嗯……) - fuz
2
请参阅 Andrew Gerrand 的 Go maps in action 中的最后一节 迭代顺序 - legends2k
2
可能是按键排序Golang映射值的重复问题 - Jonathan Hall
使用treemap:https://pkg.go.dev/github.com/emirpasic/gods/maps/treemap - wintermute
7个回答

142

收集所有的键,对它们进行排序,并按键来迭代你的 map,像下面这样:

keys := make([]int, 0)
for k, _ := range romanNumeralDict {
    keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
    fmt.Println(k, romanNumeralDict[k])
}

15
如果你的地图使用字符串作为键,那么请使用sort.Strings(keys)进行排序。 - tothemario
8
为了提高性能,代码中使用 keys := make([]int, 0, len(romanNumeralDict)) 来创建一个容量为 romanNumeralDict 长度的 int 类型切片。 - jub0bs

44
你可以通过预先分配keys来加快速度,因为你知道它的长度:
func sortedKeys(m map[Key]Value) ([]Key) {
        keys := make([]Key, len(m))
        i := 0
        for k := range m {
            keys[i] = k
            i++
        }
        sort.Keys(keys)
        return keys
}

KeyValue 替换为您的键和值类型(包括 sort 行)。咳咳,泛型咳咳 编辑:Go 1.18 终于支持泛型了!这是泛型版本:
// Ordered is a type constraint that matches any ordered type.
// An ordered type is one that supports the <, <=, >, and >= operators.
//
// Note the generics proposal suggests this type will be available from
// a standard "constraints" package in future.
type Ordered interface {
    type int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}

func sortedKeys[K Ordered, V any](m map[K]V) ([]K) {
        keys := make([]K, len(m))
        i := 0
        for k := range m {
            keys[i] = k
            i++
        }
        sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
        return keys
}

编辑2:Go 1.21以上,上述的Ordered接口可以用cmp.Ordered替代。 Playground示例

不错的改进。我也想建议同样的事情。甚至可以建议使用一个 咳咳 interface{} _咳咳_,这样就不需要泛型了。 :-) - MikeSchinkel
能否重写代码,使得当 Key 为字符串时,至少 Value 是通用的? - Setjmp
@SetJmp:不是这样的。在Go中,每个map或slice都是不同的类型。你最好接受一个interface{}类型,并使用反射来处理它。 - Ricardo Souza
constraints 包已在 Go 1.18 中确认。 - blackgreen
是我还是只有我遇到了在 int 前的 type 出现错误的问题,提示信息为 expected '}', found 'type'syntax - will.mendil
那段代码是针对预发布版本的,所以可能有些变化。此外,Ordered 已经在标准库中提供,因此您不需要自己定义它。如果您成功运行,请随时更新答案。 - Timmmm

3

您可以使用 MapKeys 获取一个可排序的键数组。

在此示例中,键的类型为 string

keys := reflect.ValueOf(myMap).MapKeys()
keysOrder := func(i, j int) bool { return keys[i].Interface().(string) < keys[j].Interface().(string) }
sort.Slice(keys, keysOrder)

// process map in key-sorted order
for _, key := range keys {
    value := myMap[key.Interface().(string)]
    fmt.Println(key, value)
}
  • 参见:从 map 中获取一部分 key
  • 注意:这会绕过一些编译时类型安全性(如果不是 map,就会 panic)
  • 您需要将每个 key 转换为其原始值才能获取它:key.Interface().(string)

2
顺便提一下,如果没有 sort.Slice(keys, keysOrder),返回的顺序不仅不是排序的,而且在运行之间也不确定。 - k1eran

1

根据@Brent的答案,我有一个场合需要在非关键代码中对排序后的映射键进行操作,而不必重复太多。所以这里提供了一个起点,用于创建适用于许多不同类型的通用映射迭代函数:

func sortedMapIteration(m interface{}, f interface{}) {
    // get value and keys
    val := reflect.ValueOf(m)
    keys := val.MapKeys()
    var sortFunc func(i, j int) bool
    kTyp := val.Type().Key()

    // determine which sorting function to use for the keys based on their types.
    switch {
    case kTyp.Kind() == reflect.Int:
        sortFunc = func(i, j int) bool { return keys[i].Int() < keys[j].Int() }
    case kTyp.Kind() == reflect.String:
        sortFunc = func(i, j int) bool { return keys[i].String() < keys[j].String() }
    }
    sort.Slice(keys, sortFunc)

    // get the function and call it for each key.
    fVal := reflect.ValueOf(f)
    for _, key := range keys {
        value := val.MapIndex(key)
        fVal.Call([]reflect.Value{key, value})
    }
}

// example:
func main() {
    sortedMapIteration(map[string]int{
        "009": 9,
        "003": 3,
        "910": 910,
    }, func(s string, v int) {
        fmt.Println(s, v)
    })
}

playground

需要强调的是:这段代码效率低下且使用了反射技术,因此它没有编译时类型安全性,通用实现应该具有更多的类型保护和处理更多的键类型。然而,对于快速简单的脚本,这可以帮助您入门。根据您期望传递的键类型,您需要向switch块添加更多的情况。


1
您可以通过先显式排序键,然后按键迭代地顺序遍历地图。由于您从罗马数字字典开始就知道键的最终大小,因此预先分配所需大小的数组更有效率。
// Slice for specifying the order of the map.
// It is initially empty but has sufficient capacity
// to hold all the keys of the romanNumeralDict map.
keys := make([]int, 0, len(romanNumeralDict))

// Collect keys of the map
i := 0
for k, _ := range romanNumeralDict {
    keys[i] = k
    i++
}

// Ints sorts a slice of ints in increasing order
sort.Ints(keys)

// Iterate over the map by key with an order
for _, k := range keys {
    fmt.Println(k, romanNumeralDict[k])
}

我不理解 i++。为什么不使用 range 的相应值呢? - guettli

0

使用Go 1.21的简短版本:

m := make(map[string]string)
// fill the map
keys := maps.Keys(m)
slices.Sort(keys)
for k := range kyes {
    fmt.Println(m[k])
}

0
您还可以使用此软件包https://github.com/wk8/go-ordered-map。 关于包的性能/内存使用情况需要进行测试,但它似乎能够满足需求。
它类似于地图,但可以像这样进行迭代:
for pair := om.Oldest(); pair != nil; pair = pair.Next() {
        fmt.Printf("%d => %s\n", pair.Key, pair.Value.payload)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接