前语

大多数Gopher在处理map迭代时,因存在过错的假定,会发生惯性问题。本文首要评论下面两种情况:

  • map迭代时次序
  • 迭代时更新数据

map排序规矩

golang中map有以下根底规矩:

  • map数据不是按key排序的(由于golang中map不是根据二叉树实现的)

  • map中的数据次序与向它里边增加数据时的次序无关,例如,先向map中增加A后增加B, 并能确保map中数据A次序在前B在后

map迭代时次序不确定

当咱们迭代map时,不能对里边的元素次序做任何假定,下面剖析这一原因。下图中的map含有4个bucket,每个bucket指向存储数据的地址。a、c、z、d、e、y别离表明元素的key.

Go言语中常见100问题-#33 map迭代时圈套与解决方法

现在选用for range迭代上述map,打印key和value值。咱们不要预期输出的成果为acdeyz(按key排序), 也不要预期是ayzcde(按刺进排序)。

for k := range m {
    fmt.Print(k)
}

那是按存储的次序aczdey输出吗?不!也不要有这种假定,在Go言语中,即使对同一个map迭代两次,输出的次序也或许是不同的。下面运行两次上述程序的成果,能够看到,一次是zdyaec,另一次是czyade, 成果是不一样的。

NOTE:虽然map的迭代输出没有次序确保,也不能说全体迭代成果是均匀分布的。在Go官方文档中说了map迭代的成果是不确定的,但不是随机的。

Go言语中的map迭代次序为啥要规划成这样呢?这是言语规划者有意为之,他们想增加一些随机性在迭代的时分,使得开发人员在操作map时不要对输出次序抱有期待(mng.bz/M2JW)。

特殊情况:规范库 encoding/json 在序列化map时按key排序

因而,作为一名Gopher,在迭代map时对输出次序不要有任何预期。可是,在使用规范库或外部库时或许有些特别的地方。例如,在使用 encoding/json库将map目标序列化到json时,会依照key的字母次序重新排序数据,不论刺进map时数据次序是什么样的,但这不是Go言语中map自生的特性。假如对次序有强烈要求,咱们应该使用根据二叉堆的数据结构(github.com/emirpasic/g…

下面这段代码将map目标序列化,然后输出序列化后的内容。运行成果能够看到,10次输出的内容都是一模一样的,都是依照key排序的,而不是刺进次序。

func main() {
    m := map[int]string{
        3: "ghi",
        1: "abc",
        2: "def",
    }
for i := 0; i < 10; i++ {
        byte, _ := json.Marshal(m)
        fmt.Println(string(byte))
    }
}

输出成果如下:

{"1":"abc","2":"def","3":"ghi"}
{"1":"abc","2":"def","3":"ghi"}
{"1":"abc","2":"def","3":"ghi"}
{"1":"abc","2":"def","3":"ghi"}
{"1":"abc","2":"def","3":"ghi"}
{"1":"abc","2":"def","3":"ghi"}
{"1":"abc","2":"def","3":"ghi"}
{"1":"abc","2":"def","3":"ghi"}
{"1":"abc","2":"def","3":"ghi"}
{"1":"abc","2":"def","3":"ghi"}

迭代map时更新数据

在Go言语中,在迭代的时分更新map(刺进或删去数据)是允许的,不会发生编译过错和运行时过错。可是有一个点咱们需求考虑,在迭代的时分向map中增加元素,需求避免不可预测性成果。

下面通过一个详细的比如阐明,定义一个map[int]bool目标,遍历的时分假如value为true,增加一个新的元素到map中,猜猜这段代码输出成果是什么?

func main() {
    m := map[int]bool{
        0: true,
        1: false,
        2: true,
    }
    for k, v := range m {
        if v {
            m[k+10] = true
        }
    }
    fmt.Println(m)
}

实际成果:输出是不确定的,屡次运行上述程序,得到的成果如下。

map[0:true 1:false 2:true 10:true 12:true 20:true 22:true 30:true]
map[0:true 1:false 2:true 10:true 12:true 20:true 22:true]
map[0:true 1:false 2:true 10:true 12:true]

为啥不具有唯一性呢?从官方文档中对map迭代时进行更新有这样的阐明。

If a map entry is created during iteration, it may be produced during the iteration or skipped. The choice may vary for each entry created and from one iteration to the next.

因而,在迭代期间向map中增加元素,新增的元素或许在接下来的迭代中拜访到,也或许拜访不到。对于Gopher来说,没办法清晰详细的行为。此外,从一轮迭代到另一个轮迭代,成果也或许不同。正如上述试验证明,屡次执行程序,得到各种不同的成果。这一点需求牢记在心,假如想在迭代的时分更新元素,并且不对迭代有影响,能够选用下面的方法,在进行迭代前,对原始map做个复制,更新元素在新的map中进行。

func copyMap(m map[int]bool) map[int]bool {
    res := make(map[int]bool, len(m))
        for k, v := range m {
        res[k] = v
    }
    return res
}
func main() {
    m := map[int]bool{
        0: true,
        1: false,
        2: true,
    }
    m2 := copyMap(m)
    for k, v := range m {
        m2[k] = v
        if v {
            m2[10+k] = true
        }
    }
    fmt.Println(m2)
}

这段程序的输出具有唯一性,由于迭代是在m进行的,更新元素是在m2上,它们之间不会影响。

map[0:true 1:false 2:true 10:true 12:true]

考虑总结

操作map目标时,咱们需求留意以下几点:

  • map中数据是无序的,不是按key进行排序的

  • map中元素存储次序不是按刺进时先后次序存储的

  • 遍历map输出的元素数据是无序的

  • 遍历map同时向里边新增元素,新增的元素不一定会被拜访到