golang 神坑range

总结一下碰到的一些range问题,尤其是边loop边delete的骚操作,我一直以为是人家开源库的bug,直到我看到golang官方也这么用。

首先是比较经典的问题,我们团队之前确实有人写这样代码出事了。

v是数据复制

  1. 代码本意是为了获取每个slice每个元素的指针,构造成新的slice
type P struct {  
    Name string
    Age  int
}

func main() {  
    o := []P{
        P{"chain1", 20},
        P{"chain2", 21},
        P{"chain3", 22},
    }
    oPointer := make([]*P, 0, 3)
    for _, v := range o {
        oPointer = append(oPointer, &v)
    }
    fmt.Println(oPointer)
    for _, v := range oPointer {
        fmt.Println(v)
    }
}

我们知道最后oPointer打印出来都是3个同样的 &{"chain3", 22},而这个&v产生的指针也并不是指向原来o中 index = 2的数据,因为range 的时候,v把 o里面的数据copy了一份,由于o[i]是结构体,所以直接copy了结构体数据

  1. 那么第二个问题,类似,想把每一个元素的age都修改成 18(希望每一个看到本文的,都永远18岁)

    type P struct {
    Name string
    Age  int
}

func main() {  
    o := []P{
        P{"chain1", 20},
        P{"chain2", 21},
        P{"chain3", 22},
    }

    for _, v := range o {
        v.Age = 18
    }

    fmt.Println(o)
}

实际上一个都没改,逃逸分析o会被分配到堆上面,而那个v在栈上,每一次都从o[i]对应的堆上面把对象活生生copy过来,然后修改栈上面自己的年龄,对于原slice没有任何改动。

v copy性能

这个copy 使得 对于len比较大并且元素结构体过大的slice,数据结构 range 有性能问题,可以看一下这个case stackoverflow#45786687

主要看下runtime.duffcopy这个函数,在异步抢占,也特地强调了这个函数。

小结

通过上面

  1. 注意到range对于slice来说,最好只用于元素是指针
  2. range千万不可对slice元素进行取地址
  3. 它就是数据的一个副本,如果元素不是指针,只读不要写

神技

删除

通天神技,在dlv项目里面看到的,当时我一度以为是bug

func (t *Target) ClearInternalBreakpoints() error {  
    bpmap := t.Breakpoints()
    threads := t.ThreadList()
    for addr, bp := range bpmap.M {                 
        ...
        delete(bpmap.M, addr)            // 就是这个!!!
    }
    return nil
}

但是你会发现,官网文档介绍for例子就是这么干的!(It is safe)

for key := range m {  
    if key.expired() {
        delete(m, key)
    }
}

其中介绍这样的

The iteration order over maps is not specified and is not guaranteed to be the same from one iteration to the next. If map entries that have not yet been reached are removed during iteration, the corresponding iteration values will not be produced. If map entries are created during iteration, that entry may be produced during the iteration or may be skipped. The choice may vary for each entry created and from one iteration to the next. If the map is nil, the number of iterations is 0.  
新增

恩恩,删除算个啥?for map 来个新增

    data := map[string]string{"1": "A", "2": "B", "3": "C"}
    for k, v := range data {
        data[v] = k
        fmt.Printf("res %v\n", data)
    }

你会发现每次运行的结果都可能不一样,有时候少,有时候多,说明这样遍历打印不是safe。

其实动一下脑子,go的map是hash桶,也就是说 unordered,其中的hash随机种子会影响这个位置(遍历顺序)。

如果你看过hmap源码,就知道了

func makemap(t *maptype, hint int, h *hmap) *hmap {  
        ...
    h.hash0 = fastrand()    // 这个!!!!
        ...
    return h
}